Make it multiplayer

This commit is contained in:
Andras Schmelczer 2019-09-22 20:21:44 +02:00
parent 19aad2b2af
commit 1598260ce3
27 changed files with 464 additions and 332 deletions

View file

@ -5,7 +5,7 @@
"ng": "ng", "ng": "ng",
"start": "ng serve", "start": "ng serve",
"build": "ng build", "build": "ng build",
"build:prod": "ng build --prod --base-href /life-qa/", "build:prod": "ng build --prod",
"format:fix": "pretty-quick --staged", "format:fix": "pretty-quick --staged",
"precommit": "run-s format:fix lint", "precommit": "run-s format:fix lint",
"format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"", "format:check": "prettier --config ./.prettierrc --list-different \"src/{app,environments,assets}/**/*{.ts,.js,.json,.css,.scss}\"",

View file

@ -21,6 +21,7 @@ import { TasksComponent } from './components/pages/page/tower/tasks/tasks.compon
import { ColorPipe } from './pipes/color.pipe'; import { ColorPipe } from './pipes/color.pipe';
import { BlocksComponent } from './components/modal/modals/blocks/blocks.component'; import { BlocksComponent } from './components/modal/modals/blocks/blocks.component';
import { FormatDatePipe } from './pipes/format-date.pipe'; import { FormatDatePipe } from './pipes/format-date.pipe';
import { HttpClientModule } from '@angular/common/http';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -43,7 +44,7 @@ import { FormatDatePipe } from './pipes/format-date.pipe';
BlocksComponent, BlocksComponent,
FormatDatePipe FormatDatePipe
], ],
imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule], imports: [BrowserModule, AppRoutingModule, FormsModule, BrowserAnimationsModule, DragDropModule, HttpClientModule],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })

View file

@ -14,4 +14,8 @@
<p *ngIf="page.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>
<input id="token" type="text" [(ngModel)]="token" />
<button (click)="setNewToken()">Set token</button>
<button (click)="$event.stopPropagation() || deletePage()">Delete current page</button> <button (click)="$event.stopPropagation() || deletePage()">Delete current page</button>

View file

@ -31,4 +31,12 @@
p { p {
font-size: var(--medium-font-size); font-size: var(--medium-font-size);
} }
input[type='text'] {
text-align: center;
}
button {
display: block;
}
} }

View file

@ -1,28 +1,56 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } 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'; import { Page } from '../../../../model/page';
import { Data } from '../../../../model/data';
import { Subscription } from 'rxjs/internal/Subscription';
import { MapStoreService } from '../../../../services/map-store.service';
@Component({ @Component({
selector: 'app-settings', selector: 'app-settings',
templateUrl: './settings.component.html', templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss'] styleUrls: ['./settings.component.scss']
}) })
export class SettingsComponent implements OnInit { export class SettingsComponent implements OnInit, OnDestroy {
data: Data;
page: Page; page: Page;
constructor(public modalService: ModalService, public dataService: DataService) {}
private dataSubscription: Subscription;
private pageSubscription: Subscription;
token: string;
constructor(public modalService: ModalService, private store: MapStoreService) {
this.token = store.userToken;
}
ngOnInit() { ngOnInit() {
this.modalService.active.input.subscribe(p => (this.page = p)); const { data$, page$ } = this.modalService.active.input;
this.dataSubscription = data$.subscribe(d => (this.data = d));
this.pageSubscription = page$.subscribe(p => (this.page = p));
} }
async deletePage() { async deletePage() {
try { try {
await this.modalService.showRemovePage(this.page.name); await this.modalService.showRemovePage(this.page.name);
this.dataService.removePage(this.page); this.data.removePage(this.page);
this.modalService.submit(); this.modalService.submit();
} catch { } catch {
// pass // pass
} }
} }
setNewToken() {
this.store.userToken = this.token;
}
ngOnDestroy() {
if (this.dataSubscription) {
this.dataSubscription.unsubscribe();
}
if (this.pageSubscription) {
this.pageSubscription.unsubscribe();
}
}
} }

