feat(storage): implement MicronStorage for tab management, enhance TileCache with state management, and update DialogUtils and NotificationUtils for improved user interactions

This commit is contained in:
2026-01-03 16:06:45 -06:00
parent baa24e1cf9
commit 1fd1405e30
6 changed files with 201 additions and 10 deletions

View File

@@ -16,8 +16,10 @@ class DialogUtils {
// running inside electron, use ipc confirm
return window.electron.confirm(message);
} else {
// running inside normal browser, use browser alert
return window.confirm(message);
// running inside normal browser, use custom confirm dialog
return new Promise((resolve) => {
GlobalEmitter.emit("confirm", { message, resolve });
});
}
}

View File

@@ -9,6 +9,19 @@ class ElectronUtils {
}
}
static shutdown() {
if (window.electron) {
window.electron.shutdown();
}
}
static async getMemoryUsage() {
if (window.electron) {
return await window.electron.getMemoryUsage();
}
return null;
}
static showPathInFolder(path) {
if (window.electron) {
window.electron.showPathInFolder(path);

View File

@@ -3,6 +3,13 @@ import { reactive } from "vue";
// global state
const globalState = reactive({
unreadConversationsCount: 0,
activeCallTab: "phone",
blockedDestinations: [],
config: {
banished_effect_enabled: true,
banished_text: "BANISHED",
banished_color: "#dc2626",
},
});
export default globalState;

View File

@@ -0,0 +1,109 @@
const DB_NAME = "micron_editor_db";
const DB_VERSION = 1;
const STORE_NAME = "tabs";
class MicronStorage {
constructor() {
this.db = null;
this.initPromise = this.init();
}
async init() {
return new Promise((resolve, reject) => {
if (this.db) {
resolve(this.db);
return;
}
const idb =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
globalThis.indexedDB;
if (!idb) {
reject("IndexedDB not supported");
return;
}
const request = idb.open(DB_NAME, DB_VERSION);
request.onerror = (event) => {
console.error("IndexedDB error:", event.target.errorCode);
reject("IndexedDB error: " + event.target.errorCode);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME, { keyPath: "id", autoIncrement: true });
}
};
request.onsuccess = (event) => {
this.db = event.target.result;
resolve(this.db);
};
});
}
async saveTabs(tabs) {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
// Clear existing tabs before saving new ones to maintain order and structure
const clearRequest = store.clear();
clearRequest.onsuccess = () => {
if (tabs.length === 0) {
return;
}
// Ensure we are storing plain objects, not Vue proxies or other non-cloneable objects.
// JSON.parse/stringify is a safe way to strip proxies and ensure serializability
// for these simple tab objects.
const plainTabs = JSON.parse(JSON.stringify(tabs));
plainTabs.forEach((tab) => {
store.add(tab);
});
};
transaction.oncomplete = () => resolve();
transaction.onerror = (event) => reject(event.target.error);
});
}
async loadTabs() {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STORE_NAME], "readonly");
const store = transaction.objectStore(STORE_NAME);
const request = store.getAll();
request.onsuccess = () => {
resolve(request.result);
};
request.onerror = () => {
reject(request.error);
};
});
}
async clearAll() {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
}
export const micronStorage = new MicronStorage();

View File

@@ -1,5 +1,9 @@
class NotificationUtils {
static showIncomingCallNotification() {
if (window.electron) {
window.electron.showNotification("Incoming Call", "Someone is calling you.");
return;
}
Notification.requestPermission().then((result) => {
if (result === "granted") {
new window.Notification("Incoming Call", {
@@ -11,6 +15,10 @@ class NotificationUtils {
}
static showMissedCallNotification(from) {
if (window.electron) {
window.electron.showNotification("Missed Call", `You missed a call from ${from}.`);
return;
}
Notification.requestPermission().then((result) => {
if (result === "granted") {
new window.Notification("Missed Call", {
@@ -22,6 +30,10 @@ class NotificationUtils {
}
static showNewVoicemailNotification(from) {
if (window.electron) {
window.electron.showNotification("New Voicemail", `You have a new voicemail from ${from}.`);
return;
}
Notification.requestPermission().then((result) => {
if (result === "granted") {
new window.Notification("New Voicemail", {
@@ -32,11 +44,18 @@ class NotificationUtils {
});
}
static showNewMessageNotification() {
static showNewMessageNotification(from, content) {
if (window.electron) {
window.electron.showNotification(
"New Message",
from ? `${from}: ${content || "Sent a message."}` : "Someone sent you a message."
);
return;
}
Notification.requestPermission().then((result) => {
if (result === "granted") {
new window.Notification("New Message", {
body: "Someone sent you a message.",
body: from ? `${from}: ${content || "Sent a message."}` : "Someone sent you a message.",
tag: "new_message", // only ever show one new message notification at a time
});
}

View File

@@ -1,6 +1,7 @@
const DB_NAME = "meshchat_map_cache";
const DB_VERSION = 1;
const DB_VERSION = 2;
const STORE_NAME = "tiles";
const STATE_STORE = "map_state";
class TileCache {
constructor() {
@@ -10,7 +11,20 @@ class TileCache {
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DB_NAME, DB_VERSION);
const idb =
window.indexedDB ||
window.mozIndexedDB ||
window.webkitIndexedDB ||
window.msIndexedDB ||
globalThis.indexedDB;
if (!idb) {
console.warn("IndexedDB not supported, map caching will be disabled");
reject("IndexedDB not supported");
return;
}
const request = idb.open(DB_NAME, DB_VERSION);
request.onerror = (event) => reject("IndexedDB error: " + event.target.errorCode);
@@ -19,6 +33,9 @@ class TileCache {
if (!db.objectStoreNames.contains(STORE_NAME)) {
db.createObjectStore(STORE_NAME);
}
if (!db.objectStoreNames.contains(STATE_STORE)) {
db.createObjectStore(STATE_STORE);
}
};
request.onsuccess = (event) => {
@@ -52,17 +69,41 @@ class TileCache {
});
}
async clear() {
async getMapState(key) {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STORE_NAME], "readwrite");
const store = transaction.objectStore(STORE_NAME);
const request = store.clear();
const transaction = this.db.transaction([STATE_STORE], "readonly");
const store = transaction.objectStore(STATE_STORE);
const request = store.get(key);
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async setMapState(key, data) {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STATE_STORE], "readwrite");
const store = transaction.objectStore(STATE_STORE);
const request = store.put(data, key);
request.onsuccess = () => resolve();
request.onerror = () => reject(request.error);
});
}
async clear() {
await this.initPromise;
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([STORE_NAME, STATE_STORE], "readwrite");
transaction.objectStore(STORE_NAME).clear();
transaction.objectStore(STATE_STORE).clear();
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
}
}
export default new TileCache();