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/page.component.ts b/src/app/components/pages/page/page.component.ts index 73a52c5..f8de9c7 100644 --- a/src/app/components/pages/page/page.component.ts +++ b/src/app/components/pages/page/page.component.ts @@ -16,7 +16,6 @@ export class PageComponent { } this._page = value; - value.subscribe(() => this.updateDates()); this.updateDates(); } @@ -54,10 +53,6 @@ export class PageComponent { constructor(private modalService: ModalService, public dataService: DataService) {} - createTower() { - this.page.addTower(); - } - dropDrag(event: any) { this.page.moveTower(event); this.isDragging = false; diff --git a/src/app/components/pages/page/tower/tasks/tasks.component.html b/src/app/components/pages/page/tower/tasks/tasks.component.html index ca6ed66..7712a63 100644 --- a/src/app/components/pages/page/tower/tasks/tasks.component.html +++ b/src/app/components/pages/page/tower/tasks/tasks.component.html @@ -6,8 +6,8 @@ {{ tasks.length == 0 ? '​' : tasks.length == 1 ? 'task' : 'tasks' }}

-
-
+
+

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