UNPKG

openmate

Version:

OpenMate: A fast CLI tool to open local repos or entire collections in VS Code, Windsurf, or Cursor using simple shortcuts for an efficient developer workflow.

1,211 lines (1,023 loc) 33.3 kB
// ======================= // State Management // ======================= const AppState = { repos: [], collections: [], setRepos(repos) { this.repos = repos; }, setCollections(collections) { this.collections = collections; }, getFilteredRepos(searchTerm) { return this.filterItems(this.repos, searchTerm); }, getFilteredCollections(searchTerm) { return this.filterItems(this.collections, searchTerm); }, filterItems(items, searchTerm) { if (!searchTerm) return items; const term = searchTerm.toLowerCase(); return items.filter( (item) => item.name.toLowerCase().includes(term) || (item.path && item.path.toLowerCase().includes(term)) || (item.repos && item.repos.toLowerCase().includes(term)) ); }, }; // ======================= // Utility Functions // ======================= const Utils = { formatPath(path) { return path ? path.replace(/\\/g, "/") : ""; }, debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; }, createElement(tag, attributes = {}, children = []) { const element = document.createElement(tag); Object.entries(attributes).forEach(([key, value]) => { if (key === "className") { element.className = value; } else { element.setAttribute(key, value); } }); children.forEach((child) => { if (typeof child === "string") { element.appendChild(document.createTextNode(child)); } else { element.appendChild(child); } }); return element; }, }; // ======================= // Storage Management // ======================= const Storage = { THEME_KEY: "openmate-theme", IDE_KEY: "openmate-ide-selector", get(key) { try { return localStorage.getItem(key); } catch (e) { console.warn("Storage get failed:", e); return null; } }, set(key, value) { try { localStorage.setItem(key, value); } catch (e) { console.warn("Storage set failed:", e); } }, }; // ======================= // Theme Management // ======================= const ThemeManager = { init() { this.themeToggle = document.getElementById("theme-toggle"); this.themeIcon = document.querySelector(".theme-icon"); this.loadTheme(); this.bindEvents(); }, loadTheme() { const savedTheme = Storage.get(Storage.THEME_KEY); const systemPrefersDark = window.matchMedia( "(prefers-color-scheme: dark)" ).matches; const theme = savedTheme || (systemPrefersDark ? "dark" : "light"); this.setTheme(theme); }, setTheme(theme) { document.documentElement.setAttribute("data-theme", theme); this.themeIcon.textContent = theme === "dark" ? "☀️" : "🌙"; Storage.set(Storage.THEME_KEY, theme); }, toggle() { const currentTheme = document.documentElement.getAttribute("data-theme"); const newTheme = currentTheme === "dark" ? "light" : "dark"; this.setTheme(newTheme); }, bindEvents() { this.themeToggle.addEventListener("click", () => this.toggle()); }, }; // ======================= // IDE Selector Management // ======================= const IDEManager = { init() { this.selector = document.getElementById("ide-selector"); this.loadPreference(); this.bindEvents(); }, loadPreference() { const savedIDE = Storage.get(Storage.IDE_KEY); if (savedIDE) { this.selector.value = savedIDE; } }, getSelectedIDE() { return this.selector.value; }, bindEvents() { this.selector.addEventListener("change", (e) => { const selectedIDE = e.target.value; if (selectedIDE) { Storage.set(Storage.IDE_KEY, selectedIDE); NotificationManager.showSuccess(`Default IDE set to ${selectedIDE}`); } }); }, }; // ======================= // Notification Management // ======================= const NotificationManager = { show(message, type = "info", duration = 3000) { const colors = { success: "#4CAF50", error: "#F44336", info: "#2196F3", }; const notification = Utils.createElement( "div", { style: ` position: fixed; bottom: 20px; right: 20px; padding: 10px 20px; background: ${colors[type]}; color: white; border-radius: 4px; z-index: 1000; transition: opacity 0.3s ease; `, }, [message] ); document.body.appendChild(notification); setTimeout(() => { notification.style.opacity = "0"; setTimeout(() => { if (document.body.contains(notification)) { document.body.removeChild(notification); } }, 300); }, duration); }, showSuccess(message) { this.show(message, "success"); }, showError(message) { this.show(message, "error"); }, showInfo(message) { this.show(message, "info"); }, }; // ======================= // Modal Management // ======================= class Modal { constructor(modalId, formId, openBtnId) { this.modal = document.getElementById(modalId); this.form = document.getElementById(formId); this.openBtn = document.getElementById(openBtnId); this.closeBtn = this.modal.querySelector(".close"); this.bindEvents(); } bindEvents() { this.openBtn?.addEventListener("click", () => this.open()); this.closeBtn?.addEventListener("click", () => this.close()); // Close on outside click window.addEventListener("click", (e) => { if (e.target === this.modal) { this.close(); } }); // Close on Escape key document.addEventListener("keydown", (e) => { if (e.key === "Escape" && this.isOpen()) { this.close(); } }); } open() { this.modal.style.display = "flex"; document.body.style.overflow = "hidden"; this.onOpen(); } close() { this.modal.style.display = "none"; document.body.style.overflow = "auto"; this.form?.reset(); this.onClose(); } isOpen() { return this.modal.style.display === "flex"; } onOpen() { // Override in subclasses } onClose() { // Override in subclasses } } // ======================= // Repository Modal // ======================= class RepositoryModal extends Modal { constructor() { super("add-repo-modal", "add-repo-form", "add-repo-btn"); this.nameInput = document.getElementById("repo-name"); this.pathInput = document.getElementById("repo-path"); this.browseBtn = document.getElementById("browse-path"); this.bindRepositoryEvents(); } onOpen() { this.nameInput.focus(); } bindRepositoryEvents() { this.form.addEventListener("submit", (e) => this.handleSubmit(e)); this.browseBtn.addEventListener("click", () => this.browsePath()); } async handleSubmit(e) { e.preventDefault(); const name = this.nameInput.value.trim(); const path = this.pathInput.value.trim(); if (!this.validateInputs(name, path)) return; try { await window.electronAPI.addRepository({ name, path }); this.close(); NotificationManager.showSuccess("Repository added successfully!"); UIManager.refresh(); } catch (error) { NotificationManager.showError( `Error adding repository: ${error.message}` ); } } validateInputs(name, path) { if (!name || !path) { NotificationManager.showError("Please fill in all fields"); return false; } return true; } async browsePath() { try { const path = await window.electronAPI.openDirectoryDialog(); if (path) { this.pathInput.value = path; } } catch (error) { NotificationManager.showError( `Error selecting directory: ${error.message}` ); } } } // ======================= // Edit Repository Modal // ======================= class EditRepositoryModal extends Modal { constructor() { super("edit-repo-modal", "edit-repo-form"); this.nameInput = document.getElementById("edit-repo-name"); this.pathInput = document.getElementById("edit-repo-path"); this.originalNameInput = document.getElementById("edit-original-name"); this.browseBtn = document.getElementById("edit-browse-path"); this.closeBtn = document.querySelector("#edit-repo-modal .close"); this.bindEvents(); } open(repo) { // Store original values this.originalNameInput.value = repo.name; // Set current values (read-only) document.getElementById("current-repo-name").textContent = repo.name; document.getElementById("current-repo-path").textContent = repo.path; // Clear and reset form fields this.nameInput.value = repo.name; this.pathInput.value = repo.path; this.nameInput.placeholder = `Current: ${repo.name}`; this.pathInput.placeholder = `Current: ${repo.path}`; // Open modal and focus super.open(); this.nameInput.focus(); } bindEvents() { this.form?.addEventListener("submit", (e) => this.handleSubmit(e)); this.browseBtn?.addEventListener("click", () => this.browsePath()); this.closeBtn?.addEventListener("click", () => this.close()); } async handleSubmit(e) { e.preventDefault(); const originalName = this.originalNameInput.value; const name = this.nameInput.value.trim() || originalName; // Use original name if new name is empty const path = this.pathInput.value.trim() || this.pathInput.placeholder.replace("Current: ", ""); // Use current path if empty if (!this.validateInputs(name, path)) return; try { // Get current repositories data const data = await window.electronAPI.getReposData(); // Ensure we have the expected data structure if (!data || typeof data !== "object" || !data.repos) { throw new Error("Invalid repository data structure"); } // Check if the repository exists if (!data.repos[originalName]) { console.error("Repository not found in:", data.repos); throw new Error("Repository not found"); } // Check if name is being changed to an existing one (except current repo) if (name !== originalName && data.repos[name]) { NotificationManager.showError( "A repository with this name already exists" ); return; } // Create a copy of the repositories const updatedRepos = { ...data.repos }; // If name changed, remove old entry if (name !== originalName) { delete updatedRepos[originalName]; } // Update or add the repository with new values and updatedAt timestamp const now = new Date().toISOString(); updatedRepos[name] = { path, updatedAt: data.repos[originalName]?.updatedAt || now, }; // Save the updated repositories using the correct IPC method await window.electronAPI.writeReposFile({ ...data, repos: updatedRepos, }); // Refresh UI UIManager.refresh(); NotificationManager.showSuccess("Repository updated successfully"); this.close(); } catch (error) { console.error("Error updating repository:", error); NotificationManager.showError( `Failed to update repository: ${error.message}` ); } } validateInputs(name, path) { if (!name) { NotificationManager.showError("Please enter a repository name"); return false; } if (!path) { NotificationManager.showError("Please select a repository path"); return false; } return true; } async browsePath() { try { const path = await window.electronAPI.openDirectoryDialog(); if (path) { this.pathInput.value = path; } } catch (error) { NotificationManager.showError( `Error selecting directory: ${error.message}` ); } } } // ======================= // Edit Collection Modal // ======================= class EditCollectionModal extends Modal { constructor() { super("edit-collection-modal", "edit-collection-form"); this.nameInput = document.getElementById("edit-collection-name"); this.originalNameInput = document.getElementById( "edit-original-collection-name" ); this.currentNameEl = document.getElementById("current-collection-name"); this.currentReposEl = document.getElementById("current-collection-repos"); this.reposSelection = document.getElementById("edit-repos-selection"); this.closeBtn = document.querySelector("#edit-collection-modal .close"); this.bindEvents(); } async open(collection) { // Store original values this.originalNameInput.value = collection.name; // Set current values this.currentNameEl.textContent = collection.name; // Handle both array and string formats for backward compatibility const repoList = Array.isArray(collection.repos) ? collection.repos : (collection.repos || "") .split(",") .map((s) => s.trim()) .filter(Boolean); this.currentReposEl.textContent = repoList.length ? repoList.join(", ") : "No repositories"; this.nameInput.value = collection.name; this.nameInput.placeholder = `Current: ${collection.name}`; // Load repositories with current selection await this.loadRepositories(repoList); // Open modal and focus super.open(); this.nameInput.focus(); } bindEvents() { this.form?.addEventListener("submit", (e) => this.handleSubmit(e)); this.closeBtn?.addEventListener("click", () => this.close()); } async loadRepositories(selectedRepos = []) { try { const data = await window.electronAPI.getReposData(); this.renderRepositorySelection(data.repos || {}, selectedRepos); } catch (error) { NotificationManager.showError( `Error loading repositories: ${error.message}` ); this.reposSelection.innerHTML = '<div class="error">Error loading repositories. Please try again.</div>'; } } renderRepositorySelection(repos, selectedRepos = []) { if (!repos || Object.keys(repos).length === 0) { this.reposSelection.innerHTML = '<div class="loading">No repositories found. Add repositories first.</div>'; return; } const reposList = Object.entries(repos) .sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) .map( ([name, repo]) => ` <div class="repo-checkbox-item"> <input type="checkbox" id="edit-repo-${name}" name="repos" value="${name}" ${selectedRepos.includes(name) ? "checked" : ""}> <label for="edit-repo-${name}"> <span>${name}</span> <span class="repo-path">${repo.path}</span> </label> </div> ` ) .join(""); this.reposSelection.innerHTML = reposList; } async handleSubmit(e) { e.preventDefault(); const originalName = this.originalNameInput.value; const name = this.nameInput.value.trim(); const selectedRepos = this.getSelectedRepositories(); if (!this.validateInputs(name, selectedRepos)) return; try { const data = await window.electronAPI.getReposData(); // Check if name is being changed to an existing one (except current collection) if (name !== originalName && data.collections && data.collections[name]) { NotificationManager.showError( "A collection with this name already exists" ); return; } // Create a copy of the collections const updatedCollections = { ...data.collections }; // Remove old entry if name changed if (name !== originalName) { delete updatedCollections[originalName]; } // Update or add the collection with new values updatedCollections[name] = { name, repos: selectedRepos, // Save as array updatedAt: updatedCollections[originalName]?.updatedAt || new Date().toISOString(), }; // Save the updated collections await window.electronAPI.writeReposFile({ ...data, collections: updatedCollections, }); this.close(); NotificationManager.showSuccess("Collection updated successfully"); UIManager.refresh(); } catch (error) { console.error("Error updating collection:", error); NotificationManager.showError( `Failed to update collection: ${error.message}` ); } } getSelectedRepositories() { return Array.from( this.reposSelection.querySelectorAll('input[type="checkbox"]:checked') ).map((checkbox) => checkbox.value); } validateInputs(name, selectedRepos) { if (!name) { NotificationManager.showError("Please enter a collection name"); return false; } if (selectedRepos.length === 0) { NotificationManager.showError("Please select at least one repository"); return false; } return true; } } // ======================= // Collection Modal // ======================= class CollectionModal extends Modal { constructor() { super("add-collection-modal", "add-collection-form", "add-collection-btn"); this.nameInput = document.getElementById("collection-name"); this.reposSelection = document.getElementById("repos-selection"); this.bindCollectionEvents(); } async onOpen() { this.nameInput.focus(); await this.loadRepositories(); } onClose() { this.reposSelection.innerHTML = '<div class="loading">Loading repositories...</div>'; } bindCollectionEvents() { this.form.addEventListener("submit", (e) => this.handleSubmit(e)); } async loadRepositories() { try { const data = await window.electronAPI.getReposData(); this.renderRepositorySelection(data.repos || {}); } catch (error) { NotificationManager.showError( `Error loading repositories: ${error.message}` ); this.reposSelection.innerHTML = '<div class="error">Error loading repositories. Please try again.</div>'; } } renderRepositorySelection(repos) { if (!repos || Object.keys(repos).length === 0) { this.reposSelection.innerHTML = '<div class="loading">No repositories found. Add repositories first.</div>'; return; } const reposList = Object.entries(repos) .sort(([nameA], [nameB]) => nameA.localeCompare(nameB)) .map( ([name, repo]) => ` <div class="repo-checkbox-item"> <input type="checkbox" id="repo-${name}" name="repos" value="${name}"> <label for="repo-${name}"> <span>${name}</span> <span class="repo-path">${repo.path}</span> </label> </div> ` ) .join(""); this.reposSelection.innerHTML = reposList; } async handleSubmit(e) { e.preventDefault(); const name = this.nameInput.value.trim(); const selectedRepos = this.getSelectedRepositories(); if (!this.validateInputs(name, selectedRepos)) return; try { await this.saveCollection(name, selectedRepos); this.close(); NotificationManager.showSuccess("Collection created successfully!"); UIManager.refresh(); } catch (error) { NotificationManager.showError( `Error creating collection: ${error.message}` ); } } getSelectedRepositories() { return Array.from( this.reposSelection.querySelectorAll('input[type="checkbox"]:checked') ).map((checkbox) => checkbox.value); } validateInputs(name, selectedRepos) { if (!name) { NotificationManager.showError("Please enter a collection name"); return false; } if (selectedRepos.length === 0) { NotificationManager.showError("Please select at least one repository"); return false; } return true; } async saveCollection(name, selectedRepos) { const data = await window.electronAPI.getReposData(); if (data.collections && data.collections[name]) { throw new Error("A collection with this name already exists"); } if (!data.collections) data.collections = {}; data.collections[name] = { name, repos: selectedRepos, updatedAt: new Date().toISOString(), }; await window.electronAPI.writeReposFile(data); } } // ======================= // UI Management // ======================= const UIManager = { init(callbacks = {}) { this.searchInput = document.getElementById("search-input"); this.refreshBtn = document.getElementById("refresh-btn"); // Store callbacks this.handleEditRepository = callbacks.onEditRepository || (() => {}); this.handleEditCollection = callbacks.onEditCollection || (() => {}); // Initialize edit collection modal if it doesn't exist if (!this.editCollectionModal) { this.editCollectionModal = new EditCollectionModal(); } this.bindEvents(); this.refresh(); }, bindEvents() { // Debounce search to improve performance const debouncedUpdate = Utils.debounce(() => this.updateDisplay(), 300); this.searchInput.addEventListener("input", debouncedUpdate); this.refreshBtn.addEventListener("click", () => this.refresh()); }, async refresh() { this.showRefreshAnimation(); try { const data = await window.electronAPI.getReposData(); this.processData(data); this.updateDisplay(); } catch (error) { NotificationManager.showError(`Error refreshing data: ${error.message}`); } finally { this.hideRefreshAnimation(); } }, processData(data) { // Process repositories if (data.repos) { const repos = Object.entries(data.repos).map(([name, repo]) => ({ name, path: repo.path, updatedAt: repo.updatedAt || new Date().toISOString(), })); AppState.setRepos(repos); } else { AppState.setRepos([]); } // Process collections if (data.collections) { const collections = Object.values(data.collections).map((collection) => ({ name: collection.name, repos: Array.isArray(collection.repos) ? collection.repos.join(", ") : "", updatedAt: collection.updatedAt || new Date().toISOString(), })); AppState.setCollections(collections); } else { AppState.setCollections([]); } }, updateDisplay() { const searchTerm = this.searchInput.value.trim(); this.updateRepositoriesTable(AppState.getFilteredRepos(searchTerm)); this.updateCollectionsTable(AppState.getFilteredCollections(searchTerm)); }, updateRepositoriesTable(repos) { const container = document.getElementById("repos-list"); const table = document.getElementById("repos-table"); const loading = document.getElementById("loading-repos"); if (repos.length === 0) { loading.textContent = "No matching repositories found."; loading.style.display = "block"; table.style.display = "none"; return; } loading.style.display = "none"; table.style.display = "table"; container.innerHTML = repos .sort((a, b) => a.name.localeCompare(b.name)) .map((repo) => this.createRepositoryRow(repo)) .join(""); this.bindRepositoryEvents(); }, createRepositoryRow(repo) { return ` <tr data-path="${Utils.formatPath(repo.path)}" data-name="${repo.name}" class="clickable-repo-row"> <td><strong>${repo.name}</strong></td> <td class="path">${Utils.formatPath(repo.path)}</td> <td class="actions"> <div style="display: flex; gap: 5px"> <button class="edit-btn" data-name="${repo.name}" data-path="${Utils.formatPath(repo.path)}" data-type="repo" title="Edit repository">✏️</button> <button class="delete-btn" data-name="${repo.name}" data-type="repo" title="Delete repository">🗑️</button> </div> </td> </tr> `; }, bindRepositoryEvents() { // Bind click events for opening repositories document.querySelectorAll(".clickable-repo-row").forEach((row) => { row.addEventListener("click", (e) => this.handleRepositoryClick(e, row)); }); // Bind edit events document.querySelectorAll('.edit-btn[data-type="repo"]').forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); const name = btn.dataset.name; const path = btn.dataset.path; if (this.handleEditRepository) { this.handleEditRepository({ name, path }); } }); }); // Bind delete events document .querySelectorAll('.delete-btn[data-type="repo"]') .forEach((btn) => { btn.addEventListener("click", (e) => this.handleRepositoryDelete(e, btn) ); }); }, async handleRepositoryClick(e, row) { if (e.target.tagName === "BUTTON") return; const selectedIDE = IDEManager.getSelectedIDE(); if (!selectedIDE) { NotificationManager.showError("Please select an IDE first"); return; } const repoPath = row.getAttribute("data-path"); const repoName = row.getAttribute("data-name"); if (!repoPath) return; try { const result = await window.electronAPI.openInIDE({ name: repoName, path: repoPath, ide: selectedIDE, }); if (!result.success) { throw new Error(result.error || "Failed to open in IDE"); } NotificationManager.showSuccess( `Successfully opened ${repoPath} in ${selectedIDE}` ); } catch (error) { NotificationManager.showError(`Error opening IDE: ${error.message}`); } }, async handleRepositoryDelete(e, btn) { e.stopPropagation(); const name = btn.getAttribute("data-name"); // if (!confirm(`Are you sure you want to delete repository "${name}"?`)) // return; const loadingEl = this.showLoadingIndicator("Deleting..."); try { const result = await window.electronAPI.deleteRepo(name); if (result?.success) { NotificationManager.showSuccess(`Successfully deleted ${name}`); this.updateDisplay(); } else { throw new Error(result?.error || "Unknown error"); } } catch (error) { NotificationManager.showError(`Error deleting ${name}: ${error.message}`); } finally { this.hideLoadingIndicator(loadingEl); } }, updateCollectionsTable(collections) { const container = document.getElementById("collections-list"); const table = document.getElementById("collections-table"); const loading = document.getElementById("loading-collections"); if (collections.length === 0) { loading.textContent = "No matching collections found."; loading.style.display = "block"; table.style.display = "none"; return; } loading.style.display = "none"; table.style.display = "table"; container.innerHTML = collections .sort((a, b) => a.name.localeCompare(b.name)) .map((collection) => this.createCollectionRow(collection)) .join(""); this.bindCollectionEvents(); }, createCollectionRow(collection) { const collectionData = JSON.stringify(collection).replace(/"/g, "&quot;"); return ` <tr class="clickable-collection-row" data-collection="${collectionData}"> <td><strong>${collection.name}</strong></td> <td>${collection.repos}</td> <td class="actions"> <div style="display: flex; gap: 5px"> <button class="edit-btn" data-name="${collection.name}" data-type="collection" title="Edit collection">✏️</button> <button class="delete-btn" data-name="${collection.name}" data-type="collection" title="Delete collection">🗑️</button> </div> </td> </tr> `; }, bindCollectionEvents() { // Bind click events for opening collections document.querySelectorAll(".clickable-collection-row").forEach((row) => { row.addEventListener("click", (e) => this.handleCollectionClick(e, row)); }); // Bind edit events document .querySelectorAll('.edit-btn[data-type="collection"]') .forEach((btn) => { btn.addEventListener("click", (e) => { e.stopPropagation(); const collectionData = JSON.parse( btn .closest("tr") .getAttribute("data-collection") .replace(/&quot;/g, '"') ); if (this.editCollectionModal) { this.editCollectionModal.open(collectionData); } }); }); // Bind delete events document .querySelectorAll('.delete-btn[data-type="collection"]') .forEach((btn) => { btn.addEventListener("click", (e) => this.handleCollectionDelete(e, btn) ); }); }, async handleCollectionClick(e, row) { if (e.target.tagName === "BUTTON") return; try { const collection = JSON.parse(row.getAttribute("data-collection")); const reposData = await window.electronAPI.getReposData(); const repos = collection.repos.split(",").map((repo) => { const [name, path, ide] = repo .trim() .split("|") .map((s) => s.trim()); return { name, path, ide }; }); // Open all repositories in parallel const promises = repos.map(async (repo) => { const repoData = reposData.repos[repo.name]; if (!repoData) { NotificationManager.showError( `${repo.name}: Not found in repositories` ); return; } const ide = repo.ide || IDEManager.getSelectedIDE(); try { await window.electronAPI.openInIDE({ name: repo.name, path: repoData.path, ide: ide, }); } catch (error) { NotificationManager.showError( `Failed to open ${repo.name}: ${error.message}` ); } }); await Promise.allSettled(promises); NotificationManager.showSuccess(`Successfully opened ${collection.name}`); } catch (error) { NotificationManager.showError( `Error processing collection: ${error.message}` ); } }, async handleCollectionDelete(e, btn) { e.stopPropagation(); const name = btn.getAttribute("data-name"); // if (!confirm(`Are you sure you want to delete collection "${name}"?`)) // return; const loadingEl = this.showLoadingIndicator("Deleting..."); try { const result = await window.electronAPI.deleteCollection(name); if (result?.success) { NotificationManager.showSuccess(`Successfully deleted ${name}`); this.updateDisplay(); } else { throw new Error(result?.error || "Unknown error"); } } catch (error) { NotificationManager.showError(`Error deleting ${name}: ${error.message}`); } finally { this.hideLoadingIndicator(loadingEl); } }, showRefreshAnimation() { this.refreshBtn.classList.add("rotating"); }, hideRefreshAnimation() { setTimeout(() => { this.refreshBtn.classList.remove("rotating"); }, 1000); }, showLoadingIndicator(text) { const indicator = Utils.createElement( "div", { style: ` position: fixed; top: 10px; right: 10px; padding: 10px; background: rgba(0,0,0,0.7); color: white; border-radius: 4px; z-index: 1000; `, }, [text] ); document.body.appendChild(indicator); return indicator; }, hideLoadingIndicator(indicator) { if (document.body.contains(indicator)) { document.body.removeChild(indicator); } }, }; // ======================= // Application Initialization // ======================= class App { constructor() { this.init(); } init() { // Initialize core systems ThemeManager.init(); IDEManager.init(); // Initialize modals this.repositoryModal = new RepositoryModal(); this.editRepositoryModal = new EditRepositoryModal(); this.collectionModal = new CollectionModal(); this.editCollectionModal = new EditCollectionModal(); // Initialize UI manager with modals UIManager.init({ onEditRepository: (repo) => this.editRepositoryModal.open(repo), onEditCollection: (collection) => this.editCollectionModal.open(collection), }); // Handle electron API data this.bindElectronEvents(); console.log("👋 OpenMate application initialized successfully"); } bindElectronEvents() { if (window.electronAPI && window.electronAPI.onReposData) { window.electronAPI.onReposData((data) => { const { repos = [], collections = [] } = data; AppState.setRepos(repos); AppState.setCollections(collections); UIManager.updateDisplay(); }); } } } // ======================= // Initialize Application // ======================= document.addEventListener("DOMContentLoaded", () => { new App(); }); // Import styles import "./index.css";