Files
DemonGatanjieu-Anomalous_Mo…/web/modules/shortcut_layout.js
T

179 lines
6.2 KiB
JavaScript

/**
* shortcut_layout.js - State and Persistence for Customizable Shortcut Bar
*
* Implements schema validation, capacity limits, pinning, unpinning,
* reordering and reset logic with localStorage persistence.
*/
import { CATALOG_TOOLS } from './tool_registry.js';
export const SHORTCUT_STORAGE_KEY = 'anomalous_shortcut_layout_v1';
export const SHORTCUT_SCHEMA_VERSION = 1;
export const MAX_PINNED_SHORTCUTS = 4;
export const DEFAULT_PINNED_SHORTCUTS = Object.freeze([
'scan',
'doctor',
'assistant',
'materials',
]);
// In-memory fallback if localStorage fails or is blocked
let sessionMemoryLayout = null;
const VALID_CATALOG_IDS = new Set(CATALOG_TOOLS.map(t => t.id));
export function isValidCatalogToolId(id) {
return VALID_CATALOG_IDS.has(id);
}
/**
* Normalizes an array of tool IDs:
* - Drops invalid IDs, anchor IDs and non-strings
* - Deduplicates preserving first occurrence
* - Truncates to MAX_PINNED_SHORTCUTS
*/
export function normalizePinnedIds(ids) {
if (!Array.isArray(ids)) return [...DEFAULT_PINNED_SHORTCUTS];
const seen = new Set();
const result = [];
for (const id of ids) {
if (typeof id === 'string' && VALID_CATALOG_IDS.has(id) && !seen.has(id)) {
seen.add(id);
result.push(id);
if (result.length >= MAX_PINNED_SHORTCUTS) break;
}
}
return result;
}
/**
* Load shortcut layout from storage.
* Explicit empty array is valid and preserved.
*/
export function loadShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (sessionMemoryLayout !== null && !storage) {
return [...sessionMemoryLayout];
}
if (!storage) {
return sessionMemoryLayout !== null ? [...sessionMemoryLayout] : [...DEFAULT_PINNED_SHORTCUTS];
}
try {
const raw = storage.getItem(SHORTCUT_STORAGE_KEY);
if (raw === null || raw === undefined) {
return [...DEFAULT_PINNED_SHORTCUTS];
}
const parsed = JSON.parse(raw);
if (!parsed || parsed.schemaVersion !== SHORTCUT_SCHEMA_VERSION || !Array.isArray(parsed.pinned)) {
return [...DEFAULT_PINNED_SHORTCUTS];
}
return normalizePinnedIds(parsed.pinned);
} catch (err) {
console.warn('[AMB] Failed to read shortcut layout from storage, resetting to defaults:', err);
sessionMemoryLayout = [...DEFAULT_PINNED_SHORTCUTS];
return [...DEFAULT_PINNED_SHORTCUTS];
}
}
/**
* Persist shortcut layout to storage and memory.
*/
export function saveShortcutLayout(pinnedIds, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const normalized = normalizePinnedIds(pinnedIds);
sessionMemoryLayout = [...normalized];
if (!storage) {
return { success: true, layout: normalized, memoryOnly: true };
}
try {
const payload = JSON.stringify({
schemaVersion: SHORTCUT_SCHEMA_VERSION,
pinned: normalized,
});
storage.setItem(SHORTCUT_STORAGE_KEY, payload);
return { success: true, layout: normalized, memoryOnly: false };
} catch (err) {
console.warn('[AMB] Failed to write shortcut layout to storage, saved to session memory:', err);
return { success: true, layout: normalized, memoryOnly: true, error: err };
}
}
/**
* Pin a tool to the shortcut bar.
* If already pinned, reorder to targetIndex.
* If not pinned and at capacity, rejects with 'capacity_full'.
*/
export function pinTool(toolId, targetIndex = -1, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
if (!isValidCatalogToolId(toolId)) {
return { success: false, reason: 'unknown_tool', layout: loadShortcutLayout(storage) };
}
const current = loadShortcutLayout(storage);
const existingIndex = current.indexOf(toolId);
if (existingIndex !== -1) {
// Already pinned: reorder to target position
current.splice(existingIndex, 1);
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
current.splice(insertAt, 0, toolId);
saveShortcutLayout(current, storage);
return { success: true, reordered: true, layout: current };
}
if (current.length >= MAX_PINNED_SHORTCUTS) {
return { success: false, reason: 'capacity_full', layout: current };
}
const insertAt = (targetIndex >= 0 && targetIndex <= current.length) ? targetIndex : current.length;
current.splice(insertAt, 0, toolId);
saveShortcutLayout(current, storage);
return { success: true, reordered: false, layout: current };
}
/**
* Unpin a tool from the shortcut bar (returns it to toolbox only).
*/
export function unpinTool(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
const index = current.indexOf(toolId);
if (index === -1) {
return { success: true, changed: false, layout: current };
}
current.splice(index, 1);
saveShortcutLayout(current, storage);
return { success: true, changed: true, layout: current };
}
/**
* Reorder an item in the shortcut bar.
*/
export function reorderShortcut(fromIndex, toIndex, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
if (fromIndex < 0 || fromIndex >= current.length || toIndex < 0 || toIndex >= current.length || fromIndex === toIndex) {
return { success: false, layout: current };
}
const [item] = current.splice(fromIndex, 1);
current.splice(toIndex, 0, item);
saveShortcutLayout(current, storage);
return { success: true, layout: current };
}
/**
* Reset shortcut layout to defaults.
*/
export function resetShortcutLayout(storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
saveShortcutLayout(DEFAULT_PINNED_SHORTCUTS, storage);
return { success: true, layout: [...DEFAULT_PINNED_SHORTCUTS] };
}
/**
* Check if a tool is currently pinned.
*/
export function isToolPinned(toolId, storage = (typeof localStorage !== 'undefined' ? localStorage : null)) {
const current = loadShortcutLayout(storage);
return current.includes(toolId);
}