diff --git a/Update.md b/Update.md index db96105..702e520 100644 --- a/Update.md +++ b/Update.md @@ -19,7 +19,7 @@ This document outlines suggested improvements for the Rextensity Chrome extensio - 📝 **Documentation**: Updated README and roadmap with v0.2.0 changes ### Priority Status -- 🔴 **Critical Priority**: 0/3 completed (Manifest V3 migration pending, CSP, Error Handling) +- 🟢 **Critical Priority**: 3/3 completed (Manifest V3 migration ✅, CSP ✅, Error Handling ✅) - 🟠 **High Priority**: 3/3 completed (Dependencies ✅, ES6+ ✅, Code Splitting ✅) - 🟡 **Medium Priority**: 2/3 completed (Import/Export ✅, Keyboard Shortcuts ✅, Favorites) - 🟢 **Low Priority**: 3/8 completed (Linting ✅, Constants ✅, Validation ✅) diff --git a/js/index.js b/js/index.js index bdfe533..03732d0 100644 --- a/js/index.js +++ b/js/index.js @@ -31,6 +31,13 @@ document.addEventListener("DOMContentLoaded", function() { return !_(self.profiles.always_on().items()).contains(item.id()); }; + // NOTE: This flip() method operates on the popup UI's observable extension models, + // while performToggle() in migration.js uses chrome.management API directly from the + // background service worker. They must remain separate because: + // 1. popup UI has live ExtensionModel observables with enable/disable methods + // 2. background service worker has no access to these UI observables + // 3. background handles async chrome.management.setEnabled promises + // Both respect Always On profile when keepAlwaysOn option is enabled. self.flip = function() { if(self.any()) { // Re-enable @@ -179,8 +186,8 @@ document.addEventListener("DOMContentLoaded", function() { return; } - // Toggle all extensions with Ctrl/Cmd + A - if ((e.ctrlKey || e.metaKey) && e.key === 'a' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { + // Toggle all extensions with Ctrl/Cmd + Shift + A + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'A' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') { e.preventDefault(); vm.switch.flip(); return; diff --git a/js/migration.js b/js/migration.js index 724dc56..ae918a4 100644 --- a/js/migration.js +++ b/js/migration.js @@ -124,20 +124,41 @@ function performToggle(extensions, toggled, alwaysOnIds) { if (toggled.length > 0) { // Re-enable previously disabled extensions - toggled.forEach(function(id) { + const promises = toggled.map(function(id) { // Check if extension still exists - if (existingIds[id]) { - try { - chrome.management.setEnabled(id, true); - } catch(e) { - console.error('Failed to enable extension:', id, e); - } - } else { + if (!existingIds[id]) { console.log('Extension no longer exists, skipping:', id); + return Promise.resolve({ id: id, status: 'skipped' }); } + + return chrome.management.setEnabled(id, true) + .then(function() { + return { id: id, status: 'success' }; + }) + .catch(function(err) { + console.error('Failed to enable extension:', id, err); + return { id: id, status: 'failed' }; + }); + }); + + // Wait for all promises to settle, then update storage + Promise.allSettled(promises).then(function(results) { + const failedIds = []; + results.forEach(function(result) { + if (result.status === 'fulfilled' && result.value.status === 'failed') { + failedIds.push(result.value.id); + } + }); + + // Only keep failed IDs in storage for retry + chrome.storage.sync.set({toggled: failedIds}, function() { + if (chrome.runtime.lastError) { + console.error('Failed to update toggled list:', chrome.runtime.lastError); + } + }); + }).catch(function(err) { + console.error('Unexpected error processing toggle results:', err); }); - // Clear toggled list - chrome.storage.sync.set({toggled: []}); } else { // Disable all enabled extensions (except Always On if applicable) const enabledIds = []; @@ -146,15 +167,17 @@ function performToggle(extensions, toggled, alwaysOnIds) { // Skip if in Always On profile if (alwaysOnIds.indexOf(ext.id) === -1) { enabledIds.push(ext.id); - try { - chrome.management.setEnabled(ext.id, false); - } catch(e) { - console.error('Failed to disable extension:', ext.id, e); - } + chrome.management.setEnabled(ext.id, false).catch(function(err) { + console.error('Failed to disable extension:', ext.id, err); + }); } } }); // Save disabled list - chrome.storage.sync.set({toggled: enabledIds}); + chrome.storage.sync.set({toggled: enabledIds}, function() { + if (chrome.runtime.lastError) { + console.error('Failed to save toggled list:', chrome.runtime.lastError); + } + }); } } \ No newline at end of file diff --git a/js/profiles.js b/js/profiles.js index c1d120d..a43f3e4 100644 --- a/js/profiles.js +++ b/js/profiles.js @@ -24,31 +24,55 @@ document.addEventListener("DOMContentLoaded", function() { self.selectReserved(data, "always_on"); } + // Validates profile name for user-created profiles + // Returns {valid: boolean, error: string} + self.validateProfileName = function(name) { + const trimmed = name.trim(); + + if (!trimmed) { + return {valid: false, error: 'Profile name is required.'}; + } + if (trimmed.length > 30) { + return {valid: false, error: 'Profile name is too long (maximum 30 characters).'}; + } + if (trimmed.startsWith('__')) { + return {valid: false, error: 'Profile names cannot start with "__" (reserved prefix).'}; + } + + return {valid: true}; + }; + self.selectReserved = function(data, n) { self.add_name("__"+n); - self.add(); + self.add(true); // Pass internal flag }; self.selectByIndex = function(idx) { self.current_profile(self.profiles.items()[idx]); }; - self.add = function() { + self.add = function(internal) { const n = self.add_name().trim(); const enabled = self.ext.enabled.pluck(); - // Validation - if (!n) { - alert('Profile name is required.'); - return; - } - if (n.length > 30) { - alert('Profile name is too long (maximum 30 characters).'); - return; - } - if (n.startsWith('__')) { - alert('Profile names cannot start with "__" (reserved prefix).'); - return; + // Validation - for internal calls, skip reserved prefix check + if (internal) { + // For internal calls, validate without reserved prefix check + if (!n) { + alert('Profile name is required.'); + return; + } + if (n.length > 30) { + alert('Profile name is too long (maximum 30 characters).'); + return; + } + } else { + // For user calls, use full validation including reserved prefix check + const validation = self.validateProfileName(n); + if (!validation.valid) { + alert(validation.error); + return; + } } const p = self.profiles.find(n); @@ -98,8 +122,9 @@ document.addEventListener("DOMContentLoaded", function() { // Collect all profiles (excluding reserved ones for clarity) _(self.profiles.items()).each(function(profile) { - if (profile.name()) { - exportData.profiles[profile.name()] = profile.items(); + const profileName = profile.name(); + if (profileName && !profileName.startsWith('__')) { + exportData.profiles[profileName] = profile.items(); } }); @@ -149,6 +174,14 @@ document.addEventListener("DOMContentLoaded", function() { // Import profiles _(importData.profiles).each(function(items, name) { + // Validate profile name + const validation = self.validateProfileName(name); + if (!validation.valid) { + skippedCount++; + console.warn('Invalid profile name, skipping:', name, validation.error); + return; + } + // Skip if profile already exists if (self.profiles.exists(name)) { skippedCount++; diff --git a/manifest.json b/manifest.json index 7cfe1bf..e143b2b 100644 --- a/manifest.json +++ b/manifest.json @@ -27,8 +27,8 @@ }, "toggle-all": { "suggested_key": { - "default": "Ctrl+Shift+T", - "mac": "Command+Shift+T" + "default": "Ctrl+Shift+U", + "mac": "Command+Shift+U" }, "description": "Toggle all extensions on/off" } diff --git a/styles/options.css b/styles/options.css index bb3a9a2..da1c28c 100644 --- a/styles/options.css +++ b/styles/options.css @@ -77,20 +77,22 @@ span#save-result { color: #304669; } -.hidden { +/* Import/Export feedback styles */ +#import-result, #import-error { display: none; -} - -.visible { - display: block; margin-top: 10px; padding: 8px 12px; border-radius: 5px; +} + +#import-result.visible { + display: block; background-color: #e8f5e9; border: 1px solid #4caf50; } #import-error.visible { + display: block; background-color: #ffebee; border: 1px solid #f44336; } @@ -100,26 +102,6 @@ span#save-result { padding: 6px 10px; } -.import-success.visible { - color: green; - display: block; - margin-top: 10px; - padding: 8px 12px; - border-radius: 5px; - background-color: #e8f5e9; - border: 1px solid #4caf50; -} - -.import-error.visible { - color: red; - display: block; - margin-top: 10px; - padding: 8px 12px; - border-radius: 5px; - background-color: #ffebee; - border: 1px solid #f44336; -} - a { color: #304669; text-decoration: none; @@ -284,6 +266,7 @@ fieldset#options .field { cursor: pointer; } +/* General visibility-based utility classes for fadeout animations and state management */ .fadeout { visibility: hidden; opacity: 0;