Start refactoring state management

This commit is contained in:
schmelczerandras 2019-08-31 21:21:21 +02:00
parent ca0bf943f7
commit a9ad628488
31 changed files with 410 additions and 360 deletions

View file

@ -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'));
}
} }

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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;

View file

@ -6,8 +6,8 @@
{{ tasks.length == 0 ? '&#8203;' : tasks.length == 1 ? 'task' : 'tasks' }} {{ tasks.length == 0 ? '&#8203;' : 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>

View file

@ -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;

View file

@ -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>

View file

@ -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
); );
} }

View file

@ -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 -->

View file

@ -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
} }

View file

@ -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;

View file

@ -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;

View file

@ -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;

View file

@ -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;
}; };

View file

@ -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;

View file

@ -0,0 +1,3 @@
export interface Typed {
type: string;
}

View file

@ -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());
}
}

View file

@ -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;

View file

@ -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}%)`;
}
}

View file

@ -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)
});
} }
} }

View 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
}),
{}
);
}
}

View file

@ -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();
} }
} }

View file

@ -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
})
);
} }
} }

View file

@ -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> {

View file

@ -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);
} }
} }

View file

@ -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) {

View file

@ -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));
} }

View file

@ -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.`);
}
} }

View file

@ -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
View 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}%)`;
};