263 lines
7.8 KiB
TypeScript
263 lines
7.8 KiB
TypeScript
import { Model } from "@ironcalc/workbook";
|
|
import { base64ToBytes, bytesToBase64 } from "./util";
|
|
|
|
const MAX_WORKBOOKS = 50;
|
|
|
|
type ModelsMetadata = Record<
|
|
string,
|
|
{ name: string; createdAt: number; pinned?: boolean }
|
|
>;
|
|
|
|
function randomUUID(): string {
|
|
try {
|
|
return crypto.randomUUID();
|
|
} catch {
|
|
// Fallback for environments without crypto.randomUUID()
|
|
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
const r = (Math.random() * 16) | 0;
|
|
const v = c === "x" ? r : (r & 0x3) | 0x8;
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
}
|
|
|
|
export function updateNameSelectedWorkbook(model: Model, newName: string) {
|
|
const uuid = localStorage.getItem("selected");
|
|
if (uuid) {
|
|
const modelsJson = localStorage.getItem("models");
|
|
if (modelsJson) {
|
|
try {
|
|
const models = JSON.parse(modelsJson);
|
|
if (models[uuid]) {
|
|
models[uuid].name = newName;
|
|
} else {
|
|
models[uuid] = { name: newName, createdAt: Date.now() };
|
|
}
|
|
localStorage.setItem("models", JSON.stringify(models));
|
|
} catch (e) {
|
|
console.warn("Failed saving new name");
|
|
}
|
|
}
|
|
const modeBytes = model.toBytes();
|
|
localStorage.setItem(uuid, bytesToBase64(modeBytes));
|
|
}
|
|
}
|
|
|
|
export function getModelsMetadata(): ModelsMetadata {
|
|
let modelsJson = localStorage.getItem("models");
|
|
if (!modelsJson) {
|
|
modelsJson = "{}";
|
|
}
|
|
const models = JSON.parse(modelsJson);
|
|
|
|
// Migrate old format to new format
|
|
const migratedModels: ModelsMetadata = {};
|
|
for (const [uuid, value] of Object.entries(models)) {
|
|
if (typeof value === "string") {
|
|
// Old format: just the name string
|
|
migratedModels[uuid] = { name: value, createdAt: Date.now() };
|
|
} else if (typeof value === "object" && value !== null && "name" in value) {
|
|
// New format: object with name and createdAt
|
|
migratedModels[uuid] = value as { name: string; createdAt: number };
|
|
}
|
|
}
|
|
|
|
// Save migrated data back to localStorage
|
|
if (JSON.stringify(models) !== JSON.stringify(migratedModels)) {
|
|
localStorage.setItem("models", JSON.stringify(migratedModels));
|
|
}
|
|
|
|
return migratedModels;
|
|
}
|
|
|
|
// Pick a different name Workbook{N} where N = 1, 2, 3
|
|
function getNewName(existingNames: string[]): string {
|
|
const baseName = "Workbook";
|
|
let index = 1;
|
|
while (index < MAX_WORKBOOKS) {
|
|
const name = `${baseName}${index}`;
|
|
index += 1;
|
|
if (!existingNames.includes(name)) {
|
|
return name;
|
|
}
|
|
}
|
|
// FIXME: Too many workbooks?
|
|
return "Workbook-Infinity";
|
|
}
|
|
|
|
export function createModelWithSafeTimezone(name: string): Model {
|
|
try {
|
|
const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
return new Model(name, "en", tz);
|
|
} catch {
|
|
console.warn("Failed to get timezone, defaulting to UTC");
|
|
return new Model(name, "en", "UTC");
|
|
}
|
|
}
|
|
|
|
export function createNewModel(): Model {
|
|
const models = getModelsMetadata();
|
|
const name = getNewName(Object.values(models).map((m) => m.name));
|
|
|
|
const model = createModelWithSafeTimezone(name);
|
|
const uuid = randomUUID();
|
|
localStorage.setItem("selected", uuid);
|
|
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
|
|
|
|
models[uuid] = { name, createdAt: Date.now() };
|
|
localStorage.setItem("models", JSON.stringify(models));
|
|
return model;
|
|
}
|
|
|
|
export function loadSelectedModelFromStorage(): Model | null {
|
|
const uuid = localStorage.getItem("selected");
|
|
if (uuid) {
|
|
// We try to load the selected model
|
|
const modelBytesString = localStorage.getItem(uuid);
|
|
if (modelBytesString) {
|
|
return Model.from_bytes(base64ToBytes(modelBytesString));
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// check if storage is empty
|
|
export function isStorageEmpty(): boolean {
|
|
const modelsJson = localStorage.getItem("models");
|
|
if (!modelsJson) {
|
|
return true;
|
|
}
|
|
try {
|
|
const models = JSON.parse(modelsJson);
|
|
return Object.keys(models).length === 0;
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
export function saveSelectedModelInStorage(model: Model) {
|
|
const uuid = localStorage.getItem("selected");
|
|
if (uuid) {
|
|
const modeBytes = model.toBytes();
|
|
localStorage.setItem(uuid, bytesToBase64(modeBytes));
|
|
}
|
|
}
|
|
|
|
export function saveModelToStorage(model: Model) {
|
|
const uuid = randomUUID();
|
|
localStorage.setItem("selected", uuid);
|
|
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
|
|
let modelsJson = localStorage.getItem("models");
|
|
if (!modelsJson) {
|
|
modelsJson = "{}";
|
|
}
|
|
const models = JSON.parse(modelsJson);
|
|
models[uuid] = { name: model.getName(), createdAt: Date.now() };
|
|
localStorage.setItem("models", JSON.stringify(models));
|
|
}
|
|
|
|
export function selectModelFromStorage(uuid: string): Model | null {
|
|
localStorage.setItem("selected", uuid);
|
|
const modelBytesString = localStorage.getItem(uuid);
|
|
if (modelBytesString) {
|
|
return Model.from_bytes(base64ToBytes(modelBytesString));
|
|
}
|
|
return null;
|
|
}
|
|
|
|
export function getSelectedUuid(): string | null {
|
|
return localStorage.getItem("selected");
|
|
}
|
|
|
|
export function deleteSelectedModel(): Model | null {
|
|
const uuid = localStorage.getItem("selected");
|
|
if (!uuid) {
|
|
return null;
|
|
}
|
|
localStorage.removeItem(uuid);
|
|
const metadata = getModelsMetadata();
|
|
delete metadata[uuid];
|
|
localStorage.setItem("models", JSON.stringify(metadata));
|
|
const uuids = Object.keys(metadata);
|
|
if (uuids.length === 0) {
|
|
return createNewModel();
|
|
}
|
|
return selectModelFromStorage(uuids[0]);
|
|
}
|
|
|
|
export function deleteModelByUuid(uuid: string): Model | null {
|
|
localStorage.removeItem(uuid);
|
|
const metadata = getModelsMetadata();
|
|
delete metadata[uuid];
|
|
localStorage.setItem("models", JSON.stringify(metadata));
|
|
|
|
// If this was the selected model, we need to select a different one
|
|
const selectedUuid = localStorage.getItem("selected");
|
|
if (selectedUuid === uuid) {
|
|
const uuids = Object.keys(metadata);
|
|
if (uuids.length === 0) {
|
|
return createNewModel();
|
|
}
|
|
// Find the newest workbook by creation timestamp
|
|
const newestUuid = uuids.reduce((newest, current) => {
|
|
const newestTime = metadata[newest]?.createdAt || 0;
|
|
const currentTime = metadata[current]?.createdAt || 0;
|
|
return currentTime > newestTime ? current : newest;
|
|
});
|
|
return selectModelFromStorage(newestUuid);
|
|
}
|
|
|
|
// If it wasn't the selected model, return the currently selected model
|
|
if (selectedUuid) {
|
|
const modelBytesString = localStorage.getItem(selectedUuid);
|
|
if (modelBytesString) {
|
|
return Model.from_bytes(base64ToBytes(modelBytesString));
|
|
}
|
|
}
|
|
|
|
// Fallback to creating a new model if no valid selected model
|
|
return createNewModel();
|
|
}
|
|
|
|
export function togglePinWorkbook(uuid: string): void {
|
|
const metadata = getModelsMetadata();
|
|
if (metadata[uuid]) {
|
|
metadata[uuid].pinned = !metadata[uuid].pinned;
|
|
localStorage.setItem("models", JSON.stringify(metadata));
|
|
}
|
|
}
|
|
|
|
export function isWorkbookPinned(uuid: string): boolean {
|
|
const metadata = getModelsMetadata();
|
|
return metadata[uuid]?.pinned || false;
|
|
}
|
|
|
|
export function duplicateModel(uuid: string): Model | null {
|
|
const originalModel = selectModelFromStorage(uuid);
|
|
if (!originalModel) return null;
|
|
|
|
const duplicatedModel = Model.from_bytes(originalModel.toBytes());
|
|
const models = getModelsMetadata();
|
|
const originalName = models[uuid]?.name || "Workbook";
|
|
const existingNames = Object.values(models).map((m) => m.name);
|
|
|
|
// Find next available number
|
|
let counter = 1;
|
|
let newName = `${originalName} (${counter})`;
|
|
while (existingNames.includes(newName)) {
|
|
counter++;
|
|
newName = `${originalName} (${counter})`;
|
|
}
|
|
|
|
duplicatedModel.setName(newName);
|
|
|
|
const newUuid = randomUUID();
|
|
localStorage.setItem("selected", newUuid);
|
|
localStorage.setItem(newUuid, bytesToBase64(duplicatedModel.toBytes()));
|
|
|
|
models[newUuid] = { name: newName, createdAt: Date.now() };
|
|
localStorage.setItem("models", JSON.stringify(models));
|
|
|
|
return duplicatedModel;
|
|
}
|