Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Update.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ✅)
Expand Down
11 changes: 9 additions & 2 deletions js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
55 changes: 39 additions & 16 deletions js/migration.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [];
Expand All @@ -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);
}
});
}
}
65 changes: 49 additions & 16 deletions js/profiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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();
}
});

Expand Down Expand Up @@ -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++;
Expand Down
4 changes: 2 additions & 2 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Expand Down
33 changes: 8 additions & 25 deletions styles/options.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down