View file

@ -36,9 +36,7 @@ export class TowerComponent implements OnInit {
return this.styledBlocks.filter(b => b.shouldDraw); return this.styledBlocks.filter(b => b.shouldDraw);
} }
public constructor(private modalService: ModalService, private changeDetection: ChangeDetectorRef) { public constructor(private modalService: ModalService, private changeDetection: ChangeDetectorRef) {}
console.log('oo');
}
ngOnInit() { ngOnInit() {
this.tower$.subscribe(value => { this.tower$.subscribe(value => {

View file

@ -4,6 +4,8 @@ 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 { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { Data } from '../../model/data';
import { of } from 'rxjs/internal/observable/of';
const USER_DATA_KEY = 'life-towers.user-data.v.2'; const USER_DATA_KEY = 'life-towers.user-data.v.2';
@ -17,6 +19,7 @@ export class PagesComponent implements OnInit {
@ViewChild('page') page: ElementRef; @ViewChild('page') page: ElementRef;
@ViewChild('bottom') bottom: ElementRef; @ViewChild('bottom') bottom: ElementRef;
data: Data;
pages: Array<Page>; pages: Array<Page>;
isDragHappening = false; isDragHappening = false;
@ -44,8 +47,10 @@ export class PagesComponent implements OnInit {
} }
ngOnInit() { ngOnInit() {
this.dataService.children$.subscribe(pages => { this.dataService.children$.subscribe(dataContainer => {
if (pages) { if (dataContainer && dataContainer.length > 0) {
this.data = dataContainer[0];
const pages = this.data.pages;
if (this.pages && !pages.includes(this._selectedPage.getValue().latestVersion)) { if (this.pages && !pages.includes(this._selectedPage.getValue().latestVersion)) {
this.selectedPageName = null; this.selectedPageName = null;
} }
@ -82,7 +87,7 @@ export class PagesComponent implements OnInit {
if (this.pages && name) { if (this.pages && name) {
if (!this.pageNames.includes(name)) { if (!this.pageNames.includes(name)) {
this.dataService.addPage(name); this.data.addPage(name);
} }
const index = this.pageNames.indexOf(name); const index = this.pageNames.indexOf(name);
@ -95,7 +100,10 @@ export class PagesComponent implements OnInit {
async openSettings() { async openSettings() {
try { try {
await this.modalService.showSettings(this.selectedPage$); await this.modalService.showSettings({
page$: this.selectedPage$,
data$: of(this.data)
});
} catch { } catch {
// pass // pass
} finally { } finally {

View file

@ -1,4 +1,6 @@
export interface IBlock { import { IUnique } from './unique';
export interface IBlock extends IUnique {
created: Date; created: Date;
tag: string; tag: string;
isDone: boolean; isDone: boolean;

View file

@ -0,0 +1,6 @@
import { IPage } from './page';
import { IUnique } from './unique';
export interface IData extends IUnique {
pages: Array<IPage>;
}

View file

@ -1,12 +1,13 @@
import { ITower } from './tower'; import { ITower } from './tower';
import { Range } from '../range'; import { Range } from '../range';
import { IUnique } from './unique';
export interface IPage { export interface IPage extends IUnique {
name: string; name: string;
towers: ITower[]; towers: ITower[];
userData: { userData: {
hideCreateTowerButton?: boolean; hideCreateTowerButton: boolean;
defaultDateRange?: Range<Date>; defaultDateRange: Range<Date>;
}; };
} }

View file

@ -1,7 +1,8 @@
import { IBlock } from './block'; import { IBlock } from './block';
import { IColor } from '../color'; import { IColor } from '../color';
import { IUnique } from './unique';
export interface ITower { export interface ITower extends IUnique {
name: string; name: string;
blocks: IBlock[]; blocks: IBlock[];
baseColor: IColor; baseColor: IColor;

View file

@ -0,0 +1,3 @@
export interface IUnique {
id?: string;
}

View file

@ -0,0 +1,3 @@
export interface ISerializable {
serialize(referenceSerializer: (ref: object) => any): object;
}

View file

@ -1,23 +1,36 @@
import { Serializable } from './serializable';
import { IBlock } from '../interfaces/persistance/block'; import { IBlock } from '../interfaces/persistance/block';
import { InnerNodeState } from '../store/inner-node'; import { InnerNode, InnerNodeState } from '../store/inner-node';
export interface BlockState extends IBlock, InnerNodeState {} export interface BlockState extends IBlock, InnerNodeState {}
export class Block extends Serializable implements IBlock, BlockState { export class Block extends InnerNode implements IBlock, BlockState {
readonly created: Date;
readonly isDone: boolean;
readonly description: string;
readonly tag: string; readonly tag: string;
readonly description: string;
readonly isDone: boolean;
readonly created: Date;
constructor(props: IBlock) { constructor(props: IBlock, referenceDeserializer: (from: any) => any = e => e) {
super([], props.id);
if (props.created.constructor.name !== 'Date') { if (props.created.constructor.name !== 'Date') {
props.created = new Date(props.created); props.created = new Date(props.created);
} }
super(props, 'Block'); this.tag = props.tag;
this.description = props.description;
this.isDone = props.isDone;
this.created = props.created;
} }
changeKeys(props: Partial<BlockState>): this { changeKeys(props: Partial<BlockState>): this {
return super.changeKeys<BlockState>(props); return super.changeKeys<BlockState>(props);
} }
serialize(referenceSerializer: (ref: object) => any): IBlock {
return {
...super.serialize(referenceSerializer),
tag: this.tag,
description: this.description,
isDone: this.isDone,
created: this.created
};
}
} }

44
src/app/model/data.ts Normal file
View file

@ -0,0 +1,44 @@
import { InnerNode, InnerNodeState } from '../store/inner-node';
import { IData } from '../interfaces/persistance/data';
import { Page } from './page';
export interface DataState extends IData, InnerNodeState {}
export class Data extends InnerNode implements IData, DataState {
constructor(props: IData, referenceDeserializer: (from: any) => any = e => e) {
super(props.pages.map(p => new Page(referenceDeserializer(p), referenceDeserializer)), props.id);
}
get pages(): Array<Page> {
return this.children as Array<Page>;
}
addPage(name: string) {
const page = new Page({
name,
userData: {
hideCreateTowerButton: false,
defaultDateRange: {
from: null,
to: null
}
},
towers: []
});
this.addChildren([page]);
page.addTower();
}
removePage(page: Page) {
this.changeKeys<any>({
children: this.children.filter(c => c !== page)
});
}
serialize(referenceSerializer: (ref: object) => any): IData {
return {
...super.serialize(referenceSerializer),
pages: this.pages.map(referenceSerializer)
};
}
}

View file

@ -1,25 +1,24 @@
import { Serializable } from './serializable';
import { IPage } from '../interfaces/persistance/page'; import { IPage } from '../interfaces/persistance/page';
import { Range } from '../interfaces/range';
import { Tower } from './tower'; import { Tower } from './tower';
import { InnerNodeState } from '../store/inner-node'; import { InnerNode, InnerNodeState } from '../store/inner-node';
export interface PageState extends InnerNodeState, IPage { export interface PageState extends InnerNodeState, IPage {
towers: Array<Tower>; towers: Array<Tower>;
} }
export class Page extends Serializable implements IPage, PageState { export class Page extends InnerNode implements IPage, PageState {
readonly name: string; readonly name: string;
readonly userData: { readonly userData: {
hideCreateTowerButton: boolean; hideCreateTowerButton: boolean;
defaultDateRange: { defaultDateRange: Range<Date>;
from: Date;
to: Date;
};
}; };
constructor(props: IPage) { constructor(props: IPage, referenceDeserializer: (from: any) => any = e => e) {
super(props, 'Page', props.towers.map(t => new Tower(t))); super(props.towers.map(t => new Tower(referenceDeserializer(t), referenceDeserializer)), props.id);
this.name = props.name;
this.userData = props.userData;
} }
get towers(): Array<Tower> { get towers(): Array<Tower> {
@ -84,4 +83,13 @@ export class Page extends Serializable implements IPage, PageState {
towers: this.towers.filter(t => t !== tower) towers: this.towers.filter(t => t !== tower)
}); });
} }
serialize(referenceSerializer: (ref: object) => any): IPage {
return {
...super.serialize(referenceSerializer),
name: this.name,
userData: this.userData,
towers: this.towers.map(referenceSerializer)
};
}
} }

View file

@ -1,44 +0,0 @@
import { InnerNode } from '../store/inner-node';
export class Serializable extends InnerNode {
static childrenMap: {
[type: string]: {
childrenConstructor: typeof Serializable;
childrenListName: string;
childrenType: string;
};
};
private static propertyList: any = {};
protected type: string;
protected constructor(properties: any, type: string, children: Array<InnerNode> = []) {
super(children);
const compiledType = this.constructor.name;
if (!Serializable.propertyList.hasOwnProperty(compiledType)) {
Serializable.propertyList[compiledType] = [];
}
for (const property in properties) {
if (properties.hasOwnProperty(property)) {
if (property !== Serializable.childrenMap[type].childrenListName) {
this[property] = properties[property];
}
if (!Serializable.propertyList[compiledType].includes(property)) {
Serializable.propertyList[compiledType].push(property);
}
}
}
}
toJSON(): object {
return Serializable.propertyList[this.constructor.name].reduce(
(object, property) => ({
[property]: this[property],
...object
}),
{}
);
}
}

View file

@ -1,10 +1,9 @@
import { ITower } from '../interfaces/persistance/tower'; import { ITower } from '../interfaces/persistance/tower';
import { lighten } from '../utils/color'; import { lighten } from '../utils/color';
import { Block } from './block'; import { Block } from './block';
import { Serializable } from './serializable';
import { hash } from '../utils/hash'; import { hash } from '../utils/hash';
import { IColor } from '../interfaces/color'; import { IColor } from '../interfaces/color';
import { InnerNodeState } from '../store/inner-node'; import { InnerNode, InnerNodeState } from '../store/inner-node';
export type ColoredBlock = Block & { color: IColor }; export type ColoredBlock = Block & { color: IColor };
@ -12,14 +11,16 @@ export interface TowerState extends ITower, InnerNodeState {
blocks: Array<Block>; blocks: Array<Block>;
} }
export class Tower extends Serializable implements ITower, TowerState { export class Tower extends InnerNode implements ITower, TowerState {
tags: string[];
readonly name: string; readonly name: string;
coloredBlocks: Array<ColoredBlock>;
readonly baseColor: IColor; readonly baseColor: IColor;
tags: string[];
coloredBlocks: Array<ColoredBlock>;
constructor(props: ITower) { constructor(props: ITower, referenceDeserializer: (from: any) => any = e => e) {
super(props, 'Tower', props.blocks.map(b => new Block(b))); super(props.blocks.map(b => new Block(referenceDeserializer(b), referenceDeserializer)), props.id);
this.name = props.name;
this.baseColor = props.baseColor;
this.onAfterClone(); this.onAfterClone();
} }
@ -52,6 +53,15 @@ export class Tower extends Serializable implements ITower, TowerState {
return lighten((hash(tag) - 0.5) * 50, this.baseColor); return lighten((hash(tag) - 0.5) * 50, this.baseColor);
} }
serialize(referenceSerializer: (ref: object) => any): ITower {
return {
...super.serialize(referenceSerializer),
name: this.name,
baseColor: this.baseColor,
blocks: this.blocks.map(referenceSerializer)
};
}
protected onAfterClone() { protected onAfterClone() {
this.blocks.sort((a, b) => { this.blocks.sort((a, b) => {
return a.created.getTime() - b.created.getTime(); return a.created.getTime() - b.created.getTime();

View file

@ -0,0 +1,53 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Unique } from '../store/unique';
const API_URI = 'https://schmelczer.dev/api/store/';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor(private http: HttpClient) {}
private static getAuthorizationHeader(id: string): HttpHeaders {
return new HttpHeaders().set('Authorization', `life-towers-v3 ${id}`);
}
async track(id: string): Promise<void> {
await this.http.post(`${API_URI}me`, {}, { headers: ApiService.getAuthorizationHeader(id) }).toPromise();
}
async register(id: string): Promise<void> {
await this.http.post(API_URI, { token: id }).toPromise();
}
async getObject(userId: string, objectId: string): Promise<Unique> {
return await this.http
.get<Unique>(`${API_URI}me/${objectId}`, { headers: ApiService.getAuthorizationHeader(userId) })
.toPromise();
}
async postObject(userId: string, objectId: string, serializedObject: string): Promise<void> {
await this.http
.post(
`${API_URI}me/${objectId}`,
{ data: serializedObject },
{ headers: ApiService.getAuthorizationHeader(userId) }
)
.toPromise();
}
async getRootId(userId: string): Promise<string> {
return await this.http
// @ts-ignore
.get<string>(`${API_URI}me/root`, { headers: ApiService.getAuthorizationHeader(userId), responseType: 'text' })
.toPromise();
}
async setRootId(userId: string, rootId: string): Promise<void> {
await this.http
.put(`${API_URI}me/root`, { root_id: rootId }, { headers: ApiService.getAuthorizationHeader(userId) })
.toPromise();
}
}

View file

@ -1,72 +1,35 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { StoreService } from './store.service';
import { Page } from '../model/page';
import { Root } from '../store/root'; import { Root } from '../store/root';
import { Serializable } from '../model/serializable'; import { MapStoreService } from './map-store.service';
import { Tower } from '../model/tower'; import { Data } from '../model/data';
import { Block } from '../model/block';
import { IPage } from '../interfaces/persistance/page';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class DataService extends Root<Page> { export class DataService extends Root<Data> {
constructor(private storeService: StoreService<Array<IPage>>) { private shouldSave = true;
constructor(private store: MapStoreService) {
super(); super();
this.init().catch();
}
get pages(): Array<Page> { this.store.data.subscribe(d => {
return this.children; if (d) {
} this.shouldSave = false;
this.changeKeys({ children: [d] });
save(timeout: number) {
this.storeService.scheduleSave(this.pages, timeout);
}
addPage(name: string) {
const page = new Page({
name,
userData: {},
towers: []
});
this.addChildren([page]);
page.addTower();
}
removePage(page: Page) {
this.changeKeys<any>({
children: this.children.filter(c => c !== page)
});
}
private async init() {
const pages = await this.storeService.load();
Serializable.childrenMap = {
Page: {
childrenListName: 'towers',
childrenConstructor: Tower,
childrenType: 'Tower'
},
Tower: {
childrenListName: 'blocks',
childrenConstructor: Block,
childrenType: 'Block'
},
Block: {
childrenListName: null,
childrenConstructor: null,
childrenType: null
} }
}; });
this.children$.subscribe(_ => { this.children$.subscribe(_ => {
this.log(); this.log();
}); });
this.addChildren(pages.map(p => new Page(p))); this.children$.subscribe(data => {
if (data && data.length && data[0]) {
this.children$.subscribe(_ => { if (!this.shouldSave) {
this.save(0); this.shouldSave = true;
return;
}
this.store.save(data[0]);
}
}); });
} }
} }

View file

@ -1,35 +1,191 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import * as uuid from 'uuid';
import { Data } from '../model/data';
import { ITower } from '../interfaces/persistance/tower';
import { IPage } from '../interfaces/persistance/page';
import { IData } from '../interfaces/persistance/data';
import { IUnique } from '../interfaces/persistance/unique';
import { ApiService } from './api.service';
import { Unique } from '../store/unique';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { Observable } from 'rxjs/internal/Observable';
const LOCAL_STORAGE_KEY = 'life-towers.data.v.3'; const LOCAL_STORAGE_KEY = 'life-towers.data.v.3';
interface LifeTowersData {
token: string;
root: string;
objects: {
[id: string]: IUnique;
};
}
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class MapStoreService<T> { export class MapStoreService {
readonly storage: { private state: LifeTowersData;
[id: number]: T; private canSaveTrigger: () => void;
} = {}; private canSave = new Promise(r => (this.canSaveTrigger = r));
private dataToSave: Data;
constructor() { private saveEverything = false;
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
if (localStorageData) { private readonly _data: BehaviorSubject<Data> = new BehaviorSubject(null);
this.storage = JSON.parse(localStorageData) as { readonly data: Observable<Data> = this._data.asObservable();
[id: number]: T;
constructor(private api: ApiService) {
const storedData: string = localStorage.getItem(LOCAL_STORAGE_KEY);
if (storedData) {
this.state = JSON.parse(storedData);
this.initWithLoad().catch();
} else {
this.initWithRegister().catch();
}
this.api.track(this.state.token).catch();
}
private static getSeed(): LifeTowersData {
const towerId = uuid.v4();
const tower: ITower = {
id: towerId,
name: null,
blocks: [],
baseColor: { h: 0, s: 100, l: 50 }
};
const pageId = uuid.v4();
const page: IPage = {
id: pageId,
name: 'My first page',
towers: [towerId],
userData: {
hideCreateTowerButton: false,
defaultDateRange: {
from: null,
to: null
}
}
};
const dataId = uuid.v4();
const data: IData = {
id: dataId,
pages: [pageId]
};
return {
token: uuid.v4(),
root: dataId,
objects: {
[dataId]: data,
[pageId]: page,
[towerId]: tower
}
};
}
save(root: Data) {
this.dataToSave = root;
setTimeout(() => {
if (this.dataToSave === root) {
this.realSave(root).catch();
}
}, 750);
}
private get root(): Data {
return new Data(this.state.objects[this.state.root] as IData, id => this.state.objects[id]);
}
get userToken(): string {
return this.state.token;
}
set userToken(value: string) {
this.state.token = value;
this.initWithLoad().catch();
}
private async realSave(root: Data): Promise<void> {
await this.canSave;
const waiting: Array<Unique> = [root];
const referenceSerializer = (e: Unique): string => {
waiting.push(e);
return e.id;
};
while (waiting.length > 0) {
const candidate = waiting.pop();
if (!this.saveEverything && this.state.objects.hasOwnProperty(candidate.id)) {
continue;
}
const serialized = candidate.serialize(referenceSerializer);
this.state.objects[candidate.id] = serialized;
this.api.postObject(this.state.token, candidate.id, JSON.stringify(serialized)).catch();
}
this.api.setRootId(this.state.token, root.id).catch();
this.state.root = root.id;
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(this.state));
}
private async initWithRegister(id?: string) {
this.state = MapStoreService.getSeed();
if (id) {
this.state.token = id;
}
this._data.next(this.root);
await this.api.register(this.state.token).catch();
this.canSaveTrigger();
this.saveEverything = true;
await this.realSave(this.root);
this.saveEverything = false;
}
private async initWithLoad() {
this.canSave = new Promise(r => (this.canSaveTrigger = r));
let realRoot: string;
try {
realRoot = await this.api.getRootId(this.state.token).catch();
} catch {
this.initWithRegister(this.state.token).catch();
return;
}
if (this.state.root !== realRoot) {
this.state.root = realRoot;
const root = await this.api.getObject(this.state.token, realRoot);
this.state.objects[this.state.root] = root;
const getUnknowns = async (element: any) => {
const childrenAliases = ['pages', 'towers', 'blocks'];
for (const childrenAlias of childrenAliases) {
if (element.hasOwnProperty(childrenAlias)) {
for (const p of element[childrenAlias]) {
if (!this.state.objects.hasOwnProperty(p)) {
const unknown = await this.api.getObject(this.state.token, p);
this.state.objects[p] = unknown;
await getUnknowns(unknown);
}
}
}
}
}; };
}
}
get(id: number) { await getUnknowns(root);
if (this.storage.hasOwnProperty(id)) {
return this.storage[id];
} }
} this._data.next(this.root);
this.canSaveTrigger();
add(id: number, value: T) {
if (this.storage.hasOwnProperty(id)) {
throw new Error('Key already set.');
}
this.storage[id] = value;
} }
} }

View file

@ -5,6 +5,7 @@ import { CancelService } from './cancel.service';
import { Page } from '../model/page'; import { Page } from '../model/page';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { Block } from '../model/block'; import { Block } from '../model/block';
import { Data } from '../model/data';
export enum ModalType { export enum ModalType {
blocks, blocks,
@ -33,8 +34,8 @@ export class ModalService {
return this.createPromiseAndPushToStack(input, ModalType.blocks); return this.createPromiseAndPushToStack(input, ModalType.blocks);
} }
showSettings(selectedPage: Observable<Page>): Promise<void> { showSettings(options: { page$: Observable<Page>; data$: Observable<Data> }): Promise<void> {
return this.createPromiseAndPushToStack(selectedPage, ModalType.settings); return this.createPromiseAndPushToStack(options, ModalType.settings);
} }
showRemoveTower(tower: Tower): Promise<void> { showRemoveTower(tower: Tower): Promise<void> {

View file

@ -1,122 +0,0 @@
import { Injectable } from '@angular/core';
const LOCAL_STORAGE_KEY = 'life-towers.data.v.2';
@Injectable({
providedIn: 'root'
})
export class StoreService<T> {
private saveScheduled = false;
private dataToSave: T;
private storedData: T;
private mockData: string = JSON.stringify([
{
name: 'Work & life',
userData: {},
towers: [
{
name: 'work',
baseColor: { h: 0, s: 100, l: 50 },
blocks: [
{
created: new Date(2015, 2, 13),
tag: 'a',
description: 'done it',
isDone: true
},
{
created: new Date(2016, 2, 15),
tag: 'go to school',
description: 'done it',
isDone: false
},
{
created: new Date(2017, 2, 15),
tag: 'go to work',
isDone: true
},
{
created: new Date(2018, 2, 13),
tag: 'go to work',
description: 'done it',
isDone: true
},
{
created: new Date(2019, 3, 13),
tag: 'go to work',
isDone: false
},
{
created: new Date(2019, 3, 15),
tag: 'go to school',
description: 'done it',
isDone: true
},
{
created: new Date(2019, 3, 15, 19),
tag: 'go to school',
isDone: false
}
]
},
{
baseColor: { h: 180, s: 100, l: 50 },
name: 'life',
blocks: [
{
created: new Date(2019, 3, 13),
tag: 'go home',
description: 'done it',
isDone: false
},
{
created: new Date(2019, 4, 13),
tag: 'go home',
isDone: false
},
{
created: new Date(2019, 4, 15),
tag: 'go to work',
description: 'done it',
isDone: false
},
{
created: new Date(2019, 4, 15, 14),
tag: 'go to work',
isDone: false
}
]
}
]
}
]);
constructor() {
const localStorageData = localStorage.getItem(LOCAL_STORAGE_KEY);
this.storedData = JSON.parse(localStorageData ? localStorageData : this.mockData) as T;
}
scheduleSave(data: T, timeout: number) {
this.dataToSave = data;
if (!this.saveScheduled) {
this.saveScheduled = true;
setTimeout(() => {
this.saveScheduled = false;
this.save(this.dataToSave).catch();
}, timeout);
}
}
async load(): Promise<T> {
console.log('load', this.storedData);
return this.storedData;
}
async save(data: T): Promise<void> {
this.storedData = data;
const stringified = JSON.stringify(this.storedData, null, 2);
console.log('save');
// console.log('save', stringified);
window.localStorage.setItem(LOCAL_STORAGE_KEY, stringified);
}
}

View file

@ -10,8 +10,8 @@ export class InnerNode extends Node implements InnerNodeState {
private nextVersion: this = null; private nextVersion: this = null;
readonly children: Array<InnerNode>; readonly children: Array<InnerNode>;
constructor(children: Array<InnerNode> = []) { protected constructor(children: Array<InnerNode> = [], id?: string) {
super(children); super(children, id);
this.children = children; this.children = children;
} }

View file

@ -8,8 +8,8 @@ export interface NodeState {
export abstract class Node extends Unique implements NodeState { export abstract class Node extends Unique implements NodeState {
abstract readonly children: Array<InnerNode>; abstract readonly children: Array<InnerNode>;
protected constructor(children: Array<InnerNode> = []) { protected constructor(children: Array<InnerNode> = [], id?: string) {
super(); super(id);
children.forEach(c => (c.parent = this)); children.forEach(c => (c.parent = this));
} }
protected abstract changeKeys<T extends NodeState>(props: Partial<T>): this; protected abstract changeKeys<T extends NodeState>(props: Partial<T>): this;
@ -32,7 +32,7 @@ export abstract class Node extends Unique implements NodeState {
protected _log(indent = ''): string { protected _log(indent = ''): string {
const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`; const basicInfo = `${indent} - ${this.constructor.name}, #${this.id}`;
let response = `${basicInfo}${' '.repeat(25 - basicInfo.length)}copies: ${this.copies}\n`; let response = `${basicInfo}${' '.repeat(70 - basicInfo.length)}copies: ${this.copies}\n`;
for (const c of this.children) { for (const c of this.children) {
response += `${c._log(indent + ' ')}`; response += `${c._log(indent + ' ')}`;
} }

View file

@ -1,29 +0,0 @@
import { uuidv4 } from 'uuid';
class Store {
private readonly onSet: (id: string, element: any) => void;
private readonly elements: {
[id: string]: any;
};
constructor(
elements: {
[id: string]: any;
},
onSet: (id: string, element: any) => void
) {
this.elements = elements;
this.onSet = onSet;
}
add(element: any): number {
const id = uuidv4();
this.elements[id] = element;
this.onSet(id, element);
return id;
}
get(id: string): any {
return this.elements[id];
}
}

View file

@ -1,18 +1,27 @@
export class Unique { import * as uuid from 'uuid';
private static nextId = 0; import { ISerializable } from '../interfaces/serializable';
private static store; import { IUnique } from '../interfaces/persistance/unique';
constructor() { export class Unique implements ISerializable, IUnique {
this.setUniqueness(); private static count = 0;
constructor(id?: string) {
if (id) {
this._id = id;
console.log('got id ' + id);
} else {
this.setUniqueness();
console.log('unique ' + this.id);
}
} }
static get ObjectCount(): number { static get ObjectCount(): number {
return Unique.nextId; return Unique.count;
} }
private _id: number; private _id: string;
get id(): number { get id(): string {
return this._id; return this._id;
} }
@ -23,7 +32,14 @@ export class Unique {
} }
protected setUniqueness() { protected setUniqueness() {
this._id = Unique.nextId++; this._id = uuid.v4();
Unique.count++;
this._copies++; this._copies++;
} }
serialize(referenceSerializer: (ref: object) => any): object {
return {
id: this.id
};
}
} }