Start refactoring state management
This commit is contained in:
parent
ca0bf943f7
commit
a9ad628488
31 changed files with 410 additions and 360 deletions
|
|
@ -1,6 +1,4 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Cloneable } from './storage/cloneable';
|
|
||||||
import { Root } from './storage/root';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
|
|
@ -9,19 +7,4 @@ import { Root } from './storage/root';
|
||||||
})
|
})
|
||||||
export class AppComponent {
|
export class AppComponent {
|
||||||
title = 'frontend';
|
title = 'frontend';
|
||||||
|
|
||||||
constructor() {
|
|
||||||
const root = new Root<Cloneable>();
|
|
||||||
|
|
||||||
const l1 = new Cloneable(root, 'l1');
|
|
||||||
const r1 = new Cloneable(root, 'r1');
|
|
||||||
const r1r1 = new Cloneable(r1, 'r1r1');
|
|
||||||
const r1l1 = new Cloneable(r1, 'r1l1');
|
|
||||||
|
|
||||||
r1r1.changeName('r1r1 new');
|
|
||||||
|
|
||||||
r1l1.changeName('r1l1 new');
|
|
||||||
|
|
||||||
r1l1.map((c: Cloneable) => c.changeNameMap('bdeiwf'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,12 +8,12 @@
|
||||||
<app-toggle
|
<app-toggle
|
||||||
[beforeText]="'Hide create tower button'"
|
[beforeText]="'Hide create tower button'"
|
||||||
[afterText]="'Show create tower button'"
|
[afterText]="'Show create tower button'"
|
||||||
[default]="!dataService.active.userData?.hideCreateTowerButton"
|
[default]="!page.userData.hideCreateTowerButton"
|
||||||
(value)="dataService.active.userData.hideCreateTowerButton = !$event"
|
(value)="page?.setHideCreateTowerButton(!$event)"
|
||||||
></app-toggle>
|
></app-toggle>
|
||||||
</div>
|
</div>
|
||||||
<!-- wrapper for easier styling -->
|
<!-- wrapper for easier styling -->
|
||||||
|
|
||||||
<p *ngIf="dataService.active?.towers.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
|
<p *ngIf="page.towers?.length == 5">There can be a maximum of <strong>5</strong> towers on each page.</p>
|
||||||
|
|
||||||
<button (click)="deletePage()">Delete current page</button>
|
<button (click)="deletePage()">Delete current page</button>
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ModalService } from '../../../../services/modal.service';
|
import { ModalService } from '../../../../services/modal.service';
|
||||||
import { DataService } from '../../../../services/data.service';
|
import { DataService } from '../../../../services/data.service';
|
||||||
|
import { Page } from '../../../../model/page';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-settings',
|
selector: 'app-settings',
|
||||||
|
|
@ -8,12 +9,16 @@ import { DataService } from '../../../../services/data.service';
|
||||||
styleUrls: ['./settings.component.scss']
|
styleUrls: ['./settings.component.scss']
|
||||||
})
|
})
|
||||||
export class SettingsComponent {
|
export class SettingsComponent {
|
||||||
constructor(public modalService: ModalService, public dataService: DataService) {}
|
constructor(public modalService: ModalService, public dataService: DataService) {
|
||||||
|
this.modalService.active.input.subscribe(p => (this.page = p));
|
||||||
|
}
|
||||||
|
|
||||||
|
page: Page;
|
||||||
|
|
||||||
async deletePage() {
|
async deletePage() {
|
||||||
try {
|
try {
|
||||||
await this.modalService.showRemovePage(this.dataService.active.name);
|
await this.modalService.showRemovePage(this.page.name);
|
||||||
this.dataService.remove();
|
this.dataService.removePage(this.page);
|
||||||
this.modalService.submit();
|
this.modalService.submit();
|
||||||
} catch {
|
} catch {
|
||||||
// pass
|
// pass
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
<button
|
<button *ngIf="page && page.towers.length < 5 && !page.userData?.hideCreateTowerButton" (click)="page.addTower()">
|
||||||
*ngIf="page && page.towers.length < 5 && !dataService.active?.userData?.hideCreateTowerButton"
|
|
||||||
(click)="createTower()"
|
|
||||||
>
|
|
||||||
Create tower
|
Create tower
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,6 @@ export class PageComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
this._page = value;
|
this._page = value;
|
||||||
value.subscribe(() => this.updateDates());
|
|
||||||
this.updateDates();
|
this.updateDates();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,10 +53,6 @@ export class PageComponent {
|
||||||
|
|
||||||
constructor(private modalService: ModalService, public dataService: DataService) {}
|
constructor(private modalService: ModalService, public dataService: DataService) {}
|
||||||
|
|
||||||
createTower() {
|
|
||||||
this.page.addTower();
|
|
||||||
}
|
|
||||||
|
|
||||||
dropDrag(event: any) {
|
dropDrag(event: any) {
|
||||||
this.page.moveTower(event);
|
this.page.moveTower(event);
|
||||||
this.isDragging = false;
|
this.isDragging = false;
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,8 @@
|
||||||
{{ tasks.length == 0 ? '​' : tasks.length == 1 ? 'task' : 'tasks' }}
|
{{ tasks.length == 0 ? '​' : tasks.length == 1 ? 'task' : 'tasks' }}
|
||||||
</p>
|
</p>
|
||||||
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
|
<div class="all-task" #allTask [ngStyle]="{ height: (isOpen ? allTask?.scrollHeight : 0) + 'px' }">
|
||||||
<div class="task-container" *ngFor="let task of tasks" [ngStyle]="{ color: task.color.toString() }">
|
<div class="task-container" *ngFor="let task of tasks" [ngStyle]="{ color: toHslString(task.color) }">
|
||||||
<div [ngStyle]="{ 'background-color': task.color.toString() }"></div>
|
<div [ngStyle]="{ 'background-color': toHslString(task.color) }"></div>
|
||||||
<p (click)="handleClick(task)" [innerText]="task.description ? task.description : 'unknown'"></p>
|
<p (click)="handleClick(task)" [innerText]="task.description ? task.description : 'unknown'"></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { Block } from '../../../../../model/block';
|
||||||
import { Tower } from '../../../../../model/tower';
|
import { Tower } from '../../../../../model/tower';
|
||||||
import { ModalService } from '../../../../../services/modal.service';
|
import { ModalService } from '../../../../../services/modal.service';
|
||||||
import { CancelService } from '../../../../../services/cancel.service';
|
import { CancelService } from '../../../../../services/cancel.service';
|
||||||
|
import { toHslString } from '../../../../../utils/color';
|
||||||
|
import { IColor } from '../../../../../interfaces/persistance/color';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tasks',
|
selector: 'app-tasks',
|
||||||
|
|
@ -10,7 +12,9 @@ import { CancelService } from '../../../../../services/cancel.service';
|
||||||
styleUrls: ['./tasks.component.scss']
|
styleUrls: ['./tasks.component.scss']
|
||||||
})
|
})
|
||||||
export class TasksComponent implements OnInit {
|
export class TasksComponent implements OnInit {
|
||||||
@Input() tasks: Block[];
|
readonly toHslString = toHslString;
|
||||||
|
|
||||||
|
@Input() tasks: Array<Block & { color: IColor }>;
|
||||||
@Input() tower: Tower;
|
@Input() tower: Tower;
|
||||||
|
|
||||||
private _isOpen = false;
|
private _isOpen = false;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,6 @@
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="name…"
|
placeholder="name…"
|
||||||
[(ngModel)]="tower.name"
|
[(ngModel)]="tower.name"
|
||||||
[ngStyle]="{ color: tower.baseColor.toString() }"
|
[ngStyle]="{ color: toHslString(tower?.baseColor) }"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { Component, Input } from '@angular/core';
|
||||||
import { Tower } from '../../../../model/tower';
|
import { Tower } from '../../../../model/tower';
|
||||||
import { ModalService } from '../../../../services/modal.service';
|
import { ModalService } from '../../../../services/modal.service';
|
||||||
import { Block } from '../../../../model/block';
|
import { Block } from '../../../../model/block';
|
||||||
|
import { IColor } from '../../../../interfaces/persistance/color';
|
||||||
|
import { toHslString } from '../../../../utils/color';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-tower',
|
selector: 'app-tower',
|
||||||
|
|
@ -9,6 +11,8 @@ import { Block } from '../../../../model/block';
|
||||||
styleUrls: ['./tower.component.scss']
|
styleUrls: ['./tower.component.scss']
|
||||||
})
|
})
|
||||||
export class TowerComponent {
|
export class TowerComponent {
|
||||||
|
readonly toHslString = toHslString;
|
||||||
|
|
||||||
@Input() set dateRange(value: { from: Date; to: Date }) {
|
@Input() set dateRange(value: { from: Date; to: Date }) {
|
||||||
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
|
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -22,14 +26,14 @@ export class TowerComponent {
|
||||||
|
|
||||||
public constructor(private modalService: ModalService) {}
|
public constructor(private modalService: ModalService) {}
|
||||||
|
|
||||||
get drawableBlocks(): Block[] {
|
get drawableBlocks(): Array<Block & { color: IColor }> {
|
||||||
return this.tower.blocks.filter(
|
return this.tower.coloredBlocks.filter(
|
||||||
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
|
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get tasks(): Block[] {
|
get tasks(): Array<Block & { color: IColor }> {
|
||||||
return this.tower.blocks.filter(
|
return this.tower.coloredBlocks.filter(
|
||||||
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone
|
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<div class="select-add-container">
|
<div class="select-add-container">
|
||||||
<!-- wrapper for easier styling -->
|
<!-- wrapper for easier styling -->
|
||||||
<app-select-add
|
<app-select-add
|
||||||
[options]="dataService.pageNames"
|
[options]="pageNames"
|
||||||
[default]="dataService.active?.name"
|
[default]="selectedPageName"
|
||||||
(value)="selectPage($event)"
|
(value)="selectPage($event)"
|
||||||
[placeholder]="'Add a new page…'"
|
[placeholder]="'Add a new page…'"
|
||||||
></app-select-add>
|
></app-select-add>
|
||||||
|
|
@ -11,11 +11,7 @@
|
||||||
|
|
||||||
<div class="page-container">
|
<div class="page-container">
|
||||||
<!-- wrapper for easier styling -->
|
<!-- wrapper for easier styling -->
|
||||||
<app-page
|
<app-page *ngIf="selectedPage" [page]="selectedPage" (isDragHappening)="isDragHappening = $event"></app-page>
|
||||||
*ngIf="dataService.active !== null"
|
|
||||||
[page]="dataService.active"
|
|
||||||
(isDragHappening)="isDragHappening = $event"
|
|
||||||
></app-page>
|
|
||||||
</div>
|
</div>
|
||||||
<!-- wrapper for easier styling -->
|
<!-- wrapper for easier styling -->
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,10 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
|
||||||
import { Page } from '../../model/page';
|
import { Page } from '../../model/page';
|
||||||
import { DataService } from '../../services/data.service';
|
import { DataService } from '../../services/data.service';
|
||||||
import { ModalService } from '../../services/modal.service';
|
import { ModalService } from '../../services/modal.service';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
|
const USER_DATA_KEY = 'life-towers.user-data.v.2';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-pages',
|
selector: 'app-pages',
|
||||||
|
|
@ -13,28 +17,70 @@ export class PagesComponent {
|
||||||
@ViewChild('page') page: ElementRef;
|
@ViewChild('page') page: ElementRef;
|
||||||
@ViewChild('bottom') bottom: ElementRef;
|
@ViewChild('bottom') bottom: ElementRef;
|
||||||
|
|
||||||
|
pages: Array<Page>;
|
||||||
isDragHappening = false;
|
isDragHappening = false;
|
||||||
|
|
||||||
constructor(public dataService: DataService, private modalService: ModalService) {}
|
get pageNames() {
|
||||||
|
if (this.pages) {
|
||||||
async selectPage(selected: string) {
|
return this.pages.map(p => p.name);
|
||||||
if (!this.dataService.pageNames.includes(selected)) {
|
}
|
||||||
const page = new Page({
|
return [];
|
||||||
name: selected,
|
|
||||||
towers: [],
|
|
||||||
userData: {}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.dataService.push(page);
|
|
||||||
page.addTower();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.dataService.changeActiveByName(selected);
|
get selectedPage(): Page {
|
||||||
|
try {
|
||||||
|
return this.pages[this.pageNames.indexOf(this.selectedPageName)];
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _selectedPageName: string;
|
||||||
|
get selectedPageName(): string {
|
||||||
|
return this._selectedPageName;
|
||||||
|
}
|
||||||
|
|
||||||
|
set selectedPageName(value: string) {
|
||||||
|
window.localStorage.setItem(
|
||||||
|
USER_DATA_KEY,
|
||||||
|
JSON.stringify({
|
||||||
|
selectedPage: value
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this._selectedPageName = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly _selectedPage: BehaviorSubject<Page> = new BehaviorSubject(null);
|
||||||
|
readonly selectedPage$: Observable<Page> = this._selectedPage.asObservable();
|
||||||
|
|
||||||
|
constructor(public dataService: DataService, private modalService: ModalService) {
|
||||||
|
const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
|
||||||
|
if (userData !== null && userData.selectedPage !== undefined) {
|
||||||
|
this._selectedPageName = userData.selectedPage;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dataService.safeChildren$.subscribe(pages => {
|
||||||
|
if (pages) {
|
||||||
|
this.pages = pages;
|
||||||
|
if (!this.selectedPage) {
|
||||||
|
this.selectedPageName = this.pages.length > 0 ? this.pages[0].name : null;
|
||||||
|
}
|
||||||
|
this._selectedPage.next(this.selectedPage);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async selectPage(selected: string) {
|
||||||
|
if (!this.pageNames.includes(selected)) {
|
||||||
|
this.dataService.addPage(selected);
|
||||||
|
}
|
||||||
|
this.selectedPageName = selected;
|
||||||
|
this._selectedPage.next(this.selectedPage);
|
||||||
}
|
}
|
||||||
|
|
||||||
async openSettings() {
|
async openSettings() {
|
||||||
try {
|
try {
|
||||||
await this.modalService.showSettings();
|
await this.modalService.showSettings(this.selectedPage$);
|
||||||
} catch {
|
} catch {
|
||||||
// pass
|
// pass
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ export class ToggleComponent {
|
||||||
@Output() value: EventEmitter<boolean> = new EventEmitter();
|
@Output() value: EventEmitter<boolean> = new EventEmitter();
|
||||||
|
|
||||||
@Input() set default(value: boolean) {
|
@Input() set default(value: boolean) {
|
||||||
this.on = value;
|
this._on = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _on = false;
|
private _on = false;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
export interface IBlock {
|
import { Typed } from './typed';
|
||||||
|
|
||||||
|
export interface IBlock extends Typed {
|
||||||
|
type: 'Block';
|
||||||
created: Date;
|
created: Date;
|
||||||
tag: string;
|
tag: string;
|
||||||
isDone: boolean;
|
isDone: boolean;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
export interface IColor {
|
import { Typed } from './typed';
|
||||||
|
|
||||||
|
export interface IColor extends Typed {
|
||||||
|
type: 'Color';
|
||||||
h: number;
|
h: number;
|
||||||
s: number;
|
s: number;
|
||||||
l: number;
|
l: number;
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,14 @@
|
||||||
import { ITower } from './tower';
|
import { ITower } from './tower';
|
||||||
|
import { Typed } from './typed';
|
||||||
|
|
||||||
export interface IPage {
|
export interface IPage extends Typed {
|
||||||
|
type: 'Page';
|
||||||
name: string;
|
name: string;
|
||||||
towers: ITower[];
|
towers: ITower[];
|
||||||
|
|
||||||
userData: {
|
userData: {
|
||||||
hideCreateTowerButton: boolean;
|
hideCreateTowerButton?: boolean;
|
||||||
defaultDateRange: {
|
defaultDateRange?: {
|
||||||
from: Date;
|
from: Date;
|
||||||
to: Date;
|
to: Date;
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
import { IBlock } from './block';
|
import { IBlock } from './block';
|
||||||
import { IColor } from './color';
|
import { IColor } from './color';
|
||||||
|
import { Typed } from './typed';
|
||||||
|
|
||||||
export interface ITower {
|
export interface ITower extends Typed {
|
||||||
|
type: 'Tower';
|
||||||
name: string;
|
name: string;
|
||||||
blocks: IBlock[];
|
blocks: IBlock[];
|
||||||
baseColor: IColor;
|
baseColor: IColor;
|
||||||
|
|
|
||||||
3
src/app/interfaces/persistance/typed.ts
Normal file
3
src/app/interfaces/persistance/typed.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export interface Typed {
|
||||||
|
type: string;
|
||||||
|
}
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
import { Subject } from 'rxjs/internal/Subject';
|
|
||||||
|
|
||||||
export class Base {
|
|
||||||
private static propertyList: any = {};
|
|
||||||
protected subscribers: (() => void)[] = [];
|
|
||||||
|
|
||||||
subject: Subject<this> = new Subject();
|
|
||||||
|
|
||||||
constructor(properties: any) {
|
|
||||||
const type = this.constructor.name;
|
|
||||||
if (!Base.propertyList.hasOwnProperty(type)) {
|
|
||||||
Base.propertyList[type] = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const property in properties) {
|
|
||||||
if (properties.hasOwnProperty(property)) {
|
|
||||||
const propertyName = `__${property}`;
|
|
||||||
this[propertyName] = properties[property];
|
|
||||||
|
|
||||||
Object.defineProperty(this, property, {
|
|
||||||
get: () => this[propertyName],
|
|
||||||
set: value => {
|
|
||||||
if (value == this[propertyName]) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this[propertyName] = value;
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!Base.propertyList[type].includes(property)) {
|
|
||||||
Base.propertyList[type].push(property);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toJSON(): object {
|
|
||||||
return Base.propertyList[this.constructor.name].reduce(
|
|
||||||
(object, property) => ({
|
|
||||||
[property]: this[property],
|
|
||||||
...object
|
|
||||||
}),
|
|
||||||
// TODO
|
|
||||||
{ type: this.constructor.name }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(func: () => void) {
|
|
||||||
this.subscribers.push(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected update() {
|
|
||||||
this.subject.next(this);
|
|
||||||
this.subscribers.map(f => f());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,26 +1,18 @@
|
||||||
import { Base } from './base';
|
import { Serializable } from './serializable';
|
||||||
import { IBlock } from '../interfaces/persistance/block';
|
import { IBlock } from '../interfaces/persistance/block';
|
||||||
import { Color } from './color';
|
import { Node } from '../storage/node';
|
||||||
|
|
||||||
export class Block extends Base implements IBlock {
|
export class Block extends Serializable implements IBlock {
|
||||||
constructor(props: IBlock) {
|
constructor(parent: Node, props: IBlock) {
|
||||||
// TODO: remove
|
super(parent, props);
|
||||||
if (props.isDone === undefined) {
|
|
||||||
props.isDone = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
|
|
||||||
if (this.created.constructor.name !== 'Date') {
|
if (this.created.constructor.name !== 'Date') {
|
||||||
// Prevent update message
|
this.created = new Date(this.created);
|
||||||
// @ts-ignore
|
|
||||||
this.__created = new Date(this.created);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
color: Color;
|
|
||||||
|
|
||||||
// Only here to prevent ts warnings.
|
// Only here to prevent ts warnings.
|
||||||
|
type: 'Block';
|
||||||
created: Date;
|
created: Date;
|
||||||
isDone: boolean;
|
isDone: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
|
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import { IColor } from '../interfaces/persistance/color';
|
|
||||||
import { Base } from './base';
|
|
||||||
|
|
||||||
export class Color extends Base implements IColor {
|
|
||||||
constructor(props: IColor) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only here to prevent ts warnings.
|
|
||||||
h: number;
|
|
||||||
s: number;
|
|
||||||
l: number;
|
|
||||||
|
|
||||||
public lighten(by: number) {
|
|
||||||
const newL = this.l + by;
|
|
||||||
if (this.l > 100) {
|
|
||||||
this.l = 100;
|
|
||||||
} else if (this.l < 0) {
|
|
||||||
this.l = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Color({ h: this.h, s: this.s, l: newL });
|
|
||||||
}
|
|
||||||
|
|
||||||
public toString(): string {
|
|
||||||
return `hsl(${this.h}, ${this.s}%, ${this.l}%)`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +1,20 @@
|
||||||
import { Base } from './base';
|
import { Serializable } from './serializable';
|
||||||
import { IPage } from '../interfaces/persistance/page';
|
import { IPage } from '../interfaces/persistance/page';
|
||||||
import { Tower } from './tower';
|
import { Tower } from './tower';
|
||||||
import { ITower } from '../interfaces/persistance/tower';
|
import { Node } from '../storage/node';
|
||||||
|
|
||||||
export class Page extends Base implements IPage {
|
export class Page extends Serializable implements IPage {
|
||||||
constructor(props) {
|
constructor(parent: Node, props: IPage) {
|
||||||
// TODO: remove
|
super(parent, props);
|
||||||
if (!props.userData) {
|
|
||||||
props.userData = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
super(props);
|
|
||||||
// @ts-ignore to prevent update message
|
|
||||||
this.__towers = this.towers.map(t => this.createTower(t));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only here to prevent ts warnings.
|
// Only here to prevent ts warnings.
|
||||||
name: string;
|
name: string;
|
||||||
towers: Tower[];
|
get towers(): Array<Tower> {
|
||||||
|
return this.children as Array<Tower>;
|
||||||
|
}
|
||||||
|
type: 'Page';
|
||||||
|
|
||||||
userData: {
|
userData: {
|
||||||
hideCreateTowerButton: boolean;
|
hideCreateTowerButton: boolean;
|
||||||
defaultDateRange: {
|
defaultDateRange: {
|
||||||
|
|
@ -26,15 +23,26 @@ export class Page extends Base implements IPage {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setHideCreateTowerButton(value: boolean) {
|
||||||
|
this.changeKey({
|
||||||
|
propertyName: 'userData',
|
||||||
|
value: {
|
||||||
|
...this.userData,
|
||||||
|
hideCreateTowerButton: value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
moveTower({ previousIndex, currentIndex }: { previousIndex: number; currentIndex: number }) {
|
moveTower({ previousIndex, currentIndex }: { previousIndex: number; currentIndex: number }) {
|
||||||
if (previousIndex === currentIndex) {
|
if (previousIndex === currentIndex) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tower = this.towers[previousIndex];
|
this.map(page => {
|
||||||
this.towers.splice(previousIndex, 1);
|
const tower = page.towers[previousIndex];
|
||||||
this.towers.splice(currentIndex, 0, tower);
|
page.towers.splice(previousIndex, 1);
|
||||||
this.update();
|
page.towers.splice(currentIndex, 0, tower);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
addTower(name = '') {
|
addTower(name = '') {
|
||||||
|
|
@ -43,24 +51,18 @@ export class Page extends Base implements IPage {
|
||||||
hue = Math.random() * 360;
|
hue = Math.random() * 360;
|
||||||
} while (30 <= hue && hue <= 200);
|
} while (30 <= hue && hue <= 200);
|
||||||
|
|
||||||
this.towers.push(
|
new Tower(this, {
|
||||||
this.createTower({
|
type: 'Tower',
|
||||||
name,
|
name,
|
||||||
blocks: [],
|
blocks: [],
|
||||||
baseColor: { h: hue, s: 100, l: 50 }
|
baseColor: { h: hue, s: 100, l: 50, type: 'Color' }
|
||||||
})
|
});
|
||||||
);
|
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
private createTower(props: ITower): Tower {
|
|
||||||
const tower = new Tower(props);
|
|
||||||
tower.subscribe(() => this.update());
|
|
||||||
return tower;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTower(tower: Tower) {
|
removeTower(tower: Tower) {
|
||||||
this.towers = this.towers.filter(t => t !== tower);
|
this.changeValue({
|
||||||
|
oldValue: this.towers,
|
||||||
|
newValue: this.towers.filter(t => t !== tower)
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
51
src/app/model/serializable.ts
Normal file
51
src/app/model/serializable.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { Cloneable } from '../storage/cloneable';
|
||||||
|
import { Node } from '../storage/node';
|
||||||
|
|
||||||
|
export class Serializable extends Cloneable {
|
||||||
|
type: string;
|
||||||
|
private static propertyList: any = {};
|
||||||
|
static childrenMap: {
|
||||||
|
[type: string]: {
|
||||||
|
childrenConstructor: typeof Serializable;
|
||||||
|
childrenListName: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor(parent: Node, properties: any) {
|
||||||
|
super(parent);
|
||||||
|
|
||||||
|
const type = this.constructor.name;
|
||||||
|
if (!Serializable.propertyList.hasOwnProperty(type)) {
|
||||||
|
Serializable.propertyList[type] = [];
|
||||||
|
}
|
||||||
|
for (const property in properties) {
|
||||||
|
if (properties.hasOwnProperty(property)) {
|
||||||
|
const propertyValue = properties[property];
|
||||||
|
if (property === Serializable.childrenMap[type].childrenListName) {
|
||||||
|
// This should be ran after the original constructor has finished.
|
||||||
|
new Promise(r => r()).then(() => {
|
||||||
|
for (let child of propertyValue) {
|
||||||
|
new Serializable.childrenMap[type].childrenConstructor(this, child);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this[property] = properties[property];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Serializable.propertyList[type].includes(property)) {
|
||||||
|
Serializable.propertyList[type].push(property);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toJSON(): object {
|
||||||
|
return Serializable.propertyList[this.constructor.name].reduce(
|
||||||
|
(object, property) => ({
|
||||||
|
[property]: this[property],
|
||||||
|
...object
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,54 +1,51 @@
|
||||||
import { ITower } from '../interfaces/persistance/tower';
|
import { ITower } from '../interfaces/persistance/tower';
|
||||||
import { Color } from './color';
|
import { lighten } from '../utils/color';
|
||||||
import { Block } from './block';
|
import { Block } from './block';
|
||||||
import { Base } from './base';
|
import { Serializable } from './serializable';
|
||||||
import { IBlock } from '../interfaces/persistance/block';
|
|
||||||
import { hash } from '../utils/hash';
|
import { hash } from '../utils/hash';
|
||||||
|
import { Node } from '../storage/node';
|
||||||
|
import { IColor } from '../interfaces/persistance/color';
|
||||||
|
|
||||||
export class Tower extends Base implements ITower {
|
export class Tower extends Serializable implements ITower {
|
||||||
constructor(props: ITower) {
|
constructor(parent: Node, props: ITower) {
|
||||||
super(props);
|
super(parent, props);
|
||||||
|
|
||||||
// @ts-ignore to prevent update message
|
|
||||||
this.__baseColor = new Color(this.baseColor);
|
|
||||||
|
|
||||||
this.blocks = this.blocks.map(b => this.createBlock(b));
|
|
||||||
this.blocks.sort((a, b) => a.created.getTime() - b.created.getTime());
|
this.blocks.sort((a, b) => a.created.getTime() - b.created.getTime());
|
||||||
|
this.calculateTagList();
|
||||||
}
|
}
|
||||||
|
|
||||||
tags: string[];
|
tags: string[];
|
||||||
|
|
||||||
// Only here to prevent ts warnings.
|
// Only here to prevent ts warnings.
|
||||||
name: string;
|
name: string;
|
||||||
blocks: Block[];
|
type: 'Tower';
|
||||||
baseColor: Color;
|
get blocks(): Array<Block> {
|
||||||
|
return this.children as Array<Block>;
|
||||||
|
}
|
||||||
|
baseColor: IColor;
|
||||||
|
|
||||||
|
get coloredBlocks(): Array<Block & { color: IColor }> {
|
||||||
|
return this.children.map(b => {
|
||||||
|
const coloredBlock = b as Block & { color: IColor };
|
||||||
|
coloredBlock.color = lighten((hash(coloredBlock.tag) - 0.5) * 50, this.baseColor);
|
||||||
|
return coloredBlock;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
addBlock(props: { tag: string; description: string; isDone: boolean }) {
|
addBlock(props: { tag: string; description: string; isDone: boolean }) {
|
||||||
this.blocks.push(
|
new Block(this, {
|
||||||
this.createBlock({
|
|
||||||
created: new Date(),
|
created: new Date(),
|
||||||
...props
|
...props,
|
||||||
})
|
type: 'Block'
|
||||||
);
|
});
|
||||||
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private createBlock(props: IBlock): Block {
|
private calculateTagList() {
|
||||||
const block = new Block(props);
|
|
||||||
block.subscribe(() => this.update());
|
|
||||||
return block;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected update() {
|
|
||||||
this.tags = [];
|
this.tags = [];
|
||||||
for (const block of this.blocks) {
|
for (const block of this.blocks) {
|
||||||
if (!this.tags.includes(block.tag)) {
|
if (!this.tags.includes(block.tag)) {
|
||||||
this.tags.push(block.tag);
|
this.tags.push(block.tag);
|
||||||
}
|
}
|
||||||
block.color = this.baseColor.lighten(hash(block.tag) * 50);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
super.update();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,93 +1,69 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { StoreService } from './store.service';
|
import { StoreService } from './store.service';
|
||||||
import { Page } from '../model/page';
|
import { Page } from '../model/page';
|
||||||
|
import { Root } from '../storage/root';
|
||||||
const USER_DATA_KEY = 'life-towers.user-data.v.1';
|
import { Serializable } from '../model/serializable';
|
||||||
|
import { Tower } from '../model/tower';
|
||||||
|
import { Block } from '../model/block';
|
||||||
|
import { IPage } from '../interfaces/persistance/page';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class DataService {
|
export class DataService extends Root<Page> {
|
||||||
get active(): Page {
|
get pages(): Array<Page> {
|
||||||
return this._active;
|
return this.children;
|
||||||
}
|
}
|
||||||
|
|
||||||
get pageNames(): string[] {
|
private readonly _safeChildren: BehaviorSubject<Array<Page>> = new BehaviorSubject(null);
|
||||||
return this.data.map(p => p.name);
|
readonly safeChildren$: Observable<Array<Page>> = this._safeChildren.asObservable();
|
||||||
}
|
|
||||||
|
|
||||||
private subscribers: (() => void)[] = [];
|
constructor(private storeService: StoreService<Array<IPage>>) {
|
||||||
private _active: Page = null;
|
super();
|
||||||
private data: Page[];
|
this.init().catch();
|
||||||
private hasLoaded = new Promise(resolve => (this.afterLoadFinished = resolve));
|
|
||||||
private afterLoadFinished: () => void;
|
|
||||||
|
|
||||||
constructor(private storeService: StoreService) {
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
push(value: Page) {
|
|
||||||
value.subscribe(() => this.save());
|
|
||||||
this.data.push(value);
|
|
||||||
this._active = value;
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
remove() {
|
|
||||||
this.data = this.data.filter(p => p !== this.active);
|
|
||||||
this._active = this.data.length > 0 ? this.data[0] : null;
|
|
||||||
this.save();
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribe(func: () => void) {
|
|
||||||
this.subscribers.push(func);
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeActiveByName(name: string): Promise<void> {
|
|
||||||
await this.hasLoaded;
|
|
||||||
this._active = this.data.filter(p => p.name === name)[0];
|
|
||||||
this.saveActiveIndex(this.data.indexOf(this.active));
|
|
||||||
this.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
async changeActiveByIndex(index: number): Promise<void> {
|
|
||||||
await this.hasLoaded;
|
|
||||||
this._active = this.data[index];
|
|
||||||
this.saveActiveIndex(index);
|
|
||||||
this.update();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async init() {
|
private async init() {
|
||||||
this.data = await this.storeService.load();
|
const pages = await this.storeService.load();
|
||||||
this.data.map(p => p.subscribe(() => this.save()));
|
Serializable.childrenMap = {
|
||||||
this._active = this.data.length > 0 ? this.data[0] : null;
|
Page: {
|
||||||
this.loadActiveIndex();
|
childrenListName: 'towers',
|
||||||
this.afterLoadFinished();
|
childrenConstructor: Tower
|
||||||
|
},
|
||||||
|
Tower: {
|
||||||
|
childrenListName: 'blocks',
|
||||||
|
childrenConstructor: Block
|
||||||
|
},
|
||||||
|
Block: {
|
||||||
|
childrenListName: null,
|
||||||
|
childrenConstructor: null
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (let page of pages) {
|
||||||
|
new Page(this, page);
|
||||||
|
}
|
||||||
|
this.children$.subscribe(value => {
|
||||||
|
this.log();
|
||||||
|
this._safeChildren.next(value);
|
||||||
|
this.storeService.scheduleSave(this.pages);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private save() {
|
addPage(name: string) {
|
||||||
this.storeService.save(this.data);
|
new Page(this, {
|
||||||
this.update();
|
name,
|
||||||
|
userData: {},
|
||||||
|
type: 'Page',
|
||||||
|
towers: []
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private update() {
|
removePage(page: Page) {
|
||||||
this.subscribers.map(f => f());
|
this.changeValue({
|
||||||
}
|
oldValue: this.children,
|
||||||
|
newValue: this.children.filter(c => c !== page)
|
||||||
private loadActiveIndex() {
|
});
|
||||||
const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
|
|
||||||
if (userData === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._active = this.data[userData.index];
|
|
||||||
}
|
|
||||||
|
|
||||||
private saveActiveIndex(index: number) {
|
|
||||||
window.localStorage.setItem(
|
|
||||||
USER_DATA_KEY,
|
|
||||||
JSON.stringify({
|
|
||||||
index
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
|
||||||
import { Tower } from '../model/tower';
|
import { Tower } from '../model/tower';
|
||||||
import { top } from '../utils/top';
|
import { top } from '../utils/top';
|
||||||
import { CancelService } from './cancel.service';
|
import { CancelService } from './cancel.service';
|
||||||
|
import { Page } from '../model/page';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
|
||||||
export enum ModalType {
|
export enum ModalType {
|
||||||
createBlock,
|
createBlock,
|
||||||
|
|
@ -44,8 +46,8 @@ export class ModalService {
|
||||||
return this.createPromiseAndPushToStack(data, ModalType.editBlock);
|
return this.createPromiseAndPushToStack(data, ModalType.editBlock);
|
||||||
}
|
}
|
||||||
|
|
||||||
showSettings(): Promise<void> {
|
showSettings(selectedPage: Observable<Page>): Promise<void> {
|
||||||
return this.createPromiseAndPushToStack(null, ModalType.settings);
|
return this.createPromiseAndPushToStack(selectedPage, ModalType.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
showRemoveTower(tower: Tower): Promise<void> {
|
showRemoveTower(tower: Tower): Promise<void> {
|
||||||
|
|
|
||||||
|
|
@ -1,82 +1,98 @@
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Page } from '../model/page';
|
import { Page } from '../model/page';
|
||||||
|
import { IPage } from '../interfaces/persistance/page';
|
||||||
|
|
||||||
const LOCAL_STORAGE_KEY = 'life-towers.data.v.2';
|
const LOCAL_STORAGE_KEY = 'life-towers.data.v.3';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
providedIn: 'root'
|
providedIn: 'root'
|
||||||
})
|
})
|
||||||
export class StoreService {
|
export class StoreService<T> {
|
||||||
private storedData: Page[];
|
private saveScheduled = false;
|
||||||
|
private dataToSave: T;
|
||||||
|
private storedData: T;
|
||||||
private mockData: string = JSON.stringify([
|
private mockData: string = JSON.stringify([
|
||||||
{
|
{
|
||||||
name: 'Work & life',
|
name: 'Work & life',
|
||||||
userData: {},
|
userData: {},
|
||||||
|
type: 'Page',
|
||||||
towers: [
|
towers: [
|
||||||
{
|
{
|
||||||
name: 'work',
|
name: 'work',
|
||||||
baseColor: { h: 0, s: 100, l: 50 },
|
baseColor: { h: 0, s: 100, l: 50, type: 'Color' },
|
||||||
|
type: 'Tower',
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
created: new Date(2015, 2, 13),
|
created: new Date(2015, 2, 13),
|
||||||
tag: 'a',
|
tag: 'a',
|
||||||
description: 'done it',
|
description: 'done it',
|
||||||
isDone: true
|
isDone: true,
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2016, 2, 15),
|
created: new Date(2016, 2, 15),
|
||||||
tag: 'go to school',
|
tag: 'go to school',
|
||||||
description: 'done it'
|
description: 'done it',
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2017, 2, 15),
|
created: new Date(2017, 2, 15),
|
||||||
tag: 'go to work',
|
tag: 'go to work',
|
||||||
isDone: true
|
isDone: true,
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2018, 2, 13),
|
created: new Date(2018, 2, 13),
|
||||||
tag: 'go to work',
|
tag: 'go to work',
|
||||||
description: 'done it',
|
description: 'done it',
|
||||||
isDone: true
|
isDone: true,
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2019, 3, 13),
|
created: new Date(2019, 3, 13),
|
||||||
tag: 'go to work'
|
tag: 'go to work',
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2020, 2, 15),
|
created: new Date(2020, 2, 15),
|
||||||
tag: 'go to school',
|
tag: 'go to school',
|
||||||
description: 'done it',
|
description: 'done it',
|
||||||
isDone: true
|
isDone: true,
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2021, 2, 15),
|
created: new Date(2021, 2, 15),
|
||||||
tag: 'go to school'
|
tag: 'go to school',
|
||||||
|
type: 'Block'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
baseColor: { h: 180, s: 100, l: 50 },
|
baseColor: { h: 180, s: 100, l: 50, type: 'Color' },
|
||||||
name: 'life',
|
name: 'life',
|
||||||
|
type: 'Tower',
|
||||||
blocks: [
|
blocks: [
|
||||||
{
|
{
|
||||||
created: new Date(2019, 3, 13),
|
created: new Date(2019, 3, 13),
|
||||||
tag: 'go home',
|
tag: 'go home',
|
||||||
description: 'done it'
|
description: 'done it',
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2019, 4, 13),
|
created: new Date(2019, 4, 13),
|
||||||
tag: 'go home'
|
tag: 'go home',
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2019, 4, 15),
|
created: new Date(2019, 4, 15),
|
||||||
tag: 'go to work',
|
tag: 'go to work',
|
||||||
description: 'done it'
|
description: 'done it',
|
||||||
|
type: 'Block'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
created: new Date(2019, 4, 15, 14),
|
created: new Date(2019, 4, 15, 14),
|
||||||
tag: 'go to work'
|
tag: 'go to work',
|
||||||
|
type: 'Block'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -86,17 +102,30 @@ export class StoreService {
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||||
const data = JSON.parse(localStorageData ? localStorageData : this.mockData);
|
this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
|
||||||
this.storedData = data.map(p => new Page(p));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(): Promise<Page[]> {
|
scheduleSave(data: T) {
|
||||||
|
this.dataToSave = data;
|
||||||
|
if (!this.saveScheduled) {
|
||||||
|
this.saveScheduled = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.saveScheduled = false;
|
||||||
|
this.save(this.dataToSave).catch();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<T> {
|
||||||
|
console.log('load', this.storedData);
|
||||||
return this.storedData;
|
return this.storedData;
|
||||||
}
|
}
|
||||||
|
|
||||||
async save(data: Page[]) {
|
async save(data: T) {
|
||||||
this.storedData = data;
|
this.storedData = data;
|
||||||
console.log('save', this.storedData);
|
const stringified = JSON.stringify(this.storedData, null, 2);
|
||||||
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.storedData));
|
console.log('save');
|
||||||
|
// console.log('save', stringified);
|
||||||
|
window.localStorage.setItem(LOCAL_STORAGE_KEY, stringified);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,23 +2,10 @@ import { InnerNode } from './inner-node';
|
||||||
import { Node } from './node';
|
import { Node } from './node';
|
||||||
|
|
||||||
export class Cloneable extends InnerNode {
|
export class Cloneable extends InnerNode {
|
||||||
name;
|
constructor(parent: Node) {
|
||||||
constructor(parent: Node, name: any) {
|
|
||||||
super(parent);
|
super(parent);
|
||||||
this.name = name;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
changeNameMap = (newValue: string) => {
|
|
||||||
this.name = newValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
changeName = (newValue: any) => {
|
|
||||||
this.changeValue({
|
|
||||||
oldValue: this.name,
|
|
||||||
newValue
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
protected cloneWithMap(map: (node: this) => void): this {
|
protected cloneWithMap(map: (node: this) => void): this {
|
||||||
const insides = Object.getOwnPropertyDescriptors(this);
|
const insides = Object.getOwnPropertyDescriptors(this);
|
||||||
|
|
||||||
|
|
@ -27,11 +14,19 @@ export class Cloneable extends InnerNode {
|
||||||
if (prop == '__target__') {
|
if (prop == '__target__') {
|
||||||
return target;
|
return target;
|
||||||
}
|
}
|
||||||
|
if (target.hasOwnProperty(prop)) {
|
||||||
const value = target[prop as string].value;
|
const value = target[prop as string].value;
|
||||||
if (typeof value === 'function') {
|
if (typeof value === 'function') {
|
||||||
return value.bind(proxy);
|
return value.bind(proxy);
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
|
} else if (this.hasOwnProperty(prop)) {
|
||||||
|
const value = this[prop];
|
||||||
|
if (typeof value === 'function') {
|
||||||
|
return value.bind(proxy);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
},
|
},
|
||||||
set: (target, prop, value) => {
|
set: (target, prop, value) => {
|
||||||
return (target[prop as string].value = value);
|
return (target[prop as string].value = value);
|
||||||
|
|
@ -39,6 +34,10 @@ export class Cloneable extends InnerNode {
|
||||||
});
|
});
|
||||||
map(<any>insidesProxy);
|
map(<any>insidesProxy);
|
||||||
|
|
||||||
|
(<any>insidesProxy.__target__).id.value = Node.id++;
|
||||||
|
(<any>insidesProxy.__target__).copyCount.value++;
|
||||||
|
Node.sumCopyCount++;
|
||||||
|
|
||||||
return Object.create(Object.getPrototypeOf(this), <any>insidesProxy.__target__);
|
return Object.create(Object.getPrototypeOf(this), <any>insidesProxy.__target__);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,6 +45,8 @@ export class Cloneable extends InnerNode {
|
||||||
const insides = Object.getOwnPropertyDescriptors(this);
|
const insides = Object.getOwnPropertyDescriptors(this);
|
||||||
insides[propertyName].value = value;
|
insides[propertyName].value = value;
|
||||||
insides.id.value = Node.id++;
|
insides.id.value = Node.id++;
|
||||||
|
insides.copyCount.value++;
|
||||||
|
Node.sumCopyCount++;
|
||||||
|
|
||||||
return Object.create(Object.getPrototypeOf(this), insides);
|
return Object.create(Object.getPrototypeOf(this), insides);
|
||||||
}
|
}
|
||||||
|
|
@ -53,6 +54,8 @@ export class Cloneable extends InnerNode {
|
||||||
protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
|
protected cloneWithModify({ oldValue, newValue }: { oldValue: any; newValue: any }): this {
|
||||||
const insides = Object.getOwnPropertyDescriptors(this);
|
const insides = Object.getOwnPropertyDescriptors(this);
|
||||||
insides.id.value = Node.id++;
|
insides.id.value = Node.id++;
|
||||||
|
insides.copyCount.value++;
|
||||||
|
Node.sumCopyCount++;
|
||||||
|
|
||||||
let wasMatch = false;
|
let wasMatch = false;
|
||||||
for (let name in insides) {
|
for (let name in insides) {
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ export abstract class InnerNode extends Node {
|
||||||
return this.update((self: this) => this.cloneWithMap.call(self, map));
|
return this.update((self: this) => this.cloneWithMap.call(self, map));
|
||||||
}
|
}
|
||||||
|
|
||||||
changeKey(update: { value: any; propertyName: string }): this {
|
changeKey(update: { propertyName: string; value: any }): this {
|
||||||
return this.update((self: this) => this.cloneWithAdd.call(self, update));
|
return this.update((self: this) => this.cloneWithAdd.call(self, update));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,17 @@
|
||||||
import { InnerNode } from './inner-node';
|
import { InnerNode } from './inner-node';
|
||||||
|
|
||||||
export abstract class Node {
|
export abstract class Node {
|
||||||
public static id = 0;
|
protected static id = 0;
|
||||||
|
protected static sumCopyCount = 0;
|
||||||
protected abstract readonly children: Array<InnerNode>;
|
protected abstract readonly children: Array<InnerNode>;
|
||||||
private id = Node.id++;
|
private id = Node.id++;
|
||||||
|
protected copyCount = 1;
|
||||||
|
|
||||||
changeKey(update: { value: any; propertyName: string }) {
|
abstract changeKey(update: { propertyName: string; value: any });
|
||||||
throw new TypeError('Not implemented!');
|
abstract changeValue(update: { oldValue: any; newValue: any });
|
||||||
}
|
|
||||||
|
|
||||||
changeValue(update: { oldValue: any; newValue: any }) {
|
constructor() {
|
||||||
throw new TypeError('Not implemented!');
|
Node.sumCopyCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
addChild(update: { value: InnerNode }) {
|
addChild(update: { value: InnerNode }) {
|
||||||
|
|
@ -26,4 +27,18 @@ export abstract class Node {
|
||||||
newValue: this.children.map(c => (c === oldValue ? newValue : c))
|
newValue: this.children.map(c => (c === oldValue ? newValue : c))
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _log(indent = ''): string {
|
||||||
|
const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`;
|
||||||
|
let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}siblings: ${this.copyCount}\n`;
|
||||||
|
for (let c of this.children) {
|
||||||
|
response += `${c._log(indent + ' ')}`;
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
public log() {
|
||||||
|
console.log(this._log());
|
||||||
|
console.log(`All in all, there are ${Node.sumCopyCount} objects.`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,22 @@ export class Root<T extends InnerNode> extends Node {
|
||||||
}
|
}
|
||||||
|
|
||||||
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
|
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
|
||||||
if (this.children === oldValue) {
|
if (this.children !== oldValue) {
|
||||||
|
throw new TypeError('Only children can be changed.');
|
||||||
|
}
|
||||||
this.children = newValue;
|
this.children = newValue;
|
||||||
for (let child of this.children) {
|
for (let child of this.children) {
|
||||||
child.parent = this;
|
child.parent = this;
|
||||||
}
|
}
|
||||||
} else {
|
}
|
||||||
|
|
||||||
|
changeKey({ propertyName, value }: { propertyName: string; value: any }) {
|
||||||
|
if (propertyName !== 'children') {
|
||||||
throw new TypeError('Only children can be changed.');
|
throw new TypeError('Only children can be changed.');
|
||||||
}
|
}
|
||||||
|
this.children = value;
|
||||||
|
for (let child of this.children) {
|
||||||
|
child.parent = this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
16
src/app/utils/color.ts
Normal file
16
src/app/utils/color.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { IColor } from '../interfaces/persistance/color';
|
||||||
|
|
||||||
|
export const lighten = (by: number, { h, s, l }: IColor): IColor => {
|
||||||
|
let newL = l + by;
|
||||||
|
if (newL > 100) {
|
||||||
|
newL = 100;
|
||||||
|
} else if (newL < 0) {
|
||||||
|
newL = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { h, s, l: newL, type: 'Color' };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toHslString = ({ h, s, l }: IColor): string => {
|
||||||
|
return `hsl(${h}, ${s}%, ${l}%)`;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue