Merge with store

This commit is contained in:
Andras Schmelczer 2026-05-28 08:42:34 +01:00
parent 706fe745d3
commit 3ad2766f82
128 changed files with 1185 additions and 0 deletions

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://store.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

@ -0,0 +1,28 @@
import { Injectable } from '@angular/core';
interface Subscriber {
callback: () => void;
object: object;
}
@Injectable({
providedIn: 'root'
})
export class CancelService {
private subscribers: Subscriber[] = [];
subscribe(object: object, callback: () => void) {
this.subscribers.push({
object,
callback
});
}
cancelAllExcept(except: object) {
this.subscribers.filter(s => s.object !== except).map(s => s.callback());
}
cancelAll() {
this.subscribers.map(s => s.callback());
}
}

View file

@ -0,0 +1,35 @@
import { Injectable } from '@angular/core';
import { Root } from '../store/root';
import { MapStoreService } from './map-store.service';
import { Data } from '../model/data';
@Injectable({
providedIn: 'root'
})
export class DataService extends Root<Data> {
private shouldSave = true;
constructor(private store: MapStoreService) {
super();
this.store.data.subscribe(d => {
if (d) {
this.shouldSave = false;
this.changeKeys({ children: [d] });
}
});
this.children$.subscribe(_ => {
this.log();
});
this.children$.subscribe(data => {
if (data && data.length && data[0]) {
if (!this.shouldSave) {
this.shouldSave = true;
return;
}
this.store.save(data[0]);
}
});
}
}

View file

@ -0,0 +1,191 @@
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';
interface LifeTowersData {
token: string;
root: string;
objects: {
[id: string]: IUnique;
};
}
@Injectable({
providedIn: 'root'
})
export class MapStoreService {
private state: LifeTowersData;
private canSaveTrigger: () => void;
private canSave = new Promise(r => (this.canSaveTrigger = r));
private dataToSave: Data;
private saveEverything = false;
private readonly _data: BehaviorSubject<Data> = new BehaviorSubject(null);
readonly data: Observable<Data> = this._data.asObservable();
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);
}
}
}
}
};
await getUnknowns(root);
}
this._data.next(this.root);
this.canSaveTrigger();
}
}

View file

@ -0,0 +1,85 @@
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';
import { Block } from '../model/block';
import { Data } from '../model/data';
export enum ModalType {
blocks,
settings,
removeTower,
removePage,
getStarted
}
interface Modal {
type: ModalType;
input: any;
resolve: (output: any) => void;
reject: () => void;
}
@Injectable({
providedIn: 'root'
})
export class ModalService {
private modalStack: Modal[] = [];
constructor(private cancelService: CancelService) {}
showBlocks(input: { tower$: Observable<Tower>; onlyDone: boolean; startBlock?: Block }): Promise<void> {
return this.createPromiseAndPushToStack(input, ModalType.blocks);
}
showSettings(options: { page$: Observable<Page>; data$: Observable<Data> }): Promise<void> {
return this.createPromiseAndPushToStack(options, ModalType.settings);
}
showRemoveTower(tower: Tower): Promise<void> {
return this.createPromiseAndPushToStack(tower, ModalType.removeTower);
}
showRemovePage(name: string): Promise<void> {
return this.createPromiseAndPushToStack(name, ModalType.removePage);
}
get active(): Modal {
return top(this.modalStack);
}
submit(output?: any) {
const modal = this.modalStack.pop();
if (modal) {
modal.resolve(output);
}
}
cancel() {
const modal = this.modalStack.pop();
if (modal) {
modal.reject();
}
}
private createPromiseAndPushToStack(input: any, type: ModalType): Promise<any> {
this.cancelService.cancelAll();
const modal = {
input,
type,
resolve: () => {},
reject: () => {}
};
const modalPromise = new Promise((resolve, reject) => {
modal.resolve = resolve;
modal.reject = reject;
});
this.modalStack.push(modal);
return modalPromise;
}
}