Merge pull request #1648 from thecodingmachine/feature-picture-of-user-merge
User's WOKA used in UI (3)
This commit is contained in:
commit
94dcf54675
@ -1,7 +1,7 @@
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { onDestroy } from "svelte";
|
|
||||||
|
|
||||||
import { gameManager } from "../../Phaser/Game/GameManager";
|
import { gameManager } from "../../Phaser/Game/GameManager";
|
||||||
|
import type { PictureStore } from "../../Stores/PictureStore";
|
||||||
|
import { onDestroy } from "svelte";
|
||||||
|
|
||||||
export let userId: number;
|
export let userId: number;
|
||||||
export let placeholderSrc: string;
|
export let placeholderSrc: string;
|
||||||
@ -9,14 +9,25 @@
|
|||||||
export let height: string = "62px";
|
export let height: string = "62px";
|
||||||
|
|
||||||
const gameScene = gameManager.getCurrentGameScene();
|
const gameScene = gameManager.getCurrentGameScene();
|
||||||
const playerWokaPictureStore = gameScene.getUserCompanionPictureStore(userId);
|
let companionWokaPictureStore: PictureStore | undefined;
|
||||||
|
if (userId === -1) {
|
||||||
|
companionWokaPictureStore = gameScene.CurrentPlayer.companion?.pictureStore;
|
||||||
|
} else {
|
||||||
|
companionWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(
|
||||||
|
userId,
|
||||||
|
(item) => item.companion?.pictureStore
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let src = placeholderSrc;
|
let src = placeholderSrc;
|
||||||
const unsubscribe = playerWokaPictureStore.picture.subscribe((source) => {
|
|
||||||
src = source ?? placeholderSrc;
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(unsubscribe);
|
if (companionWokaPictureStore) {
|
||||||
|
const unsubscribe = companionWokaPictureStore.subscribe((source) => {
|
||||||
|
src = source ?? placeholderSrc;
|
||||||
|
});
|
||||||
|
|
||||||
|
onDestroy(unsubscribe);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
|
<img {src} alt="" class="nes-pointer" style="--theme-width: {width}; --theme-height: {height}" />
|
||||||
|
@ -9,10 +9,16 @@
|
|||||||
export let height: string = "62px";
|
export let height: string = "62px";
|
||||||
|
|
||||||
const gameScene = gameManager.getCurrentGameScene();
|
const gameScene = gameManager.getCurrentGameScene();
|
||||||
const playerWokaPictureStore = gameScene.getUserWokaPictureStore(userId);
|
let playerWokaPictureStore;
|
||||||
|
if (userId === -1) {
|
||||||
|
playerWokaPictureStore = gameScene.CurrentPlayer.pictureStore;
|
||||||
|
} else {
|
||||||
|
playerWokaPictureStore = gameScene.MapPlayersByKey.getNestedStore(userId, (item) => item.pictureStore);
|
||||||
|
}
|
||||||
|
|
||||||
let src = placeholderSrc;
|
let src = placeholderSrc;
|
||||||
const unsubscribe = playerWokaPictureStore.picture.subscribe((source) => {
|
|
||||||
|
const unsubscribe = playerWokaPictureStore.subscribe((source) => {
|
||||||
src = source ?? placeholderSrc;
|
src = source ?? placeholderSrc;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -2,6 +2,8 @@ import Sprite = Phaser.GameObjects.Sprite;
|
|||||||
import Container = Phaser.GameObjects.Container;
|
import Container = Phaser.GameObjects.Container;
|
||||||
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
|
import { PlayerAnimationDirections, PlayerAnimationTypes } from "../Player/Animation";
|
||||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||||
|
import { Writable, writable } from "svelte/store";
|
||||||
|
import type { PictureStore } from "../../Stores/PictureStore";
|
||||||
|
|
||||||
export interface CompanionStatus {
|
export interface CompanionStatus {
|
||||||
x: number;
|
x: number;
|
||||||
@ -22,6 +24,7 @@ export class Companion extends Container {
|
|||||||
private companionName: string;
|
private companionName: string;
|
||||||
private direction: PlayerAnimationDirections;
|
private direction: PlayerAnimationDirections;
|
||||||
private animationType: PlayerAnimationTypes;
|
private animationType: PlayerAnimationTypes;
|
||||||
|
private readonly _pictureStore: Writable<string | undefined>;
|
||||||
|
|
||||||
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
|
constructor(scene: Phaser.Scene, x: number, y: number, name: string, texturePromise: Promise<string>) {
|
||||||
super(scene, x + 14, y + 4);
|
super(scene, x + 14, y + 4);
|
||||||
@ -36,11 +39,14 @@ export class Companion extends Container {
|
|||||||
this.animationType = PlayerAnimationTypes.Idle;
|
this.animationType = PlayerAnimationTypes.Idle;
|
||||||
|
|
||||||
this.companionName = name;
|
this.companionName = name;
|
||||||
|
this._pictureStore = writable(undefined);
|
||||||
|
|
||||||
texturePromise.then((resource) => {
|
texturePromise.then((resource) => {
|
||||||
this.addResource(resource);
|
this.addResource(resource);
|
||||||
this.invisible = false;
|
this.invisible = false;
|
||||||
this.emit("texture-loaded");
|
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||||
|
this._pictureStore.set(htmlImageElementSrc);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.scene.physics.world.enableBody(this);
|
this.scene.physics.world.enableBody(this);
|
||||||
@ -238,4 +244,8 @@ export class Companion extends Container {
|
|||||||
|
|
||||||
super.destroy();
|
super.destroy();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get pictureStore(): PictureStore {
|
||||||
|
return this._pictureStore;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ import type OutlinePipelinePlugin from "phaser3-rex-plugins/plugins/outlinepipel
|
|||||||
import { isSilentStore } from "../../Stores/MediaStore";
|
import { isSilentStore } from "../../Stores/MediaStore";
|
||||||
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
|
import { lazyLoadPlayerCharacterTextures, loadAllDefaultModels } from "./PlayerTexturesLoadingManager";
|
||||||
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
import { TexturesHelper } from "../Helpers/TexturesHelper";
|
||||||
|
import type { PictureStore } from "../../Stores/PictureStore";
|
||||||
|
import { Writable, writable } from "svelte/store";
|
||||||
|
|
||||||
const playerNameY = -25;
|
const playerNameY = -25;
|
||||||
|
|
||||||
@ -37,6 +39,7 @@ export abstract class Character extends Container {
|
|||||||
private emote: Phaser.GameObjects.DOMElement | null = null;
|
private emote: Phaser.GameObjects.DOMElement | null = null;
|
||||||
private emoteTween: Phaser.Tweens.Tween | null = null;
|
private emoteTween: Phaser.Tweens.Tween | null = null;
|
||||||
scene: GameScene;
|
scene: GameScene;
|
||||||
|
private readonly _pictureStore: Writable<string | undefined>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
scene: GameScene,
|
scene: GameScene,
|
||||||
@ -57,6 +60,7 @@ export abstract class Character extends Container {
|
|||||||
this.invisible = true;
|
this.invisible = true;
|
||||||
|
|
||||||
this.sprites = new Map<string, Sprite>();
|
this.sprites = new Map<string, Sprite>();
|
||||||
|
this._pictureStore = writable(undefined);
|
||||||
|
|
||||||
//textures are inside a Promise in case they need to be lazyloaded before use.
|
//textures are inside a Promise in case they need to be lazyloaded before use.
|
||||||
texturesPromise
|
texturesPromise
|
||||||
@ -64,7 +68,9 @@ export abstract class Character extends Container {
|
|||||||
this.addTextures(textures, frame);
|
this.addTextures(textures, frame);
|
||||||
this.invisible = false;
|
this.invisible = false;
|
||||||
this.playAnimation(direction, moving);
|
this.playAnimation(direction, moving);
|
||||||
this.emit("woka-textures-loaded");
|
return this.getSnapshot().then((htmlImageElementSrc) => {
|
||||||
|
this._pictureStore.set(htmlImageElementSrc);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
|
return lazyLoadPlayerCharacterTextures(scene.load, ["color_22", "eyes_23"]).then((textures) => {
|
||||||
@ -118,7 +124,7 @@ export abstract class Character extends Container {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSnapshot(): Promise<string> {
|
private async getSnapshot(): Promise<string> {
|
||||||
const sprites = Array.from(this.sprites.values()).map((sprite) => {
|
const sprites = Array.from(this.sprites.values()).map((sprite) => {
|
||||||
return { sprite, frame: 1 };
|
return { sprite, frame: 1 };
|
||||||
});
|
});
|
||||||
@ -137,9 +143,6 @@ export abstract class Character extends Container {
|
|||||||
public addCompanion(name: string, texturePromise?: Promise<string>): void {
|
public addCompanion(name: string, texturePromise?: Promise<string>): void {
|
||||||
if (typeof texturePromise !== "undefined") {
|
if (typeof texturePromise !== "undefined") {
|
||||||
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
|
this.companion = new Companion(this.scene, this.x, this.y, name, texturePromise);
|
||||||
this.companion.once("texture-loaded", () => {
|
|
||||||
this.emit("companion-texture-loaded", this.companion?.getSnapshot());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -394,4 +397,8 @@ export abstract class Character extends Container {
|
|||||||
this.emote = null;
|
this.emote = null;
|
||||||
this.playerName.setVisible(true);
|
this.playerName.setVisible(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public get pictureStore(): PictureStore {
|
||||||
|
return this._pictureStore;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,8 +77,6 @@ import { emoteStore, emoteMenuStore } from "../../Stores/EmoteStore";
|
|||||||
import { userIsAdminStore } from "../../Stores/GameStore";
|
import { userIsAdminStore } from "../../Stores/GameStore";
|
||||||
import { contactPageStore } from "../../Stores/MenuStore";
|
import { contactPageStore } from "../../Stores/MenuStore";
|
||||||
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
|
import { audioManagerFileStore, audioManagerVisibilityStore } from "../../Stores/AudioManagerStore";
|
||||||
import { UserWokaPictureStore } from "../../Stores/UserWokaPictureStore";
|
|
||||||
import { UserCompanionPictureStore } from "../../Stores/UserCompanionPictureStore";
|
|
||||||
|
|
||||||
import EVENT_TYPE = Phaser.Scenes.Events;
|
import EVENT_TYPE = Phaser.Scenes.Events;
|
||||||
import Texture = Phaser.Textures.Texture;
|
import Texture = Phaser.Textures.Texture;
|
||||||
@ -89,6 +87,7 @@ import DOMElement = Phaser.GameObjects.DOMElement;
|
|||||||
import Tileset = Phaser.Tilemaps.Tileset;
|
import Tileset = Phaser.Tilemaps.Tileset;
|
||||||
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
|
import SpriteSheetFile = Phaser.Loader.FileTypes.SpriteSheetFile;
|
||||||
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
import FILE_LOAD_ERROR = Phaser.Loader.Events.FILE_LOAD_ERROR;
|
||||||
|
import { MapStore } from "../../Stores/Utils/MapStore";
|
||||||
export interface GameSceneInitInterface {
|
export interface GameSceneInitInterface {
|
||||||
initPosition: PointInterface | null;
|
initPosition: PointInterface | null;
|
||||||
reconnecting: boolean;
|
reconnecting: boolean;
|
||||||
@ -128,7 +127,7 @@ export class GameScene extends DirtyScene {
|
|||||||
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
Terrains: Array<Phaser.Tilemaps.Tileset>;
|
||||||
CurrentPlayer!: Player;
|
CurrentPlayer!: Player;
|
||||||
MapPlayers!: Phaser.Physics.Arcade.Group;
|
MapPlayers!: Phaser.Physics.Arcade.Group;
|
||||||
MapPlayersByKey: Map<number, RemotePlayer> = new Map<number, RemotePlayer>();
|
MapPlayersByKey: MapStore<number, RemotePlayer> = new MapStore<number, RemotePlayer>();
|
||||||
Map!: Phaser.Tilemaps.Tilemap;
|
Map!: Phaser.Tilemaps.Tilemap;
|
||||||
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
|
Objects!: Array<Phaser.Physics.Arcade.Sprite>;
|
||||||
mapFile!: ITiledMap;
|
mapFile!: ITiledMap;
|
||||||
@ -204,11 +203,6 @@ export class GameScene extends DirtyScene {
|
|||||||
private objectsByType = new Map<string, ITiledMapObject[]>();
|
private objectsByType = new Map<string, ITiledMapObject[]>();
|
||||||
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
|
private embeddedWebsiteManager!: EmbeddedWebsiteManager;
|
||||||
private loader: Loader;
|
private loader: Loader;
|
||||||
private userWokaPictureStores: Map<number, UserWokaPictureStore> = new Map<number, UserWokaPictureStore>();
|
|
||||||
private userCompanionPictureStores: Map<number, UserCompanionPictureStore> = new Map<
|
|
||||||
number,
|
|
||||||
UserCompanionPictureStore
|
|
||||||
>();
|
|
||||||
|
|
||||||
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
constructor(private room: Room, MapUrlFile: string, customKey?: string | undefined) {
|
||||||
super({
|
super({
|
||||||
@ -342,24 +336,6 @@ export class GameScene extends DirtyScene {
|
|||||||
this.loader.addLoader();
|
this.loader.addLoader();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getUserWokaPictureStore(userId: number) {
|
|
||||||
let store = this.userWokaPictureStores.get(userId);
|
|
||||||
if (!store) {
|
|
||||||
store = new UserWokaPictureStore();
|
|
||||||
this.userWokaPictureStores.set(userId, store);
|
|
||||||
}
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getUserCompanionPictureStore(userId: number) {
|
|
||||||
let store = this.userCompanionPictureStores.get(userId);
|
|
||||||
if (!store) {
|
|
||||||
store = new UserCompanionPictureStore();
|
|
||||||
this.userCompanionPictureStores.set(userId, store);
|
|
||||||
}
|
|
||||||
return store;
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
|
// FIXME: we need to put a "unknown" instead of a "any" and validate the structure of the JSON we are receiving.
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
private async onMapLoad(data: any): Promise<void> {
|
private async onMapLoad(data: any): Promise<void> {
|
||||||
@ -1466,7 +1442,7 @@ ${escapedMessage}
|
|||||||
|
|
||||||
this.MapPlayers.remove(player);
|
this.MapPlayers.remove(player);
|
||||||
});
|
});
|
||||||
this.MapPlayersByKey = new Map<number, RemotePlayer>();
|
this.MapPlayersByKey.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getExitUrl(layer: ITiledMapLayer): string | undefined {
|
private getExitUrl(layer: ITiledMapLayer): string | undefined {
|
||||||
@ -1559,14 +1535,6 @@ ${escapedMessage}
|
|||||||
this.companion,
|
this.companion,
|
||||||
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
|
this.companion !== null ? lazyLoadCompanionResource(this.load, this.companion) : undefined
|
||||||
);
|
);
|
||||||
this.CurrentPlayer.once("woka-textures-loaded", () => {
|
|
||||||
this.savePlayerWokaPicture(this.CurrentPlayer, -1);
|
|
||||||
});
|
|
||||||
this.CurrentPlayer.once("companion-texture-loaded", (snapshotPromise: Promise<string>) => {
|
|
||||||
snapshotPromise.then((snapshot: string) => {
|
|
||||||
this.savePlayerCompanionPicture(-1, snapshot);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
this.CurrentPlayer.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
this.CurrentPlayer.on("pointerdown", (pointer: Phaser.Input.Pointer) => {
|
||||||
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
|
if (pointer.wasTouch && (pointer.event as TouchEvent).touches.length > 1) {
|
||||||
return; //we don't want the menu to open when pinching on a touch screen.
|
return; //we don't want the menu to open when pinching on a touch screen.
|
||||||
@ -1594,15 +1562,6 @@ ${escapedMessage}
|
|||||||
this.createCollisionWithPlayer();
|
this.createCollisionWithPlayer();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async savePlayerWokaPicture(character: Character, userId: number): Promise<void> {
|
|
||||||
const htmlImageElementSrc = await character.getSnapshot();
|
|
||||||
this.getUserWokaPictureStore(userId).picture.set(htmlImageElementSrc);
|
|
||||||
}
|
|
||||||
|
|
||||||
private savePlayerCompanionPicture(userId: number, snapshot: string): void {
|
|
||||||
this.getUserCompanionPictureStore(userId).picture.set(snapshot);
|
|
||||||
}
|
|
||||||
|
|
||||||
pushPlayerPosition(event: HasPlayerMovedEvent) {
|
pushPlayerPosition(event: HasPlayerMovedEvent) {
|
||||||
if (this.lastMoveEventSent === event) {
|
if (this.lastMoveEventSent === event) {
|
||||||
return;
|
return;
|
||||||
@ -1790,9 +1749,6 @@ ${escapedMessage}
|
|||||||
addPlayerData.companion,
|
addPlayerData.companion,
|
||||||
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
|
addPlayerData.companion !== null ? lazyLoadCompanionResource(this.load, addPlayerData.companion) : undefined
|
||||||
);
|
);
|
||||||
player.once("woka-textures-loaded", () => {
|
|
||||||
this.savePlayerWokaPicture(player, addPlayerData.userId);
|
|
||||||
});
|
|
||||||
this.MapPlayers.add(player);
|
this.MapPlayers.add(player);
|
||||||
this.MapPlayersByKey.set(player.userId, player);
|
this.MapPlayersByKey.set(player.userId, player);
|
||||||
player.updatePosition(addPlayerData.position);
|
player.updatePosition(addPlayerData.position);
|
||||||
|
6
front/src/Stores/PictureStore.ts
Normal file
6
front/src/Stores/PictureStore.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store that contains the player/companion avatar picture
|
||||||
|
*/
|
||||||
|
export type PictureStore = Readable<string | undefined>;
|
@ -12,7 +12,7 @@ let idCount = 0;
|
|||||||
function createPlayersStore() {
|
function createPlayersStore() {
|
||||||
let players = new Map<number, PlayerInterface>();
|
let players = new Map<number, PlayerInterface>();
|
||||||
|
|
||||||
const { subscribe, set, update } = writable(players);
|
const { subscribe, set, update } = writable<Map<number, PlayerInterface>>(players);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
subscribe,
|
subscribe,
|
||||||
|
@ -1,8 +0,0 @@
|
|||||||
import { writable, Writable } from "svelte/store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A store that contains the player companion picture
|
|
||||||
*/
|
|
||||||
export class UserCompanionPictureStore {
|
|
||||||
constructor(public picture: Writable<string | undefined> = writable(undefined)) {}
|
|
||||||
}
|
|
@ -1,8 +0,0 @@
|
|||||||
import { writable, Writable } from "svelte/store";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A store that contains the player avatar picture
|
|
||||||
*/
|
|
||||||
export class UserWokaPictureStore {
|
|
||||||
constructor(public picture: Writable<string | undefined> = writable(undefined)) {}
|
|
||||||
}
|
|
122
front/src/Stores/Utils/MapStore.ts
Normal file
122
front/src/Stores/Utils/MapStore.ts
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
import type { Readable, Subscriber, Unsubscriber, Writable } from "svelte/store";
|
||||||
|
import { get, readable, writable } from "svelte/store";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is it a Map? Is it a Store? No! It's a MapStore!
|
||||||
|
*
|
||||||
|
* The MapStore behaves just like a regular JS Map, but... it is also a regular Svelte store.
|
||||||
|
*
|
||||||
|
* As a bonus, you can also get a store on any given key of the map.
|
||||||
|
*
|
||||||
|
* For instance:
|
||||||
|
*
|
||||||
|
* const mapStore = new MapStore<string, string>();
|
||||||
|
* mapStore.getStore('foo').subscribe((value) => {
|
||||||
|
* console.log('Foo key has been written to the store. New value: ', value);
|
||||||
|
* });
|
||||||
|
* mapStore.set('foo', 'bar');
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* Even better, if the items stored in map contain stores, you can directly get the store to those values:
|
||||||
|
*
|
||||||
|
* const mapStore = new MapStore<string, {
|
||||||
|
* nestedStore: Readable<string>
|
||||||
|
* }>();
|
||||||
|
*
|
||||||
|
* mapStore.getNestedStore('foo', item => item.nestedStore).subscribe((value) => {
|
||||||
|
* console.log('Foo key has been written to the store or the nested store has been updated. New value: ', value);
|
||||||
|
* });
|
||||||
|
* mapStore.set('foo', {
|
||||||
|
* nestedStore: writable('bar')
|
||||||
|
* });
|
||||||
|
* // Whenever the nested store is updated OR the 'foo' key is overwritten, the store returned by mapStore.getNestedStore
|
||||||
|
* // will be triggered.
|
||||||
|
*/
|
||||||
|
export class MapStore<K, V> extends Map<K, V> implements Readable<Map<K, V>> {
|
||||||
|
private readonly store = writable(this);
|
||||||
|
private readonly storesByKey = new Map<K, Writable<V | undefined>>();
|
||||||
|
|
||||||
|
subscribe(run: Subscriber<Map<K, V>>, invalidate?: (value?: Map<K, V>) => void): Unsubscriber {
|
||||||
|
return this.store.subscribe(run, invalidate);
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
super.clear();
|
||||||
|
this.store.set(this);
|
||||||
|
this.storesByKey.forEach((store) => {
|
||||||
|
store.set(undefined);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
delete(key: K): boolean {
|
||||||
|
const result = super.delete(key);
|
||||||
|
if (result) {
|
||||||
|
this.store.set(this);
|
||||||
|
this.storesByKey.get(key)?.set(undefined);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: K, value: V): this {
|
||||||
|
super.set(key, value);
|
||||||
|
this.store.set(this);
|
||||||
|
this.storesByKey.get(key)?.set(value);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
getStore(key: K): Readable<V | undefined> {
|
||||||
|
const store = writable(this.get(key), () => {
|
||||||
|
return () => {
|
||||||
|
// No more subscribers!
|
||||||
|
this.storesByKey.delete(key);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
this.storesByKey.set(key, store);
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an "inner" store inside a value stored in the map.
|
||||||
|
*/
|
||||||
|
getNestedStore<T>(key: K, accessor: (value: V) => Readable<T> | undefined): Readable<T | undefined> {
|
||||||
|
const initVal = this.get(key);
|
||||||
|
let initStore: Readable<T> | undefined;
|
||||||
|
let initStoreValue: T | undefined;
|
||||||
|
if (initVal) {
|
||||||
|
initStore = accessor(initVal);
|
||||||
|
if (initStore !== undefined) {
|
||||||
|
initStoreValue = get(initStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return readable(initStoreValue, (set) => {
|
||||||
|
const storeByKey = this.getStore(key);
|
||||||
|
|
||||||
|
let unsubscribeDeepStore: Unsubscriber | undefined;
|
||||||
|
const unsubscribe = storeByKey.subscribe((newMapValue) => {
|
||||||
|
if (unsubscribeDeepStore) {
|
||||||
|
unsubscribeDeepStore();
|
||||||
|
}
|
||||||
|
if (newMapValue === undefined) {
|
||||||
|
set(undefined);
|
||||||
|
} else {
|
||||||
|
const deepValueStore = accessor(newMapValue);
|
||||||
|
if (deepValueStore !== undefined) {
|
||||||
|
set(get(deepValueStore));
|
||||||
|
|
||||||
|
unsubscribeDeepStore = deepValueStore.subscribe((value) => {
|
||||||
|
set(value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
unsubscribe();
|
||||||
|
if (unsubscribeDeepStore) {
|
||||||
|
unsubscribeDeepStore();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
97
front/tests/Stores/Utils/MapStoreTest.ts
Normal file
97
front/tests/Stores/Utils/MapStoreTest.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import "jasmine";
|
||||||
|
import {MapStore} from "../../../src/Stores/Utils/MapStore";
|
||||||
|
import type {Readable, Writable} from "svelte/store";
|
||||||
|
import {get, writable} from "svelte/store";
|
||||||
|
|
||||||
|
describe("Main store", () => {
|
||||||
|
it("Set / delete / clear triggers main store updates", () => {
|
||||||
|
const mapStore = new MapStore<string, string>();
|
||||||
|
|
||||||
|
let triggered = false;
|
||||||
|
|
||||||
|
mapStore.subscribe((map) => {
|
||||||
|
triggered = true;
|
||||||
|
expect(map).toBe(mapStore);
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(triggered).toBeTrue();
|
||||||
|
triggered = false;
|
||||||
|
mapStore.set('foo', 'bar');
|
||||||
|
expect(triggered).toBeTrue();
|
||||||
|
|
||||||
|
triggered = false;
|
||||||
|
mapStore.delete('baz');
|
||||||
|
expect(triggered).toBe(false);
|
||||||
|
mapStore.delete('foo');
|
||||||
|
expect(triggered).toBe(true);
|
||||||
|
|
||||||
|
triggered = false;
|
||||||
|
mapStore.clear();
|
||||||
|
expect(triggered).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates stores for keys with getStore", () => {
|
||||||
|
|
||||||
|
const mapStore = new MapStore<string, string>();
|
||||||
|
|
||||||
|
let valueReceivedInStoreForFoo: string|undefined;
|
||||||
|
let valueReceivedInStoreForBar: string|undefined;
|
||||||
|
|
||||||
|
mapStore.set('foo', 'someValue');
|
||||||
|
|
||||||
|
mapStore.getStore('foo').subscribe((value) => {
|
||||||
|
valueReceivedInStoreForFoo = value;
|
||||||
|
});
|
||||||
|
const unsubscribeBar = mapStore.getStore('bar').subscribe((value) => {
|
||||||
|
valueReceivedInStoreForBar = value;
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(valueReceivedInStoreForFoo).toBe('someValue');
|
||||||
|
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||||
|
mapStore.set('foo', 'someOtherValue');
|
||||||
|
expect(valueReceivedInStoreForFoo).toBe('someOtherValue');
|
||||||
|
mapStore.delete('foo');
|
||||||
|
expect(valueReceivedInStoreForFoo).toBe(undefined);
|
||||||
|
mapStore.set('bar', 'baz');
|
||||||
|
expect(valueReceivedInStoreForBar).toBe('baz');
|
||||||
|
mapStore.clear();
|
||||||
|
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||||
|
unsubscribeBar();
|
||||||
|
mapStore.set('bar', 'fiz');
|
||||||
|
expect(valueReceivedInStoreForBar).toBe(undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates stores with getStoreByAccessor", () => {
|
||||||
|
const mapStore = new MapStore<string, {
|
||||||
|
foo: string,
|
||||||
|
store: Writable<string>
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const fooStore = mapStore.getNestedStore('foo', (value) => {
|
||||||
|
return value.store;
|
||||||
|
});
|
||||||
|
|
||||||
|
mapStore.set('foo', {
|
||||||
|
foo: 'bar',
|
||||||
|
store: writable('init')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(get(fooStore)).toBe('init');
|
||||||
|
|
||||||
|
mapStore.get('foo')?.store.set('newVal');
|
||||||
|
|
||||||
|
expect(get(fooStore)).toBe('newVal');
|
||||||
|
|
||||||
|
mapStore.set('foo', {
|
||||||
|
foo: 'bar',
|
||||||
|
store: writable('anotherVal')
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(get(fooStore)).toBe('anotherVal');
|
||||||
|
|
||||||
|
mapStore.delete('foo');
|
||||||
|
|
||||||
|
expect(get(fooStore)).toBeUndefined();
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user