mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
358 lines
8.9 KiB
TypeScript
358 lines
8.9 KiB
TypeScript
/**
|
|
* Copyright (c) OpenLens Authors. All rights reserved.
|
|
* Licensed under MIT License. See LICENSE in root directory for more information.
|
|
*/
|
|
|
|
import { action, comparer, observable, makeObservable, computed } from "mobx";
|
|
import type { BaseStoreDependencies } from "../base-store";
|
|
import { BaseStore } from "../base-store";
|
|
import migrations from "../../migrations/hotbar-store";
|
|
import { toJS } from "../utils";
|
|
import type { CatalogEntity } from "../catalog";
|
|
import { broadcastMessage } from "../ipc";
|
|
import type { Hotbar, CreateHotbarData, CreateHotbarOptions } from "./types";
|
|
import { defaultHotbarCells, getEmptyHotbar } from "./types";
|
|
import { hotbarTooManyItemsChannel } from "../ipc/hotbar";
|
|
import type { GeneralEntity } from "../catalog-entities";
|
|
import type { Logger } from "../logger";
|
|
import assert from "assert";
|
|
|
|
export interface HotbarStoreModel {
|
|
hotbars: Hotbar[];
|
|
activeHotbarId: string;
|
|
}
|
|
|
|
interface Dependencies extends BaseStoreDependencies {
|
|
readonly catalogCatalogEntity: GeneralEntity;
|
|
readonly logger: Logger;
|
|
}
|
|
|
|
export class HotbarStore extends BaseStore<HotbarStoreModel> {
|
|
readonly displayName = "HotbarStore";
|
|
@observable hotbars: Hotbar[] = [];
|
|
@observable private _activeHotbarId!: string;
|
|
|
|
constructor(protected readonly dependencies: Dependencies) {
|
|
super(dependencies, {
|
|
configName: "lens-hotbar-store",
|
|
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
|
syncOptions: {
|
|
equals: comparer.structural,
|
|
},
|
|
migrations,
|
|
});
|
|
|
|
makeObservable(this);
|
|
}
|
|
|
|
@computed get activeHotbarId() {
|
|
return this._activeHotbarId;
|
|
}
|
|
|
|
/**
|
|
* If `hotbar` points to a known hotbar, make it active. Otherwise, ignore
|
|
* @param hotbar The hotbar instance, or the index, or its ID
|
|
*/
|
|
setActiveHotbar(hotbar: Hotbar | number | string) {
|
|
if (typeof hotbar === "number") {
|
|
if (hotbar >= 0 && hotbar < this.hotbars.length) {
|
|
this._activeHotbarId = this.hotbars[hotbar].id;
|
|
}
|
|
} else if (typeof hotbar === "string") {
|
|
if (this.findById(hotbar)) {
|
|
this._activeHotbarId = hotbar;
|
|
}
|
|
} else {
|
|
if (this.hotbars.indexOf(hotbar) >= 0) {
|
|
this._activeHotbarId = hotbar.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
private hotbarIndexById(id: string) {
|
|
return this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
|
}
|
|
|
|
private hotbarIndex(hotbar: Hotbar) {
|
|
return this.hotbars.indexOf(hotbar);
|
|
}
|
|
|
|
@computed get activeHotbarIndex() {
|
|
return this.hotbarIndexById(this.activeHotbarId);
|
|
}
|
|
|
|
@action
|
|
protected fromStore(data: Partial<HotbarStoreModel> = {}) {
|
|
if (!data.hotbars || !data.hotbars.length) {
|
|
const hotbar = getEmptyHotbar("Default");
|
|
const {
|
|
metadata: { uid, name, source },
|
|
} = this.dependencies.catalogCatalogEntity;
|
|
const initialItem = { entity: { uid, name, source }};
|
|
|
|
hotbar.items[0] = initialItem;
|
|
|
|
this.hotbars = [hotbar];
|
|
} else {
|
|
this.hotbars = data.hotbars;
|
|
}
|
|
|
|
this.hotbars.forEach(ensureExactHotbarItemLength);
|
|
|
|
if (data.activeHotbarId) {
|
|
this.setActiveHotbar(data.activeHotbarId);
|
|
}
|
|
|
|
if (!this.activeHotbarId) {
|
|
this.setActiveHotbar(0);
|
|
}
|
|
}
|
|
|
|
toJSON(): HotbarStoreModel {
|
|
const model: HotbarStoreModel = {
|
|
hotbars: this.hotbars,
|
|
activeHotbarId: this.activeHotbarId,
|
|
};
|
|
|
|
return toJS(model);
|
|
}
|
|
|
|
getActive(): Hotbar {
|
|
const hotbar = this.findById(this.activeHotbarId);
|
|
|
|
assert(hotbar, "There MUST always be an active hotbar");
|
|
|
|
return hotbar;
|
|
}
|
|
|
|
findByName(name: string) {
|
|
return this.hotbars.find((hotbar) => hotbar.name === name);
|
|
}
|
|
|
|
findById(id: string) {
|
|
return this.hotbars.find((hotbar) => hotbar.id === id);
|
|
}
|
|
|
|
@action
|
|
add(data: CreateHotbarData, { setActive = false }: CreateHotbarOptions = {}) {
|
|
const hotbar = getEmptyHotbar(data.name, data.id);
|
|
|
|
this.hotbars.push(hotbar);
|
|
|
|
if (setActive) {
|
|
this._activeHotbarId = hotbar.id;
|
|
}
|
|
}
|
|
|
|
@action
|
|
setHotbarName(id: string, name: string): void {
|
|
const index = this.hotbars.findIndex((hotbar) => hotbar.id === id);
|
|
|
|
if (index < 0) {
|
|
return this.dependencies.logger.warn(
|
|
`[HOTBAR-STORE]: cannot setHotbarName: unknown id`,
|
|
{ id },
|
|
);
|
|
}
|
|
|
|
this.hotbars[index].name = name;
|
|
}
|
|
|
|
@action
|
|
remove(hotbar: Hotbar) {
|
|
assert(this.hotbars.length >= 2, "Cannot remove the last hotbar");
|
|
|
|
this.hotbars = this.hotbars.filter((h) => h !== hotbar);
|
|
|
|
if (this.activeHotbarId === hotbar.id) {
|
|
this.setActiveHotbar(0);
|
|
}
|
|
}
|
|
|
|
@action
|
|
addToHotbar(item: CatalogEntity, cellIndex?: number) {
|
|
const hotbar = this.getActive();
|
|
const uid = item.getId();
|
|
const name = item.getName();
|
|
|
|
if (typeof uid !== "string") {
|
|
throw new TypeError("CatalogEntity's ID must be a string");
|
|
}
|
|
|
|
if (typeof name !== "string") {
|
|
throw new TypeError("CatalogEntity's NAME must be a string");
|
|
}
|
|
|
|
if (this.isAddedToActive(item)) {
|
|
return;
|
|
}
|
|
|
|
const entity = {
|
|
uid,
|
|
name,
|
|
source: item.metadata.source,
|
|
};
|
|
const newItem = { entity };
|
|
|
|
if (cellIndex === undefined) {
|
|
// Add item to empty cell
|
|
const emptyCellIndex = hotbar.items.indexOf(null);
|
|
|
|
if (emptyCellIndex != -1) {
|
|
hotbar.items[emptyCellIndex] = newItem;
|
|
} else {
|
|
broadcastMessage(hotbarTooManyItemsChannel);
|
|
}
|
|
} else if (0 <= cellIndex && cellIndex < hotbar.items.length) {
|
|
hotbar.items[cellIndex] = newItem;
|
|
} else {
|
|
this.dependencies.logger.error(
|
|
`[HOTBAR-STORE]: cannot pin entity to hotbar outside of index range`,
|
|
{ entityId: uid, hotbarId: hotbar.id, cellIndex },
|
|
);
|
|
}
|
|
}
|
|
|
|
@action
|
|
removeFromHotbar(uid: string): void {
|
|
const hotbar = this.getActive();
|
|
const index = hotbar.items.findIndex((item) => item?.entity.uid === uid);
|
|
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
|
|
hotbar.items[index] = null;
|
|
}
|
|
|
|
/**
|
|
* Remove all hotbar items that reference the `uid`.
|
|
* @param uid The `EntityId` that each hotbar item refers to
|
|
* @returns A function that will (in an action) undo the removing of the hotbar items. This function will not complete if the hotbar has changed.
|
|
*/
|
|
@action
|
|
removeAllHotbarItems(uid: string) {
|
|
for (const hotbar of this.hotbars) {
|
|
const index = hotbar.items.findIndex((i) => i?.entity.uid === uid);
|
|
|
|
if (index >= 0) {
|
|
hotbar.items[index] = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
findClosestEmptyIndex(from: number, direction = 1) {
|
|
let index = from;
|
|
const hotbar = this.getActive();
|
|
|
|
while (hotbar.items[index] != null) {
|
|
index += direction;
|
|
}
|
|
|
|
return index;
|
|
}
|
|
|
|
@action
|
|
restackItems(from: number, to: number): void {
|
|
const { items } = this.getActive();
|
|
const source = items[from];
|
|
const moveDown = from < to;
|
|
|
|
if (
|
|
from < 0 ||
|
|
to < 0 ||
|
|
from >= items.length ||
|
|
to >= items.length ||
|
|
isNaN(from) ||
|
|
isNaN(to)
|
|
) {
|
|
throw new Error("Invalid 'from' or 'to' arguments");
|
|
}
|
|
|
|
if (from == to) {
|
|
return;
|
|
}
|
|
|
|
items.splice(from, 1, null);
|
|
|
|
if (items[to] == null) {
|
|
items.splice(to, 1, source);
|
|
} else {
|
|
// Move cells up or down to closes empty cell
|
|
items.splice(this.findClosestEmptyIndex(to, moveDown ? -1 : 1), 1);
|
|
items.splice(to, 0, source);
|
|
}
|
|
}
|
|
|
|
switchToPrevious() {
|
|
let index = this.activeHotbarIndex - 1;
|
|
|
|
if (index < 0) {
|
|
index = this.hotbars.length - 1;
|
|
}
|
|
|
|
this.setActiveHotbar(index);
|
|
}
|
|
|
|
switchToNext() {
|
|
let index = this.activeHotbarIndex + 1;
|
|
|
|
if (index >= this.hotbars.length) {
|
|
index = 0;
|
|
}
|
|
|
|
this.setActiveHotbar(index);
|
|
}
|
|
|
|
/**
|
|
* Checks if entity already pinned to the active hotbar
|
|
*/
|
|
isAddedToActive(entity: CatalogEntity | null | undefined): boolean {
|
|
if (!entity) {
|
|
return false;
|
|
}
|
|
|
|
const indexInActiveHotbar = this.getActive().items.findIndex(item => item?.entity.uid === entity.getId());
|
|
|
|
return indexInActiveHotbar >= 0;
|
|
}
|
|
|
|
getDisplayLabel(hotbar: Hotbar): string {
|
|
return `${this.getDisplayIndex(hotbar)}: ${hotbar.name}`;
|
|
}
|
|
|
|
getDisplayIndex(hotbar: Hotbar): string {
|
|
const index = this.hotbarIndex(hotbar);
|
|
|
|
if (index < 0) {
|
|
return "??";
|
|
}
|
|
|
|
return `${index + 1}`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function ensures that there are always exactly `defaultHotbarCells`
|
|
* worth of items in the hotbar.
|
|
* @param hotbar The hotbar to modify
|
|
*/
|
|
function ensureExactHotbarItemLength(hotbar: Hotbar) {
|
|
// if there are not enough items
|
|
while (hotbar.items.length < defaultHotbarCells) {
|
|
hotbar.items.push(null);
|
|
}
|
|
|
|
// if for some reason the hotbar was overfilled before, remove as many entries
|
|
// as needed, but prefer empty slots and items at the end first.
|
|
while (hotbar.items.length > defaultHotbarCells) {
|
|
const lastNull = hotbar.items.lastIndexOf(null);
|
|
|
|
if (lastNull >= 0) {
|
|
hotbar.items.splice(lastNull, 1);
|
|
} else {
|
|
hotbar.items.length = defaultHotbarCells;
|
|
}
|
|
}
|
|
}
|