From a9ad6284883caa4165efda4c2442cd325939bedc Mon Sep 17 00:00:00 2001
From: schmelczerandras
Date: Sat, 31 Aug 2019 21:21:21 +0200
Subject: [PATCH] Start refactoring state management
---
src/app/app.component.ts | 17 ---
.../modals/settings/settings.component.html | 6 +-
.../modals/settings/settings.component.ts | 11 +-
.../components/pages/page/page.component.html | 5 +-
.../components/pages/page/page.component.ts | 5 -
.../page/tower/tasks/tasks.component.html | 4 +-
.../pages/page/tower/tasks/tasks.component.ts | 6 +-
.../pages/page/tower/tower.component.html | 2 +-
.../pages/page/tower/tower.component.ts | 12 +-
src/app/components/pages/pages.component.html | 10 +-
src/app/components/pages/pages.component.ts | 70 ++++++++--
.../shared/toggle/toggle.component.ts | 2 +-
src/app/interfaces/persistance/block.ts | 5 +-
src/app/interfaces/persistance/color.ts | 5 +-
src/app/interfaces/persistance/page.ts | 8 +-
src/app/interfaces/persistance/tower.ts | 4 +-
src/app/interfaces/persistance/typed.ts | 3 +
src/app/model/base.ts | 57 --------
src/app/model/block.ts | 22 +--
src/app/model/color.ts | 28 ----
src/app/model/page.ts | 68 +++++-----
src/app/model/serializable.ts | 51 +++++++
src/app/model/tower.ts | 57 ++++----
src/app/services/data.service.ts | 126 +++++++-----------
src/app/services/modal.service.ts | 6 +-
src/app/services/store.service.ts | 75 +++++++----
src/app/storage/cloneable.ts | 39 +++---
src/app/storage/inner-node.ts | 2 +-
src/app/storage/node.ts | 27 +++-
src/app/storage/root.ts | 21 ++-
src/app/utils/color.ts | 16 +++
31 files changed, 410 insertions(+), 360 deletions(-)
create mode 100644 src/app/interfaces/persistance/typed.ts
delete mode 100644 src/app/model/base.ts
delete mode 100644 src/app/model/color.ts
create mode 100644 src/app/model/serializable.ts
create mode 100644 src/app/utils/color.ts
diff --git a/src/app/app.component.ts b/src/app/app.component.ts
index 70b176e..0c5a793 100644
--- a/src/app/app.component.ts
+++ b/src/app/app.component.ts
@@ -1,6 +1,4 @@
import { Component } from '@angular/core';
-import { Cloneable } from './storage/cloneable';
-import { Root } from './storage/root';
@Component({
selector: 'app-root',
@@ -9,19 +7,4 @@ import { Root } from './storage/root';
})
export class AppComponent {
title = 'frontend';
-
- constructor() {
- const root = new Root();
-
- 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'));
- }
}
diff --git a/src/app/components/modal/modals/settings/settings.component.html b/src/app/components/modal/modals/settings/settings.component.html
index 5a32e53..e3714aa 100644
--- a/src/app/components/modal/modals/settings/settings.component.html
+++ b/src/app/components/modal/modals/settings/settings.component.html
@@ -8,12 +8,12 @@
-There can be a maximum of 5 towers on each page.
+There can be a maximum of 5 towers on each page.
diff --git a/src/app/components/modal/modals/settings/settings.component.ts b/src/app/components/modal/modals/settings/settings.component.ts
index 27e17d1..5fcfe8c 100644
--- a/src/app/components/modal/modals/settings/settings.component.ts
+++ b/src/app/components/modal/modals/settings/settings.component.ts
@@ -1,6 +1,7 @@
import { Component } from '@angular/core';
import { ModalService } from '../../../../services/modal.service';
import { DataService } from '../../../../services/data.service';
+import { Page } from '../../../../model/page';
@Component({
selector: 'app-settings',
@@ -8,12 +9,16 @@ import { DataService } from '../../../../services/data.service';
styleUrls: ['./settings.component.scss']
})
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() {
try {
- await this.modalService.showRemovePage(this.dataService.active.name);
- this.dataService.remove();
+ await this.modalService.showRemovePage(this.page.name);
+ this.dataService.removePage(this.page);
this.modalService.submit();
} catch {
// pass
diff --git a/src/app/components/pages/page/page.component.html b/src/app/components/pages/page/page.component.html
index 3283c47..0a94a08 100644
--- a/src/app/components/pages/page/page.component.html
+++ b/src/app/components/pages/page/page.component.html
@@ -1,7 +1,4 @@
-
-
diff --git a/src/app/components/pages/page/tower/tasks/tasks.component.ts b/src/app/components/pages/page/tower/tasks/tasks.component.ts
index de0a612..39b845f 100644
--- a/src/app/components/pages/page/tower/tasks/tasks.component.ts
+++ b/src/app/components/pages/page/tower/tasks/tasks.component.ts
@@ -3,6 +3,8 @@ import { Block } from '../../../../../model/block';
import { Tower } from '../../../../../model/tower';
import { ModalService } from '../../../../../services/modal.service';
import { CancelService } from '../../../../../services/cancel.service';
+import { toHslString } from '../../../../../utils/color';
+import { IColor } from '../../../../../interfaces/persistance/color';
@Component({
selector: 'app-tasks',
@@ -10,7 +12,9 @@ import { CancelService } from '../../../../../services/cancel.service';
styleUrls: ['./tasks.component.scss']
})
export class TasksComponent implements OnInit {
- @Input() tasks: Block[];
+ readonly toHslString = toHslString;
+
+ @Input() tasks: Array
;
@Input() tower: Tower;
private _isOpen = false;
diff --git a/src/app/components/pages/page/tower/tower.component.html b/src/app/components/pages/page/tower/tower.component.html
index 65fc8fe..1a6f196 100644
--- a/src/app/components/pages/page/tower/tower.component.html
+++ b/src/app/components/pages/page/tower/tower.component.html
@@ -19,6 +19,6 @@
type="text"
placeholder="name…"
[(ngModel)]="tower.name"
- [ngStyle]="{ color: tower.baseColor.toString() }"
+ [ngStyle]="{ color: toHslString(tower?.baseColor) }"
/>
diff --git a/src/app/components/pages/page/tower/tower.component.ts b/src/app/components/pages/page/tower/tower.component.ts
index 116e245..c3400ce 100644
--- a/src/app/components/pages/page/tower/tower.component.ts
+++ b/src/app/components/pages/page/tower/tower.component.ts
@@ -2,6 +2,8 @@ import { Component, Input } from '@angular/core';
import { Tower } from '../../../../model/tower';
import { ModalService } from '../../../../services/modal.service';
import { Block } from '../../../../model/block';
+import { IColor } from '../../../../interfaces/persistance/color';
+import { toHslString } from '../../../../utils/color';
@Component({
selector: 'app-tower',
@@ -9,6 +11,8 @@ import { Block } from '../../../../model/block';
styleUrls: ['./tower.component.scss']
})
export class TowerComponent {
+ readonly toHslString = toHslString;
+
@Input() set dateRange(value: { from: Date; to: Date }) {
if (this.dateRange !== undefined && this.dateRange.from === value.from && this.dateRange.to === value.to) {
return;
@@ -22,14 +26,14 @@ export class TowerComponent {
public constructor(private modalService: ModalService) {}
- get drawableBlocks(): Block[] {
- return this.tower.blocks.filter(
+ get drawableBlocks(): Array {
+ return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && block.isDone
);
}
- get tasks(): Block[] {
- return this.tower.blocks.filter(
+ get tasks(): Array {
+ return this.tower.coloredBlocks.filter(
block => this.dateRange.from <= block.created && block.created <= this.dateRange.to && !block.isDone
);
}
diff --git a/src/app/components/pages/pages.component.html b/src/app/components/pages/pages.component.html
index 328126b..d6c427f 100644
--- a/src/app/components/pages/pages.component.html
+++ b/src/app/components/pages/pages.component.html
@@ -1,8 +1,8 @@
@@ -11,11 +11,7 @@
diff --git a/src/app/components/pages/pages.component.ts b/src/app/components/pages/pages.component.ts
index e8d077d..c006b5a 100644
--- a/src/app/components/pages/pages.component.ts
+++ b/src/app/components/pages/pages.component.ts
@@ -2,6 +2,10 @@ import { Component, ElementRef, ViewChild } from '@angular/core';
import { Page } from '../../model/page';
import { DataService } from '../../services/data.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({
selector: 'app-pages',
@@ -13,28 +17,70 @@ export class PagesComponent {
@ViewChild('page') page: ElementRef;
@ViewChild('bottom') bottom: ElementRef;
+ pages: Array
;
isDragHappening = false;
- constructor(public dataService: DataService, private modalService: ModalService) {}
+ get pageNames() {
+ if (this.pages) {
+ return this.pages.map(p => p.name);
+ }
+ return [];
+ }
- async selectPage(selected: string) {
- if (!this.dataService.pageNames.includes(selected)) {
- const page = new Page({
- name: selected,
- towers: [],
- userData: {}
- });
+ get selectedPage(): Page {
+ try {
+ return this.pages[this.pageNames.indexOf(this.selectedPageName)];
+ } catch {
+ return null;
+ }
+ }
- this.dataService.push(page);
- page.addTower();
+ 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 = new BehaviorSubject(null);
+ readonly selectedPage$: Observable = 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;
}
- await this.dataService.changeActiveByName(selected);
+ 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() {
try {
- await this.modalService.showSettings();
+ await this.modalService.showSettings(this.selectedPage$);
} catch {
// pass
}
diff --git a/src/app/components/shared/toggle/toggle.component.ts b/src/app/components/shared/toggle/toggle.component.ts
index dcf0125..29b6980 100644
--- a/src/app/components/shared/toggle/toggle.component.ts
+++ b/src/app/components/shared/toggle/toggle.component.ts
@@ -12,7 +12,7 @@ export class ToggleComponent {
@Output() value: EventEmitter = new EventEmitter();
@Input() set default(value: boolean) {
- this.on = value;
+ this._on = value;
}
private _on = false;
diff --git a/src/app/interfaces/persistance/block.ts b/src/app/interfaces/persistance/block.ts
index 5021ea3..75be455 100644
--- a/src/app/interfaces/persistance/block.ts
+++ b/src/app/interfaces/persistance/block.ts
@@ -1,4 +1,7 @@
-export interface IBlock {
+import { Typed } from './typed';
+
+export interface IBlock extends Typed {
+ type: 'Block';
created: Date;
tag: string;
isDone: boolean;
diff --git a/src/app/interfaces/persistance/color.ts b/src/app/interfaces/persistance/color.ts
index a414a2a..1a49186 100644
--- a/src/app/interfaces/persistance/color.ts
+++ b/src/app/interfaces/persistance/color.ts
@@ -1,4 +1,7 @@
-export interface IColor {
+import { Typed } from './typed';
+
+export interface IColor extends Typed {
+ type: 'Color';
h: number;
s: number;
l: number;
diff --git a/src/app/interfaces/persistance/page.ts b/src/app/interfaces/persistance/page.ts
index 74cfbd5..ba8ed10 100644
--- a/src/app/interfaces/persistance/page.ts
+++ b/src/app/interfaces/persistance/page.ts
@@ -1,12 +1,14 @@
import { ITower } from './tower';
+import { Typed } from './typed';
-export interface IPage {
+export interface IPage extends Typed {
+ type: 'Page';
name: string;
towers: ITower[];
userData: {
- hideCreateTowerButton: boolean;
- defaultDateRange: {
+ hideCreateTowerButton?: boolean;
+ defaultDateRange?: {
from: Date;
to: Date;
};
diff --git a/src/app/interfaces/persistance/tower.ts b/src/app/interfaces/persistance/tower.ts
index 28693df..352a674 100644
--- a/src/app/interfaces/persistance/tower.ts
+++ b/src/app/interfaces/persistance/tower.ts
@@ -1,7 +1,9 @@
import { IBlock } from './block';
import { IColor } from './color';
+import { Typed } from './typed';
-export interface ITower {
+export interface ITower extends Typed {
+ type: 'Tower';
name: string;
blocks: IBlock[];
baseColor: IColor;
diff --git a/src/app/interfaces/persistance/typed.ts b/src/app/interfaces/persistance/typed.ts
new file mode 100644
index 0000000..7d34e2e
--- /dev/null
+++ b/src/app/interfaces/persistance/typed.ts
@@ -0,0 +1,3 @@
+export interface Typed {
+ type: string;
+}
diff --git a/src/app/model/base.ts b/src/app/model/base.ts
deleted file mode 100644
index d3c75f6..0000000
--- a/src/app/model/base.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { Subject } from 'rxjs/internal/Subject';
-
-export class Base {
- private static propertyList: any = {};
- protected subscribers: (() => void)[] = [];
-
- subject: Subject = 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());
- }
-}
diff --git a/src/app/model/block.ts b/src/app/model/block.ts
index f4d3cec..06fd0f0 100644
--- a/src/app/model/block.ts
+++ b/src/app/model/block.ts
@@ -1,26 +1,18 @@
-import { Base } from './base';
+import { Serializable } from './serializable';
import { IBlock } from '../interfaces/persistance/block';
-import { Color } from './color';
+import { Node } from '../storage/node';
-export class Block extends Base implements IBlock {
- constructor(props: IBlock) {
- // TODO: remove
- if (props.isDone === undefined) {
- props.isDone = true;
- }
-
- super(props);
+export class Block extends Serializable implements IBlock {
+ constructor(parent: Node, props: IBlock) {
+ super(parent, props);
if (this.created.constructor.name !== 'Date') {
- // Prevent update message
- // @ts-ignore
- this.__created = new Date(this.created);
+ this.created = new Date(this.created);
}
}
- color: Color;
-
// Only here to prevent ts warnings.
+ type: 'Block';
created: Date;
isDone: boolean;
description: string;
diff --git a/src/app/model/color.ts b/src/app/model/color.ts
deleted file mode 100644
index 73f7874..0000000
--- a/src/app/model/color.ts
+++ /dev/null
@@ -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}%)`;
- }
-}
diff --git a/src/app/model/page.ts b/src/app/model/page.ts
index d1cabe2..dd9424d 100644
--- a/src/app/model/page.ts
+++ b/src/app/model/page.ts
@@ -1,23 +1,20 @@
-import { Base } from './base';
+import { Serializable } from './serializable';
import { IPage } from '../interfaces/persistance/page';
import { Tower } from './tower';
-import { ITower } from '../interfaces/persistance/tower';
+import { Node } from '../storage/node';
-export class Page extends Base implements IPage {
- constructor(props) {
- // TODO: remove
- if (!props.userData) {
- props.userData = {};
- }
-
- super(props);
- // @ts-ignore to prevent update message
- this.__towers = this.towers.map(t => this.createTower(t));
+export class Page extends Serializable implements IPage {
+ constructor(parent: Node, props: IPage) {
+ super(parent, props);
}
// Only here to prevent ts warnings.
name: string;
- towers: Tower[];
+ get towers(): Array {
+ return this.children as Array;
+ }
+ type: 'Page';
+
userData: {
hideCreateTowerButton: boolean;
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 }) {
if (previousIndex === currentIndex) {
return;
}
- const tower = this.towers[previousIndex];
- this.towers.splice(previousIndex, 1);
- this.towers.splice(currentIndex, 0, tower);
- this.update();
+ this.map(page => {
+ const tower = page.towers[previousIndex];
+ page.towers.splice(previousIndex, 1);
+ page.towers.splice(currentIndex, 0, tower);
+ });
}
addTower(name = '') {
@@ -43,24 +51,18 @@ export class Page extends Base implements IPage {
hue = Math.random() * 360;
} while (30 <= hue && hue <= 200);
- this.towers.push(
- this.createTower({
- name,
- blocks: [],
- baseColor: { h: hue, s: 100, l: 50 }
- })
- );
-
- this.update();
- }
-
- private createTower(props: ITower): Tower {
- const tower = new Tower(props);
- tower.subscribe(() => this.update());
- return tower;
+ new Tower(this, {
+ type: 'Tower',
+ name,
+ blocks: [],
+ baseColor: { h: hue, s: 100, l: 50, type: 'Color' }
+ });
}
removeTower(tower: Tower) {
- this.towers = this.towers.filter(t => t !== tower);
+ this.changeValue({
+ oldValue: this.towers,
+ newValue: this.towers.filter(t => t !== tower)
+ });
}
}
diff --git a/src/app/model/serializable.ts b/src/app/model/serializable.ts
new file mode 100644
index 0000000..36f1967
--- /dev/null
+++ b/src/app/model/serializable.ts
@@ -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
+ }),
+ {}
+ );
+ }
+}
diff --git a/src/app/model/tower.ts b/src/app/model/tower.ts
index be97bb8..90c1323 100644
--- a/src/app/model/tower.ts
+++ b/src/app/model/tower.ts
@@ -1,54 +1,51 @@
import { ITower } from '../interfaces/persistance/tower';
-import { Color } from './color';
+import { lighten } from '../utils/color';
import { Block } from './block';
-import { Base } from './base';
-import { IBlock } from '../interfaces/persistance/block';
+import { Serializable } from './serializable';
import { hash } from '../utils/hash';
+import { Node } from '../storage/node';
+import { IColor } from '../interfaces/persistance/color';
-export class Tower extends Base implements ITower {
- constructor(props: ITower) {
- super(props);
+export class Tower extends Serializable implements ITower {
+ constructor(parent: Node, props: ITower) {
+ 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.calculateTagList();
}
tags: string[];
// Only here to prevent ts warnings.
name: string;
- blocks: Block[];
- baseColor: Color;
+ type: 'Tower';
+ get blocks(): Array {
+ return this.children as Array;
+ }
+ baseColor: IColor;
+
+ get coloredBlocks(): Array {
+ 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 }) {
- this.blocks.push(
- this.createBlock({
- created: new Date(),
- ...props
- })
- );
-
- this.update();
+ new Block(this, {
+ created: new Date(),
+ ...props,
+ type: 'Block'
+ });
}
- private createBlock(props: IBlock): Block {
- const block = new Block(props);
- block.subscribe(() => this.update());
- return block;
- }
-
- protected update() {
+ private calculateTagList() {
this.tags = [];
for (const block of this.blocks) {
if (!this.tags.includes(block.tag)) {
this.tags.push(block.tag);
}
- block.color = this.baseColor.lighten(hash(block.tag) * 50);
}
-
- super.update();
}
}
diff --git a/src/app/services/data.service.ts b/src/app/services/data.service.ts
index e7d7356..95ce3cf 100644
--- a/src/app/services/data.service.ts
+++ b/src/app/services/data.service.ts
@@ -1,93 +1,69 @@
import { Injectable } from '@angular/core';
import { StoreService } from './store.service';
import { Page } from '../model/page';
-
-const USER_DATA_KEY = 'life-towers.user-data.v.1';
+import { Root } from '../storage/root';
+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({
providedIn: 'root'
})
-export class DataService {
- get active(): Page {
- return this._active;
+export class DataService extends Root {
+ get pages(): Array {
+ return this.children;
}
- get pageNames(): string[] {
- return this.data.map(p => p.name);
- }
+ private readonly _safeChildren: BehaviorSubject> = new BehaviorSubject(null);
+ readonly safeChildren$: Observable> = this._safeChildren.asObservable();
- private subscribers: (() => void)[] = [];
- private _active: Page = null;
- private data: Page[];
- 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 {
- 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 {
- await this.hasLoaded;
- this._active = this.data[index];
- this.saveActiveIndex(index);
- this.update();
+ constructor(private storeService: StoreService>) {
+ super();
+ this.init().catch();
}
private async init() {
- this.data = await this.storeService.load();
- this.data.map(p => p.subscribe(() => this.save()));
- this._active = this.data.length > 0 ? this.data[0] : null;
- this.loadActiveIndex();
- this.afterLoadFinished();
- }
-
- private save() {
- this.storeService.save(this.data);
- this.update();
- }
-
- private update() {
- this.subscribers.map(f => f());
- }
-
- private loadActiveIndex() {
- const userData = JSON.parse(window.localStorage.getItem(USER_DATA_KEY));
- if (userData === null) {
- return;
+ const pages = await this.storeService.load();
+ Serializable.childrenMap = {
+ Page: {
+ childrenListName: 'towers',
+ childrenConstructor: Tower
+ },
+ Tower: {
+ childrenListName: 'blocks',
+ childrenConstructor: Block
+ },
+ Block: {
+ childrenListName: null,
+ childrenConstructor: null
+ }
+ };
+ for (let page of pages) {
+ new Page(this, page);
}
- this._active = this.data[userData.index];
+ this.children$.subscribe(value => {
+ this.log();
+ this._safeChildren.next(value);
+ this.storeService.scheduleSave(this.pages);
+ });
}
- private saveActiveIndex(index: number) {
- window.localStorage.setItem(
- USER_DATA_KEY,
- JSON.stringify({
- index
- })
- );
+ addPage(name: string) {
+ new Page(this, {
+ name,
+ userData: {},
+ type: 'Page',
+ towers: []
+ });
+ }
+
+ removePage(page: Page) {
+ this.changeValue({
+ oldValue: this.children,
+ newValue: this.children.filter(c => c !== page)
+ });
}
}
diff --git a/src/app/services/modal.service.ts b/src/app/services/modal.service.ts
index e7c85fa..5054bf0 100644
--- a/src/app/services/modal.service.ts
+++ b/src/app/services/modal.service.ts
@@ -2,6 +2,8 @@ import { Injectable } from '@angular/core';
import { Tower } from '../model/tower';
import { top } from '../utils/top';
import { CancelService } from './cancel.service';
+import { Page } from '../model/page';
+import { Observable } from 'rxjs/internal/Observable';
export enum ModalType {
createBlock,
@@ -44,8 +46,8 @@ export class ModalService {
return this.createPromiseAndPushToStack(data, ModalType.editBlock);
}
- showSettings(): Promise {
- return this.createPromiseAndPushToStack(null, ModalType.settings);
+ showSettings(selectedPage: Observable): Promise {
+ return this.createPromiseAndPushToStack(selectedPage, ModalType.settings);
}
showRemoveTower(tower: Tower): Promise {
diff --git a/src/app/services/store.service.ts b/src/app/services/store.service.ts
index 702ae3b..8b677dc 100644
--- a/src/app/services/store.service.ts
+++ b/src/app/services/store.service.ts
@@ -1,82 +1,98 @@
import { Injectable } from '@angular/core';
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({
providedIn: 'root'
})
-export class StoreService {
- private storedData: Page[];
-
+export class StoreService {
+ private saveScheduled = false;
+ private dataToSave: T;
+ private storedData: T;
private mockData: string = JSON.stringify([
{
name: 'Work & life',
userData: {},
+ type: 'Page',
towers: [
{
name: 'work',
- baseColor: { h: 0, s: 100, l: 50 },
+ baseColor: { h: 0, s: 100, l: 50, type: 'Color' },
+ type: 'Tower',
blocks: [
{
created: new Date(2015, 2, 13),
tag: 'a',
description: 'done it',
- isDone: true
+ isDone: true,
+ type: 'Block'
},
{
created: new Date(2016, 2, 15),
tag: 'go to school',
- description: 'done it'
+ description: 'done it',
+ type: 'Block'
},
{
created: new Date(2017, 2, 15),
tag: 'go to work',
- isDone: true
+ isDone: true,
+ type: 'Block'
},
{
created: new Date(2018, 2, 13),
tag: 'go to work',
description: 'done it',
- isDone: true
+ isDone: true,
+ type: 'Block'
},
{
created: new Date(2019, 3, 13),
- tag: 'go to work'
+ tag: 'go to work',
+ type: 'Block'
},
{
created: new Date(2020, 2, 15),
tag: 'go to school',
description: 'done it',
- isDone: true
+ isDone: true,
+ type: 'Block'
},
{
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',
+ type: 'Tower',
blocks: [
{
created: new Date(2019, 3, 13),
tag: 'go home',
- description: 'done it'
+ description: 'done it',
+ type: 'Block'
},
{
created: new Date(2019, 4, 13),
- tag: 'go home'
+ tag: 'go home',
+ type: 'Block'
},
{
created: new Date(2019, 4, 15),
tag: 'go to work',
- description: 'done it'
+ description: 'done it',
+ type: 'Block'
},
{
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() {
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
- const data = JSON.parse(localStorageData ? localStorageData : this.mockData);
- this.storedData = data.map(p => new Page(p));
+ this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
}
- async load(): Promise {
+ 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 {
+ console.log('load', this.storedData);
return this.storedData;
}
- async save(data: Page[]) {
+ async save(data: T) {
this.storedData = data;
- console.log('save', this.storedData);
- window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.storedData));
+ const stringified = JSON.stringify(this.storedData, null, 2);
+ console.log('save');
+ // console.log('save', stringified);
+ window.localStorage.setItem(LOCAL_STORAGE_KEY, stringified);
}
}
diff --git a/src/app/storage/cloneable.ts b/src/app/storage/cloneable.ts
index 9534c67..767b5f0 100644
--- a/src/app/storage/cloneable.ts
+++ b/src/app/storage/cloneable.ts
@@ -2,23 +2,10 @@ import { InnerNode } from './inner-node';
import { Node } from './node';
export class Cloneable extends InnerNode {
- name;
- constructor(parent: Node, name: any) {
+ constructor(parent: Node) {
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 {
const insides = Object.getOwnPropertyDescriptors(this);
@@ -27,11 +14,19 @@ export class Cloneable extends InnerNode {
if (prop == '__target__') {
return target;
}
- const value = target[prop as string].value;
- if (typeof value === 'function') {
- return value.bind(proxy);
+ if (target.hasOwnProperty(prop)) {
+ const value = target[prop as string].value;
+ if (typeof value === 'function') {
+ return value.bind(proxy);
+ }
+ return value;
+ } else if (this.hasOwnProperty(prop)) {
+ const value = this[prop];
+ if (typeof value === 'function') {
+ return value.bind(proxy);
+ }
+ return value;
}
- return value;
},
set: (target, prop, value) => {
return (target[prop as string].value = value);
@@ -39,6 +34,10 @@ export class Cloneable extends InnerNode {
});
map(insidesProxy);
+ (insidesProxy.__target__).id.value = Node.id++;
+ (insidesProxy.__target__).copyCount.value++;
+ Node.sumCopyCount++;
+
return Object.create(Object.getPrototypeOf(this), insidesProxy.__target__);
}
@@ -46,6 +45,8 @@ export class Cloneable extends InnerNode {
const insides = Object.getOwnPropertyDescriptors(this);
insides[propertyName].value = value;
insides.id.value = Node.id++;
+ insides.copyCount.value++;
+ Node.sumCopyCount++;
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 {
const insides = Object.getOwnPropertyDescriptors(this);
insides.id.value = Node.id++;
+ insides.copyCount.value++;
+ Node.sumCopyCount++;
let wasMatch = false;
for (let name in insides) {
diff --git a/src/app/storage/inner-node.ts b/src/app/storage/inner-node.ts
index 8d2f8a7..06425d6 100644
--- a/src/app/storage/inner-node.ts
+++ b/src/app/storage/inner-node.ts
@@ -26,7 +26,7 @@ export abstract class InnerNode extends Node {
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));
}
diff --git a/src/app/storage/node.ts b/src/app/storage/node.ts
index e0dc6ff..81e29f6 100644
--- a/src/app/storage/node.ts
+++ b/src/app/storage/node.ts
@@ -1,16 +1,17 @@
import { InnerNode } from './inner-node';
export abstract class Node {
- public static id = 0;
+ protected static id = 0;
+ protected static sumCopyCount = 0;
protected abstract readonly children: Array;
private id = Node.id++;
+ protected copyCount = 1;
- changeKey(update: { value: any; propertyName: string }) {
- throw new TypeError('Not implemented!');
- }
+ abstract changeKey(update: { propertyName: string; value: any });
+ abstract changeValue(update: { oldValue: any; newValue: any });
- changeValue(update: { oldValue: any; newValue: any }) {
- throw new TypeError('Not implemented!');
+ constructor() {
+ Node.sumCopyCount++;
}
addChild(update: { value: InnerNode }) {
@@ -26,4 +27,18 @@ export abstract class Node {
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.`);
+ }
}
diff --git a/src/app/storage/root.ts b/src/app/storage/root.ts
index 88b9f57..1cbeed9 100644
--- a/src/app/storage/root.ts
+++ b/src/app/storage/root.ts
@@ -16,13 +16,22 @@ export class Root extends Node {
}
changeValue({ oldValue, newValue }: { oldValue: any; newValue: any }) {
- if (this.children === oldValue) {
- this.children = newValue;
- for (let child of this.children) {
- child.parent = this;
- }
- } else {
+ if (this.children !== oldValue) {
throw new TypeError('Only children can be changed.');
}
+ this.children = newValue;
+ for (let child of this.children) {
+ child.parent = this;
+ }
+ }
+
+ changeKey({ propertyName, value }: { propertyName: string; value: any }) {
+ if (propertyName !== 'children') {
+ throw new TypeError('Only children can be changed.');
+ }
+ this.children = value;
+ for (let child of this.children) {
+ child.parent = this;
+ }
}
}
diff --git a/src/app/utils/color.ts b/src/app/utils/color.ts
new file mode 100644
index 0000000..7d7eb85
--- /dev/null
+++ b/src/app/utils/color.ts
@@ -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}%)`;
+};