update: Add a left drawer to improve workbook management (#453)

* update: add leftbar to app

* style: a few cosmetic changes

* update: allow pinning workbooks

* style: show ellipsis button only on hover

* update: add basic responsiveness

* style: use active state when file and help menus are open

* style: increase transition time

* update: allow duplication of workbooks

* chore: standardize menus
This commit is contained in:
Daniel González-Albo
2025-10-19 10:20:31 +02:00
committed by GitHub
parent dd4467f95d
commit f2da24326b
14 changed files with 1007 additions and 212 deletions

View File

@@ -3,7 +3,10 @@ import { base64ToBytes, bytesToBase64 } from "./util";
const MAX_WORKBOOKS = 50;
type ModelsMetadata = Record<string, string>;
type ModelsMetadata = Record<
string,
{ name: string; createdAt: number; pinned?: boolean }
>;
export function updateNameSelectedWorkbook(model: Model, newName: string) {
const uuid = localStorage.getItem("selected");
@@ -12,7 +15,11 @@ export function updateNameSelectedWorkbook(model: Model, newName: string) {
if (modelsJson) {
try {
const models = JSON.parse(modelsJson);
models[uuid] = newName;
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");
@@ -28,7 +35,26 @@ export function getModelsMetadata(): ModelsMetadata {
if (!modelsJson) {
modelsJson = "{}";
}
return JSON.parse(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
@@ -48,14 +74,14 @@ function getNewName(existingNames: string[]): string {
export function createNewModel(): Model {
const models = getModelsMetadata();
const name = getNewName(Object.values(models));
const name = getNewName(Object.values(models).map((m) => m.name));
const model = new Model(name, "en", "UTC");
const uuid = crypto.randomUUID();
localStorage.setItem("selected", uuid);
localStorage.setItem(uuid, bytesToBase64(model.toBytes()));
models[uuid] = name;
models[uuid] = { name, createdAt: Date.now() };
localStorage.setItem("models", JSON.stringify(models));
return model;
}
@@ -103,7 +129,7 @@ export function saveModelToStorage(model: Model) {
modelsJson = "{}";
}
const models = JSON.parse(modelsJson);
models[uuid] = model.getName();
models[uuid] = { name: model.getName(), createdAt: Date.now() };
localStorage.setItem("models", JSON.stringify(models));
}
@@ -135,3 +161,79 @@ export function deleteSelectedModel(): Model | null {
}
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 = crypto.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;
}