initial commit

This commit is contained in:
wt committed 2026-10-09 15:53:24 +07:00
commit dded3d2fac
19 files changed
+2888

No files matched your search

+1
View File
@@ -0,0 +1 @@
web-ext-artifacts
+7
View File
@@ -0,0 +1,7 @@
default: build
build:
web-ext build --overwrite-dest --filename proxyswitcherforkbywt.xpi
clean:
rm -rvf web-ext-artifacts
+99
View File
@@ -0,0 +1,99 @@
# Firefox Proxy Switcher
A Firefox extension that allows selective application of proxy servers to specific domains, providing flexible proxy management for different websites.
## Features
- **Domain-specific proxy configuration**: Apply different proxy settings to different domains
- **Proxy profiles**: Save and reuse proxy configurations
- **HTTP and SOCKS5 support**: Support for both HTTP and SOCKS5 proxy types
- **Authentication support**: Username/password authentication for proxies
- **Tag-based organization**: Organize domains with custom tags
- **Quick toggle**: Enable/disable proxy for domains with one click
## Security and Privacy
This extension is designed with user privacy and security in mind:
### Data Storage
- All configuration data (domains, profiles, proxy settings) is stored locally in the browser using Firefox's `browser.storage.local` API
- Proxy credentials (usernames/passwords) are encrypted and stored only in the local browser storage
- No data is transmitted to external servers or collected by the extension
### Network Permissions
The extension requests the following permissions necessary for its functionality:
- `proxy`: Required to configure and manage proxy settings
- `storage`: Required to save user configuration locally
- `tabs`: Required to determine the current domain when configuring proxy settings
- `activeTab`: Required to access the current tab for configuration
- `webRequest`: Required to handle proxy authentication requests
- `webRequestBlocking`: Required to intercept and modify network requests for proxy routing
- `<all_urls>`: Required to apply proxy settings to any website the user visits
### How Proxy Routing Works
1. When a user visits a website, the extension checks if that domain has proxy configuration
2. If configured, the extension routes the traffic through the specified proxy server
3. If not configured, traffic uses the direct connection (no proxy)
4. Authentication credentials are only used when required by the proxy server and are transmitted securely
### Security Considerations
- **No data collection**: The extension does not collect, store, or transmit any browsing data
- **Local-only storage**: All settings remain on the user's device
- **Transparent operation**: Users can see exactly which domains use which proxies
- **No external dependencies**: The extension works entirely offline once configured
## Installation
1. Download the extension package
2. Open Firefox and navigate to `about:addons`
3. Click the gear icon and select "Install Add-on From File"
4. Select the downloaded extension file
5. The extension will be installed and ready to use
For Firefox Android, install the signed XPI from the extension's AMO page or use
Firefox's self-distributed add-on installation flow. The manifest declares Android
compatibility through `browser_specific_settings.gecko_android`; unsigned local
packages are still rejected by Firefox's extension signing policy.
### Advanced Settings
Open **Advanced Settings** from the extension popup to manage all domains and
profiles. The **Theme** selector supports **System**, **Light**, and **Dark** and
is stored locally for use in both the options page and popup.
## Usage
### Basic Setup
1. Click the Proxy Switcher icon in the Firefox toolbar
2. Configure proxy settings for the current domain or access full settings
### Advanced Configuration
1. Right-click the extension icon and select "Options"
2. Add proxy profiles for reuse across domains
3. Configure domain-specific proxy settings
4. Use tags to organize your proxy configurations
## Permissions Explanation
### Required Permissions
- **proxy**: Allows the extension to configure Firefox's proxy settings
- **storage**: Enables saving your proxy configurations locally
- **tabs**: Lets the extension know which domain you're currently viewing
- **activeTab**: Provides access to the currently active tab for configuration
- **webRequest**: Handles authentication with proxy servers
- **webRequestBlocking**: Intercepts and routes network requests through proxies
- **<all_urls>**: Applies proxy settings to any website you visit
All permissions are used solely for proxy management and do not enable data collection or tracking.
## Support
If you encounter issues or have questions:
1. Check that all required permissions are granted
2. Verify your proxy server settings are correct
3. Ensure the proxy server is accessible from your network
## Version
Current version: 1.0.1
+238
View File
@@ -0,0 +1,238 @@
// Global configuration reference for backward compatibility
// configManager is available via window.configManager from config-manager.js
let config = window.configManager.getConfig();
// Initialization
async function init() {
await configManager.load();
config = configManager.getConfig(); // Update global reference
setupProxyListener();
// Subscribe to configuration changes
configManager.addListener((event, data) => {
config = configManager.getConfig(); // Update global reference
});
}
// Setup proxy listener
// SECURITY NOTE: The extension only routes traffic through user-configured proxies.
// No data is collected or transmitted externally. All proxy settings are user-controlled.
function setupProxyListener() {
// Listen for all URLs to apply domain-specific proxy routing
// This permission is required but only routes traffic through user-specified proxies
browser.proxy.onRequest.addListener(
handleProxyRequest,
{ urls: ["<all_urls>"] }
);
// Listener for automatic proxy authentication
// SECURITY NOTE: Credentials are only used for proxy authentication and stored locally
browser.webRequest.onAuthRequired.addListener(
handleAuthRequired,
{ urls: ["<all_urls>"] },
["blocking"]
);
// Track tabs to determine origin domain for proxy configuration
// SECURITY NOTE: Only tracks domain names, no URL content or personal data
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (changeInfo.status === 'loading' && tab.url) {
updateTabOrigin(tabId, tab.url);
}
});
browser.tabs.onRemoved.addListener((tabId) => {
delete config.activeTabs[tabId];
});
}
// Extract domain from URL
function getDomainFromUrl(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace(/^www\./, '');
} catch (e) {
return null;
}
}
// Determine origin for tab
function updateTabOrigin(tabId, url) {
const domain = getDomainFromUrl(url);
if (domain) {
config.activeTabs[tabId] = domain;
}
}
// Get proxy settings for domain
function getProxyConfigForDomain(domain) {
return config.domains[domain];
}
// Proxy request handler
function handleProxyRequest(requestInfo) {
const { tabId, url, originUrl, type } = requestInfo;
// Determine request domain
let domain;
if (originUrl) {
// For sub-requests use origin
domain = getDomainFromUrl(originUrl);
} else if (tabId in config.activeTabs) {
// For main requests use stored tab origin
domain = config.activeTabs[tabId];
} else {
// Fallback option
domain = getDomainFromUrl(url);
}
if (!domain) {
return { type: 'direct' };
}
// Check domain configuration
const proxyConfig = getProxyConfigForDomain(domain);
if (!proxyConfig || !proxyConfig.enabled) {
return { type: 'direct' };
}
// Form proxy object
const proxyObj = {
type: proxyConfig.type,
host: proxyConfig.host,
port: parseInt(proxyConfig.port, 10)
};
// Add authentication if present
if (proxyConfig.username) {
proxyObj.username = proxyConfig.username;
}
if (proxyConfig.password) {
proxyObj.password = proxyConfig.password;
}
return proxyObj;
}
// Authentication request handler
function handleAuthRequired(details) {
const { url, originUrl, tabId, isProxy, realm } = details;
// Process only proxy authentication
if (!isProxy) {
return {};
}
// Determine request domain
let domain;
if (originUrl) {
domain = getDomainFromUrl(originUrl);
} else if (tabId in config.activeTabs) {
domain = config.activeTabs[tabId];
} else {
domain = getDomainFromUrl(url);
}
if (!domain) {
return {};
}
// Get proxy configuration for domain
const proxyConfig = getProxyConfigForDomain(domain);
if (!proxyConfig || !proxyConfig.enabled || !proxyConfig.username || !proxyConfig.password) {
return {};
}
// Return credentials for authentication
return {
authCredentials: {
username: proxyConfig.username,
password: proxyConfig.password
}
};
}
// Message handler from other extension parts
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'getConfig') {
sendResponse({ config: configManager.getConfig() });
} else if (message.action === 'updateDomain') {
configManager.updateDomain(message.domain, message.domainConfig)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Async response
} else if (message.action === 'removeDomain') {
configManager.removeDomain(message.domain)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Async response
} else if (message.action === 'toggleProxy') {
// Quick enable/disable proxy for domain
const domain = message.domain;
const enabled = message.enabled;
const domainConfig = configManager.getDomainConfig(domain);
if (domainConfig) {
// If domain is already configured, just change enabled
domainConfig.enabled = enabled;
configManager.updateDomain(domain, domainConfig)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
} else {
// If domain is not configured, send error
sendResponse({ success: false, error: 'Domain not configured' });
}
return true; // Async response
} else if (message.action === 'reloadConfig') {
configManager.reload()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Async response
} else if (message.action === 'getProfiles') {
sendResponse({ profiles: configManager.getProfiles() });
} else if (message.action === 'updateProfile') {
configManager.updateProfile(message.profileName, message.profileConfig)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Async response
} else if (message.action === 'removeProfile') {
configManager.removeProfile(message.profileName)
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Async response
} else if (message.action === 'getProfile') {
sendResponse({ profile: configManager.getProfileConfig(message.profileName) });
} else if (message.action === 'updateSettings') {
configManager.updateSettings(message.settings || {})
.then(() => sendResponse({ success: true, settings: configManager.getConfig().settings }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.action === 'clearAllDomains') {
configManager.config.domains = {};
configManager.save()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.action === 'clearAllProfiles') {
configManager.config.profiles = {};
configManager.save()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
} else if (message.action === 'clearEverything') {
configManager.config.domains = {};
configManager.config.profiles = {};
configManager.save()
.then(() => sendResponse({ success: true }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true;
}
});
// Initialization on load
init();
+146
View File
@@ -0,0 +1,146 @@
// Centralized configuration manager for Firefox Proxy Switcher
// Default configuration
const DEFAULT_CONFIG = {
domains: {},
profiles: {}, // profileName -> profileConfig
settings: {
theme: 'system'
},
activeTabs: {} // tabId -> domain for tracking active tabs
};
// Centralized configuration manager
class ConfigManager {
constructor() {
this.config = { ...DEFAULT_CONFIG };
this.listeners = [];
}
// Load configuration from storage
// SECURITY NOTE: Data is stored locally only, never transmitted externally
async load() {
try {
const stored = await browser.storage.local.get('config');
this.config = {
...DEFAULT_CONFIG,
...stored.config,
settings: { ...DEFAULT_CONFIG.settings, ...(stored.config && stored.config.settings) }
};
this.notifyListeners('loaded');
return this.config;
} catch (error) {
console.error('Failed to load config:', error);
throw error;
}
}
// Save configuration to storage
// SECURITY NOTE: All proxy credentials and settings remain on user's device
async save() {
try {
await browser.storage.local.set({ config: this.config });
this.notifyListeners('saved');
return true;
} catch (error) {
console.error('Failed to save config:', error);
throw error;
}
}
// Reload configuration
async reload() {
return await this.load();
}
// Get current configuration
getConfig() {
return { ...this.config };
}
// Update general extension settings
async updateSettings(settings) {
this.config.settings = { ...this.config.settings, ...settings };
await this.save();
this.notifyListeners('settingsUpdated', this.config.settings);
}
// Update domain
async updateDomain(domain, domainConfig) {
// Ensure tags is an array of strings
if (domainConfig.tags && typeof domainConfig.tags === 'string') {
domainConfig.tags = domainConfig.tags.split(',').map(tag => tag.trim()).filter(tag => tag);
} else if (!domainConfig.tags) {
domainConfig.tags = [];
}
this.config.domains[domain] = domainConfig;
await this.save();
this.notifyListeners('domainUpdated', { domain, config: domainConfig });
}
// Remove domain
async removeDomain(domain) {
if (this.config.domains[domain]) {
delete this.config.domains[domain];
await this.save();
this.notifyListeners('domainRemoved', { domain });
}
}
// Get domain configuration
getDomainConfig(domain) {
return this.config.domains[domain] || null;
}
// Update profile
async updateProfile(profileName, profileConfig) {
this.config.profiles[profileName] = profileConfig;
await this.save();
this.notifyListeners('profileUpdated', { profileName, config: profileConfig });
}
// Remove profile
async removeProfile(profileName) {
if (this.config.profiles[profileName]) {
delete this.config.profiles[profileName];
await this.save();
this.notifyListeners('profileRemoved', { profileName });
}
}
// Get profile configuration
getProfileConfig(profileName) {
return this.config.profiles[profileName] || null;
}
// Get all profiles
getProfiles() {
return { ...this.config.profiles };
}
// Add change listener
addListener(callback) {
this.listeners.push(callback);
}
// Notify listeners
notifyListeners(event, data) {
this.listeners.forEach(callback => {
try {
callback(event, data);
} catch (error) {
console.error('Error in config listener:', error);
}
});
}
}
// Create global instance of configuration manager
const configManager = new ConfigManager();
// Create global variable for use in other scripts
window.ConfigManager = ConfigManager;
window.configManager = configManager;
window.DEFAULT_CONFIG = DEFAULT_CONFIG;
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 763 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.6 KiB

+48
View File
@@ -0,0 +1,48 @@
{
"manifest_version": 2,
"name": "Proxy Switcher Fork By WT",
"version": "1.0.1",
"description": "Selectively apply proxy servers to specific domains. All settings are stored locally and no data is collected or transmitted externally.",
"icons": {
"16": "icons/proxy-switcher-16.png",
"32": "icons/proxy-switcher-32.png",
"48": "icons/proxy-switcher-48.png",
"64": "icons/proxy-switcher-64.png",
"96": "icons/proxy-switcher-96.png",
"128": "icons/proxy-switcher-128.png",
"512": "icons/proxy-switcher-512.png"
},
"permissions": [
"proxy",
"storage",
"tabs",
"activeTab",
"webRequest",
"webRequestBlocking",
"<all_urls>"
],
"background": {
"scripts": ["config-manager.js", "background.js"],
"persistent": true
},
"browser_action": {
"default_popup": "popup.html",
"default_icon": "icons/proxy-switcher-16.png"
},
"options_ui": {
"page": "options.html",
"open_in_tab": true
},
"browser_specific_settings": {
"gecko": {
"id": "proxy-switcher-for-by-wt@9c0b2d3d",
"strict_min_version": "91.1.0",
"data_collection_permissions": {
"required": ["none"]
}
},
"gecko_android": {
"strict_min_version": "91.1.0"
}
}
}
+511
View File
@@ -0,0 +1,511 @@
:root {
--primary-color: #0066cc;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--background-color: #f8f9fa;
--card-background: #ffffff;
--border-color: #dee2e6;
--text-color: #212529;
--text-secondary: #6c757d;
--input-background: #ffffff;
--input-text: #212529;
}
:root[data-theme="dark"] {
color-scheme: dark;
--background-color: #202124;
--card-background: #2b2d31;
--border-color: #4a4d52;
--text-color: #f1f3f4;
--text-secondary: #bdc1c6;
--input-background: #303134;
--input-text: #f1f3f4;
}
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
color-scheme: dark;
--background-color: #202124;
--card-background: #2b2d31;
--border-color: #4a4d52;
--text-color: #f1f3f4;
--text-secondary: #bdc1c6;
--input-background: #303134;
--input-text: #f1f3f4;
}
}
select option {
background-color: var(--input-background);
color: var(--input-text);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 1px solid var(--border-color);
}
header h1 {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
.subtitle {
color: var(--text-secondary);
font-size: 14px;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.search-controls {
margin-bottom: 20px;
}
.search-group {
display: flex;
gap: 10px;
align-items: center;
}
.search-group input {
flex: 1;
}
.search-group select {
flex: 0 0 20%;
}
.profiles-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.domains-list {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 20px;
margin-bottom: 40px;
}
.domain-card {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
transition: box-shadow 0.2s;
}
.domain-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.profile-card {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 20px;
transition: box-shadow 0.2s;
}
.profile-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.domain-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 15px;
}
.domain-name {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
margin-bottom: 5px;
}
.domain-tags {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin-top: 5px;
}
.tag {
font-size: 11px;
padding: 2px 6px;
background-color: var(--secondary-color);
color: white;
border-radius: 10px;
font-weight: 500;
}
.domain-status {
font-size: 12px;
padding: 4px 8px;
border-radius: 12px;
font-weight: 600;
margin-left: auto;
}
.domain-status.enabled {
background-color: #d4edda;
color: #155724;
}
.domain-status.disabled {
background-color: #f8d7da;
color: #721c24;
}
.domain-details {
margin-bottom: 20px;
}
.detail {
display: flex;
margin-bottom: 8px;
font-size: 14px;
}
.detail-label {
font-weight: 600;
min-width: 80px;
color: var(--text-secondary);
}
.detail-value {
color: var(--text-color);
}
.domain-actions {
display: flex;
gap: 10px;
}
.profile-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.profile-name {
font-size: 16px;
font-weight: 600;
color: var(--primary-color);
}
.profile-details {
margin-bottom: 20px;
}
.profile-actions {
display: flex;
gap: 10px;
}
.domain-form-section {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.profile-form-section {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.advanced-settings-section {
background: var(--card-background);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 30px;
margin-bottom: 30px;
}
.settings-row {
max-width: 600px;
}
.profile-form {
max-width: 600px;
}
.domain-form {
max-width: 600px;
}
.form-group {
margin-bottom: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
}
label {
display: block;
margin-bottom: 6px;
font-weight: 600;
font-size: 14px;
}
input, select {
width: 100%;
padding: 10px 12px;
background-color: var(--input-background);
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 14px;
transition: border-color 0.2s;
}
input:focus, select:focus {
outline: none;
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
input::placeholder {
color: var(--text-secondary);
}
input:invalid {
border-color: var(--danger-color);
}
.form-hint {
display: block;
margin-top: 6px;
font-size: 12px;
color: var(--text-secondary);
}
.password-input {
position: relative;
}
.toggle-password {
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 16px;
padding: 5px;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
}
.form-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
margin-top: 30px;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #0052a3;
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.btn-danger {
background-color: var(--danger-color);
color: white;
}
.btn-danger:hover {
background-color: #c82333;
}
.empty-state {
text-align: center;
padding: 40px;
color: var(--text-secondary);
grid-column: 1 / -1;
}
.hidden {
display: none !important;
}
/* Modal Styles */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
z-index: 1000;
opacity: 0;
transition: opacity 0.3s ease;
}
.modal-overlay:not(.hidden) {
opacity: 1;
}
.domain-form-section:not(.hidden),
.profile-form-section:not(.hidden) {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.9);
z-index: 1001;
max-height: 90vh;
overflow-y: auto;
opacity: 0;
transition: all 0.3s ease;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
}
.domain-form-section:not(.hidden),
.profile-form-section:not(.hidden) {
transform: translate(-50%, -50%) scale(1);
opacity: 1;
}
body.modal-open {
overflow: hidden;
}
footer {
margin-top: 40px;
padding-top: 20px;
border-top: 1px solid var(--border-color);
text-align: center;
color: var(--text-secondary);
font-size: 12px;
}
.footer-content {
display: flex;
justify-content: space-between;
align-items: center;
}
@keyframes slideIn {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideOut {
from {
transform: translateX(0);
opacity: 1;
}
to {
transform: translateX(100%);
opacity: 0;
}
}
@media (max-width: 768px) {
.container {
padding: 15px;
}
.form-row {
grid-template-columns: 1fr;
gap: 0;
}
.domains-list {
grid-template-columns: 1fr;
}
.profiles-list {
grid-template-columns: 1fr;
}
.footer-content {
flex-direction: column;
gap: 10px;
}
}
+248
View File
@@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy Switcher - Options</title>
<link rel="stylesheet" href="options.css">
</head>
<body>
<!-- Modal Overlay -->
<div id="modal-overlay" class="modal-overlay hidden"></div>
<div class="container">
<header>
<h1><img src="icons/proxy-switcher-64.png" alt="Proxy Switcher"> Proxy Switcher</h1>
<p class="subtitle">Manage domain-specific proxy settings</p>
</header>
<main>
<section class="profiles-section">
<div class="section-header">
<h2>Proxy Profiles</h2>
<button id="add-profile" class="btn btn-primary">
<span class="icon">+</span> Add Profile
</button>
</div>
<div id="profiles-list" class="profiles-list">
<!-- Dynamically populated -->
</div>
</section>
<section class="domains-section">
<div class="section-header">
<h2>Configured Domains</h2>
<button id="add-domain" class="btn btn-primary">
<span class="icon">+</span> Add Domain
</button>
</div>
<div class="search-controls">
<div class="search-group">
<input type="text" id="domain-search" placeholder="Search domains...">
<select id="tag-filter">
<option value="">All tags</option>
<!-- Tags will be added dynamically -->
</select>
</div>
</div>
<div id="domains-list" class="domains-list">
<!-- Dynamically populated -->
</div>
</section>
<section id="domain-form-section" class="domain-form-section hidden">
<h2 id="form-title">Add Domain</h2>
<form id="domain-form" class="domain-form">
<div class="form-group">
<label for="domain-input">Domain *</label>
<input type="text" id="domain-input"
placeholder="example.com"
pattern="^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$"
required>
<small class="form-hint">Enter domain without protocol (e.g., example.com)</small>
</div>
<div class="form-group">
<label for="proxy-type">Proxy Type *</label>
<select id="proxy-type" required>
<option value="http">HTTP</option>
<option value="socks">SOCKS5</option>
</select>
</div>
<div class="form-group">
<label for="profile-select">Use Profile (optional)</label>
<select id="profile-select">
<option value="">-- Select Profile --</option>
<!-- Profiles will be added dynamically -->
</select>
<small class="form-hint">Selecting a profile will fill the fields below</small>
</div>
<div class="form-row">
<div class="form-group">
<label for="proxy-host">Host *</label>
<input type="text" id="proxy-host"
placeholder="proxy.example.com"
required>
</div>
<div class="form-group">
<label for="proxy-port">Port *</label>
<input type="number" id="proxy-port"
min="1" max="65535"
placeholder="8080"
required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="proxy-username">Username (optional)</label>
<input type="text" id="proxy-username"
placeholder="username">
</div>
<div class="form-group">
<label for="proxy-password">Password (optional)</label>
<div class="password-input">
<input type="password" id="proxy-password"
placeholder="••••••••">
<button type="button" class="toggle-password"
data-target="proxy-password">
👁
</button>
</div>
</div>
</div>
<div class="form-group">
<label for="domain-tags">Tags (optional)</label>
<input type="text" id="domain-tags"
placeholder="work, personal, blocked">
<small class="form-hint">Comma-separated tags for organization</small>
</div>
<div class="form-group checkbox-group">
<label class="checkbox-label">
<input type="checkbox" id="proxy-enabled" checked>
<span>Enable proxy for this domain</span>
</label>
</div>
<div class="form-actions">
<button type="button" id="cancel-form" class="btn btn-secondary">
Cancel
</button>
<button type="submit" id="save-domain" class="btn btn-primary">
Save Domain
</button>
</div>
</form>
</section>
<section id="profile-form-section" class="profile-form-section hidden">
<h2 id="profile-form-title">Add Profile</h2>
<form id="profile-form" class="profile-form">
<div class="form-group">
<label for="profile-name-input">Profile Name *</label>
<input type="text" id="profile-name-input"
placeholder="My Proxy Profile"
required>
<small class="form-hint">Enter a unique name for this profile</small>
</div>
<div class="form-group">
<label for="profile-proxy-type">Proxy Type *</label>
<select id="profile-proxy-type" required>
<option value="http">HTTP</option>
<option value="socks">SOCKS5</option>
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="profile-proxy-host">Host *</label>
<input type="text" id="profile-proxy-host"
placeholder="proxy.example.com"
required>
</div>
<div class="form-group">
<label for="profile-proxy-port">Port *</label>
<input type="number" id="profile-proxy-port"
min="1" max="65535"
placeholder="8080"
required>
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="profile-proxy-username">Username (optional)</label>
<input type="text" id="profile-proxy-username"
placeholder="username">
</div>
<div class="form-group">
<label for="profile-proxy-password">Password (optional)</label>
<div class="password-input">
<input type="password" id="profile-proxy-password"
placeholder="••••••••">
<button type="button" class="toggle-password"
data-target="profile-proxy-password">
👁
</button>
</div>
</div>
</div>
<div class="form-actions">
<button type="button" id="cancel-profile-form" class="btn btn-secondary">
Cancel
</button>
<button type="submit" id="save-profile" class="btn btn-primary">
Save Profile
</button>
</div>
</form>
</section>
<section class="advanced-settings-section">
<h2>Advanced Settings</h2>
<div class="form-group settings-row">
<label for="theme-select">Theme</label>
<select id="theme-select">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
<small class="form-hint">Choose light, dark, or follow your device theme.</small>
</div>
</section>
<section class="management-section">
<h2>Management</h2>
<div class="management-buttons">
<button id="clear-domains" class="btn btn-danger">
Clear All Domains
</button>
<button id="clear-profiles" class="btn btn-danger">
Clear All Profiles
</button>
<button id="clear-all" class="btn btn-danger">
Clear Everything
</button>
</div>
</section>
</main>
<footer>
<div class="footer-content">
<p>Proxy Switcher v1.0.1</p>
</div>
</footer>
</div>
<script src="options.js"></script>
</body>
</html>
+940
View File
@@ -0,0 +1,940 @@
class OptionsManager {
constructor() {
this.currentEditDomain = null;
this.currentEditProfile = null;
this.searchQuery = '';
this.tagFilter = '';
this.allDomains = {}; // For storing all domains for filtering
this.init();
}
// Sanitizer for user data
sanitize(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
async init() {
await this.loadDomains();
await this.loadProfiles();
await this.loadSettings();
this.setupEventListeners();
}
async loadSettings() {
try {
const response = await browser.runtime.sendMessage({ action: 'getConfig' });
const theme = response.config && response.config.settings && response.config.settings.theme || 'system';
document.getElementById('theme-select').value = theme;
this.applyTheme(theme);
} catch (error) {
console.error('Failed to load settings:', error);
this.applyTheme('system');
}
}
applyTheme(theme) {
document.documentElement.dataset.theme = ['light', 'dark', 'system'].includes(theme) ? theme : 'system';
}
async saveTheme(theme) {
const response = await browser.runtime.sendMessage({ action: 'updateSettings', settings: { theme } });
if (!response.success) throw new Error(response.error || 'Failed to save theme');
this.applyTheme(theme);
}
async loadDomains() {
try {
const response = await browser.runtime.sendMessage({ action: 'getConfig' });
const config = response.config || { domains: {} };
this.allDomains = config.domains;
this.updateTagFilter();
this.renderDomainsList(this.filterDomains());
} catch (error) {
console.error('Failed to load domains:', error);
this.showError('Failed to load configuration');
}
}
renderDomainsList(domains) {
const container = document.getElementById('domains-list');
container.innerHTML = ''; // Clear the container
if (Object.keys(domains).length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<p>No domains configured yet.</p>
<p>Click "Add Domain" to get started.</p>
`;
container.appendChild(emptyState);
return;
}
const domainsList = Object.entries(domains)
.sort(([a], [b]) => a.localeCompare(b))
.map(([domain, config]) => this.createDomainCard(domain, config));
domainsList.forEach(card => {
container.appendChild(card);
});
// Add event handlers for buttons
document.querySelectorAll('.edit-domain').forEach(btn => {
btn.addEventListener('click', (e) => {
const domain = e.target.dataset.domain;
this.editDomain(domain);
});
});
document.querySelectorAll('.delete-domain').forEach(btn => {
btn.addEventListener('click', (e) => {
const domain = e.target.dataset.domain;
this.deleteDomain(domain);
});
});
}
createDomainCard(domain, config) {
const typeMap = {
http: 'HTTP',
socks: 'SOCKS5'
};
const statusClass = config.enabled ? 'enabled' : 'disabled';
const statusText = config.enabled ? 'Enabled' : 'Disabled';
const card = document.createElement('div');
card.className = 'domain-card';
// Header
const header = document.createElement('div');
header.className = 'domain-header';
const headerContent = document.createElement('div');
const domainName = document.createElement('h3');
domainName.className = 'domain-name';
domainName.textContent = this.sanitize(domain);
headerContent.appendChild(domainName);
if (config.tags && config.tags.length) {
const tagsContainer = document.createElement('div');
tagsContainer.className = 'domain-tags';
config.tags.forEach(tag => {
const tagSpan = document.createElement('span');
tagSpan.className = 'tag';
tagSpan.textContent = this.sanitize(tag);
tagsContainer.appendChild(tagSpan);
});
headerContent.appendChild(tagsContainer);
}
header.appendChild(headerContent);
const status = document.createElement('span');
status.className = `domain-status ${statusClass}`;
status.textContent = statusText;
header.appendChild(status);
card.appendChild(header);
// Details
const details = document.createElement('div');
details.className = 'domain-details';
const typeDetail = document.createElement('div');
typeDetail.className = 'detail';
const typeLabel = document.createElement('span');
typeLabel.className = 'detail-label';
typeLabel.textContent = 'Type:';
const typeValue = document.createElement('span');
typeValue.className = 'detail-value';
typeValue.textContent = typeMap[config.type] || config.type;
typeDetail.appendChild(typeLabel);
typeDetail.appendChild(typeValue);
details.appendChild(typeDetail);
const serverDetail = document.createElement('div');
serverDetail.className = 'detail';
const serverLabel = document.createElement('span');
serverLabel.className = 'detail-label';
serverLabel.textContent = 'Server:';
const serverValue = document.createElement('span');
serverValue.className = 'detail-value';
serverValue.textContent = `${this.sanitize(config.host)}:${config.port}`;
serverDetail.appendChild(serverLabel);
serverDetail.appendChild(serverValue);
details.appendChild(serverDetail);
if (config.username) {
const usernameDetail = document.createElement('div');
usernameDetail.className = 'detail';
const usernameLabel = document.createElement('span');
usernameLabel.className = 'detail-label';
usernameLabel.textContent = 'Username:';
const usernameValue = document.createElement('span');
usernameValue.className = 'detail-value';
usernameValue.textContent = this.sanitize(config.username);
usernameDetail.appendChild(usernameLabel);
usernameDetail.appendChild(usernameValue);
details.appendChild(usernameDetail);
}
card.appendChild(details);
// Actions
const actions = document.createElement('div');
actions.className = 'domain-actions';
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-secondary edit-domain';
editBtn.dataset.domain = domain;
editBtn.textContent = 'Edit';
actions.appendChild(editBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-danger delete-domain';
deleteBtn.dataset.domain = domain;
deleteBtn.textContent = 'Delete';
actions.appendChild(deleteBtn);
card.appendChild(actions);
return card;
}
showDomainForm(domain = null, config = null) {
this.currentEditDomain = domain;
const formSection = document.getElementById('domain-form-section');
const formTitle = document.getElementById('form-title');
const form = document.getElementById('domain-form');
const overlay = document.getElementById('modal-overlay');
if (domain) {
formTitle.textContent = `Edit ${domain}`;
this.populateForm(config);
} else {
formTitle.textContent = 'Add Domain';
form.reset();
}
// Show modal window
overlay.classList.remove('hidden');
formSection.classList.remove('hidden');
document.body.classList.add('modal-open');
document.getElementById('domain-input').focus();
}
hideDomainForm() {
const formSection = document.getElementById('domain-form-section');
const overlay = document.getElementById('modal-overlay');
// Hide modal window
overlay.classList.add('hidden');
formSection.classList.add('hidden');
document.body.classList.remove('modal-open');
this.currentEditDomain = null;
const form = document.getElementById('domain-form');
form.reset();
}
populateForm(config) {
document.getElementById('domain-input').value = this.currentEditDomain;
document.getElementById('proxy-type').value = config.type || 'http';
document.getElementById('proxy-host').value = config.host || '';
document.getElementById('proxy-port').value = config.port || '';
document.getElementById('proxy-username').value = config.username || '';
document.getElementById('proxy-password').value = config.password || '';
document.getElementById('domain-tags').value = config.tags ? config.tags.join(', ') : '';
document.getElementById('proxy-enabled').checked = config.enabled !== false;
}
async saveDomain(event) {
event.preventDefault();
const form = event.target;
// Validation
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const domain = document.getElementById('domain-input').value.trim().toLowerCase();
const tagsInput = document.getElementById('domain-tags').value.trim();
const domainConfig = {
type: document.getElementById('proxy-type').value,
host: document.getElementById('proxy-host').value.trim(),
port: parseInt(document.getElementById('proxy-port').value, 10),
username: document.getElementById('proxy-username').value.trim() || null,
password: document.getElementById('proxy-password').value || null,
enabled: document.getElementById('proxy-enabled').checked,
tags: tagsInput ? tagsInput.split(',').map(tag => tag.trim()).filter(tag => tag) : []
};
// Additional validation
if (domainConfig.port < 1 || domainConfig.port > 65535) {
this.showError('Port must be between 1 and 65535');
return;
}
try {
// If domain name has changed, remove the old domain
if (this.currentEditDomain && this.currentEditDomain !== domain) {
const removeResponse = await browser.runtime.sendMessage({
action: 'removeDomain',
domain: this.currentEditDomain
});
if (!removeResponse.success) {
throw new Error(removeResponse.error || 'Failed to remove old domain');
}
}
// Use centralized API to update domain
const response = await browser.runtime.sendMessage({
action: 'updateDomain',
domain: domain,
domainConfig: domainConfig
});
if (response.success) {
this.showSuccess(`Domain ${domain} saved successfully`);
this.hideDomainForm();
await this.loadDomains();
} else {
throw new Error(response.error || 'Failed to save domain');
}
} catch (error) {
console.error('Failed to save domain:', error);
this.showError('Failed to save domain configuration');
}
}
async deleteDomain(domain) {
if (!confirm(`Are you sure you want to remove proxy settings for ${domain}?`)) {
return;
}
try {
// Use centralized API to remove domain
const response = await browser.runtime.sendMessage({
action: 'removeDomain',
domain: domain
});
if (response.success) {
this.showSuccess(`Domain ${domain} removed`);
await this.loadDomains();
} else {
throw new Error(response.error || 'Failed to delete domain');
}
} catch (error) {
console.error('Failed to delete domain:', error);
this.showError('Failed to delete domain');
}
}
async editDomain(domain) {
try {
const response = await browser.runtime.sendMessage({ action: 'getConfig' });
const config = response.config || { domains: {} };
if (config.domains[domain]) {
this.showDomainForm(domain, config.domains[domain]);
}
} catch (error) {
console.error('Failed to load domain:', error);
this.showError('Failed to load domain configuration');
}
}
// Methods for profiles
async loadProfiles() {
try {
const response = await browser.runtime.sendMessage({ action: 'getProfiles' });
const profiles = response.profiles || {};
this.renderProfilesList(profiles);
this.updateProfileSelect(profiles);
} catch (error) {
console.error('Failed to load profiles:', error);
this.showError('Failed to load profiles');
}
}
renderProfilesList(profiles) {
const container = document.getElementById('profiles-list');
container.innerHTML = ''; // Clear the container
if (Object.keys(profiles).length === 0) {
const emptyState = document.createElement('div');
emptyState.className = 'empty-state';
emptyState.innerHTML = `
<p>No profiles configured yet.</p>
<p>Click "Add Profile" to create your first profile.</p>
`;
container.appendChild(emptyState);
return;
}
const profilesList = Object.entries(profiles)
.sort(([a], [b]) => a.localeCompare(b))
.map(([profileName, config]) => this.createProfileCard(profileName, config));
profilesList.forEach(card => {
container.appendChild(card);
});
// Add event handlers for buttons
document.querySelectorAll('.edit-profile').forEach(btn => {
btn.addEventListener('click', (e) => {
const profileName = e.target.dataset.profile;
this.editProfile(profileName);
});
});
document.querySelectorAll('.delete-profile').forEach(btn => {
btn.addEventListener('click', (e) => {
const profileName = e.target.dataset.profile;
this.deleteProfile(profileName);
});
});
}
createProfileCard(profileName, config) {
const typeMap = {
http: 'HTTP',
socks: 'SOCKS5'
};
const card = document.createElement('div');
card.className = 'profile-card';
// Header
const header = document.createElement('div');
header.className = 'profile-header';
const profileNameEl = document.createElement('h3');
profileNameEl.className = 'profile-name';
profileNameEl.textContent = this.sanitize(profileName);
header.appendChild(profileNameEl);
card.appendChild(header);
// Details
const details = document.createElement('div');
details.className = 'profile-details';
const typeDetail = document.createElement('div');
typeDetail.className = 'detail';
const typeLabel = document.createElement('span');
typeLabel.className = 'detail-label';
typeLabel.textContent = 'Type:';
const typeValue = document.createElement('span');
typeValue.className = 'detail-value';
typeValue.textContent = typeMap[config.type] || config.type;
typeDetail.appendChild(typeLabel);
typeDetail.appendChild(typeValue);
details.appendChild(typeDetail);
const serverDetail = document.createElement('div');
serverDetail.className = 'detail';
const serverLabel = document.createElement('span');
serverLabel.className = 'detail-label';
serverLabel.textContent = 'Server:';
const serverValue = document.createElement('span');
serverValue.className = 'detail-value';
serverValue.textContent = `${this.sanitize(config.host)}:${config.port}`;
serverDetail.appendChild(serverLabel);
serverDetail.appendChild(serverValue);
details.appendChild(serverDetail);
if (config.username) {
const usernameDetail = document.createElement('div');
usernameDetail.className = 'detail';
const usernameLabel = document.createElement('span');
usernameLabel.className = 'detail-label';
usernameLabel.textContent = 'Username:';
const usernameValue = document.createElement('span');
usernameValue.className = 'detail-value';
usernameValue.textContent = this.sanitize(config.username);
usernameDetail.appendChild(usernameLabel);
usernameDetail.appendChild(usernameValue);
details.appendChild(usernameDetail);
}
card.appendChild(details);
// Actions
const actions = document.createElement('div');
actions.className = 'profile-actions';
const editBtn = document.createElement('button');
editBtn.className = 'btn btn-secondary edit-profile';
editBtn.dataset.profile = profileName;
editBtn.textContent = 'Edit';
actions.appendChild(editBtn);
const deleteBtn = document.createElement('button');
deleteBtn.className = 'btn btn-danger delete-profile';
deleteBtn.dataset.profile = profileName;
deleteBtn.textContent = 'Delete';
actions.appendChild(deleteBtn);
card.appendChild(actions);
return card;
}
updateProfileSelect(profiles) {
const select = document.getElementById('profile-select');
const currentValue = select.value;
// Clear the select, keeping only the default option
select.innerHTML = '<option value="">-- Select Profile --</option>';
// Add profiles
Object.keys(profiles).sort().forEach(profileName => {
const option = document.createElement('option');
option.value = profileName;
option.textContent = profileName;
select.appendChild(option);
});
// Restore selected value if it still exists
if (currentValue && profiles[currentValue]) {
select.value = currentValue;
}
}
showProfileForm(profileName = null, config = null) {
this.currentEditProfile = profileName;
const formSection = document.getElementById('profile-form-section');
const formTitle = document.getElementById('profile-form-title');
const form = document.getElementById('profile-form');
const overlay = document.getElementById('modal-overlay');
if (profileName) {
formTitle.textContent = `Edit ${profileName}`;
this.populateProfileForm(config);
} else {
formTitle.textContent = 'Add Profile';
form.reset();
}
// Show modal window
overlay.classList.remove('hidden');
formSection.classList.remove('hidden');
document.body.classList.add('modal-open');
document.getElementById('profile-name-input').focus();
}
hideProfileForm() {
const formSection = document.getElementById('profile-form-section');
const overlay = document.getElementById('modal-overlay');
// Hide modal window
overlay.classList.add('hidden');
formSection.classList.add('hidden');
document.body.classList.remove('modal-open');
this.currentEditProfile = null;
const form = document.getElementById('profile-form');
form.reset();
}
populateProfileForm(config) {
document.getElementById('profile-name-input').value = this.currentEditProfile;
document.getElementById('profile-proxy-type').value = config.type || 'http';
document.getElementById('profile-proxy-host').value = config.host || '';
document.getElementById('profile-proxy-port').value = config.port || '';
document.getElementById('profile-proxy-username').value = config.username || '';
document.getElementById('profile-proxy-password').value = config.password || '';
}
async saveProfile(event) {
event.preventDefault();
const form = event.target;
// Validation
if (!form.checkValidity()) {
form.reportValidity();
return;
}
const profileName = document.getElementById('profile-name-input').value.trim();
const profileConfig = {
type: document.getElementById('profile-proxy-type').value,
host: document.getElementById('profile-proxy-host').value.trim(),
port: parseInt(document.getElementById('profile-proxy-port').value, 10),
username: document.getElementById('profile-proxy-username').value.trim() || null,
password: document.getElementById('profile-proxy-password').value || null
};
// Additional validation
if (profileConfig.port < 1 || profileConfig.port > 65535) {
this.showError('Port must be between 1 and 65535');
return;
}
try {
// If profile name has changed, remove the old profile
if (this.currentEditProfile && this.currentEditProfile !== profileName) {
const removeResponse = await browser.runtime.sendMessage({
action: 'removeProfile',
profileName: this.currentEditProfile
});
if (!removeResponse.success) {
throw new Error(removeResponse.error || 'Failed to remove old profile');
}
}
const response = await browser.runtime.sendMessage({
action: 'updateProfile',
profileName: profileName,
profileConfig: profileConfig
});
if (response.success) {
this.showSuccess(`Profile "${profileName}" saved successfully`);
this.hideProfileForm();
await this.loadProfiles();
} else {
throw new Error(response.error || 'Failed to save profile');
}
} catch (error) {
console.error('Failed to save profile:', error);
this.showError('Failed to save profile');
}
}
async deleteProfile(profileName) {
if (!confirm(`Are you sure you want to delete profile "${profileName}"?`)) {
return;
}
try {
const response = await browser.runtime.sendMessage({
action: 'removeProfile',
profileName: profileName
});
if (response.success) {
this.showSuccess(`Profile "${profileName}" deleted`);
await this.loadProfiles();
} else {
throw new Error(response.error || 'Failed to delete profile');
}
} catch (error) {
console.error('Failed to delete profile:', error);
this.showError('Failed to delete profile');
}
}
async editProfile(profileName) {
try {
const response = await browser.runtime.sendMessage({ action: 'getProfile', profileName });
const profile = response.profile;
if (profile) {
this.showProfileForm(profileName, profile);
}
} catch (error) {
console.error('Failed to load profile:', error);
this.showError('Failed to load profile');
}
}
// Fill domain form with profile data
fillFormFromProfile(profileName) {
if (!profileName) {
// If empty profile is selected, do nothing
return;
}
// Get profile data
browser.runtime.sendMessage({ action: 'getProfile', profileName })
.then(response => {
const profile = response.profile;
if (profile) {
// Fill domain form fields
document.getElementById('proxy-type').value = profile.type || 'http';
document.getElementById('proxy-host').value = profile.host || '';
document.getElementById('proxy-port').value = profile.port || '';
document.getElementById('proxy-username').value = profile.username || '';
document.getElementById('proxy-password').value = profile.password || '';
}
})
.catch(error => {
console.error('Failed to load profile for form:', error);
});
}
// Filter domains by search and tags
filterDomains() {
let filtered = { ...this.allDomains };
// Search filter
if (this.searchQuery) {
const query = this.searchQuery.toLowerCase();
filtered = Object.fromEntries(
Object.entries(filtered).filter(([domain]) =>
domain.toLowerCase().includes(query)
)
);
}
// Tag filter
if (this.tagFilter) {
filtered = Object.fromEntries(
Object.entries(filtered).filter(([, config]) =>
config.tags && config.tags.includes(this.tagFilter)
)
);
}
return filtered;
}
// Update tag filter
updateTagFilter() {
const tagFilter = document.getElementById('tag-filter');
const currentValue = tagFilter.value;
// Collect all unique tags
const allTags = new Set();
Object.values(this.allDomains).forEach(config => {
if (config.tags) {
config.tags.forEach(tag => allTags.add(tag));
}
});
// Clear and add options
tagFilter.innerHTML = '<option value="">All tags</option>';
Array.from(allTags).sort().forEach(tag => {
const option = document.createElement('option');
option.value = tag;
option.textContent = tag;
tagFilter.appendChild(option);
});
// Restore selected value
if (currentValue && allTags.has(currentValue)) {
tagFilter.value = currentValue;
}
}
// Update domain display when filters change
updateDomainDisplay() {
this.renderDomainsList(this.filterDomains());
}
// Methods for bulk deletion
async clearAllDomains() {
if (!confirm('Are you sure you want to delete ALL domain configurations? This action cannot be undone.')) {
return;
}
try {
const response = await browser.runtime.sendMessage({ action: 'clearAllDomains' });
if (response.success) {
this.showSuccess('All domains cleared');
await this.loadDomains();
} else {
throw new Error(response.error || 'Failed to clear domains');
}
} catch (error) {
console.error('Failed to clear domains:', error);
this.showError('Failed to clear domains');
}
}
async clearAllProfiles() {
if (!confirm('Are you sure you want to delete ALL profile configurations? This action cannot be undone.')) {
return;
}
try {
const response = await browser.runtime.sendMessage({ action: 'clearAllProfiles' });
if (response.success) {
this.showSuccess('All profiles cleared');
await this.loadProfiles();
} else {
throw new Error(response.error || 'Failed to clear profiles');
}
} catch (error) {
console.error('Failed to clear profiles:', error);
this.showError('Failed to clear profiles');
}
}
async clearEverything() {
if (!confirm('Are you sure you want to delete ALL configurations (domains and profiles)? This action cannot be undone.')) {
return;
}
try {
const response = await browser.runtime.sendMessage({ action: 'clearEverything' });
if (response.success) {
this.showSuccess('All configurations cleared');
await this.loadDomains();
await this.loadProfiles();
} else {
throw new Error(response.error || 'Failed to clear everything');
}
} catch (error) {
console.error('Failed to clear everything:', error);
this.showError('Failed to clear everything');
}
}
showMessage(message, type = 'info') {
const messageDiv = document.createElement('div');
messageDiv.className = `message message-${type}`;
messageDiv.textContent = message;
messageDiv.style.cssText = `
position: fixed;
top: 20px;
right: 20px;
padding: 12px 20px;
border-radius: 4px;
color: white;
z-index: 1000;
animation: slideIn 0.3s ease;
`;
if (type === 'success') {
messageDiv.style.backgroundColor = '#28a745';
} else if (type === 'error') {
messageDiv.style.backgroundColor = '#dc3545';
} else {
messageDiv.style.backgroundColor = '#17a2b8';
}
document.body.appendChild(messageDiv);
setTimeout(() => {
messageDiv.style.animation = 'slideOut 0.3s ease';
setTimeout(() => messageDiv.remove(), 300);
}, 3000);
}
showSuccess(message) {
this.showMessage(message, 'success');
}
showError(message) {
this.showMessage(message, 'error');
}
setupEventListeners() {
// Modal overlay - close on click
document.getElementById('modal-overlay').addEventListener('click', () => {
// Determine which form is open
const domainForm = document.getElementById('domain-form-section');
const profileForm = document.getElementById('profile-form-section');
if (!domainForm.classList.contains('hidden')) {
this.hideDomainForm();
} else if (!profileForm.classList.contains('hidden')) {
this.hideProfileForm();
}
});
// Add domain
document.getElementById('add-domain').addEventListener('click', () => {
this.showDomainForm();
});
// Domain form
document.getElementById('domain-form').addEventListener('submit',
(e) => this.saveDomain(e));
// Cancel editing
document.getElementById('cancel-form').addEventListener('click', () => {
this.hideDomainForm();
});
// Show/hide password
document.querySelectorAll('.toggle-password').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetId = e.target.dataset.target;
const input = document.getElementById(targetId);
const type = input.type === 'password' ? 'text' : 'password';
input.type = type;
e.target.textContent = type === 'password' ? '👁' : '👁️‍🗨️';
});
});
// Profiles
document.getElementById('add-profile').addEventListener('click', () => {
this.showProfileForm();
});
document.getElementById('profile-form').addEventListener('submit',
(e) => this.saveProfile(e));
document.getElementById('cancel-profile-form').addEventListener('click', () => {
this.hideProfileForm();
});
// Fill domain form from profile
document.getElementById('profile-select').addEventListener('change', (e) => {
this.fillFormFromProfile(e.target.value);
});
// Domain search
document.getElementById('domain-search').addEventListener('input', (e) => {
this.searchQuery = e.target.value.trim();
this.updateDomainDisplay();
});
// Tag filter
document.getElementById('tag-filter').addEventListener('change', (e) => {
this.tagFilter = e.target.value;
this.updateDomainDisplay();
});
// Control buttons
document.getElementById('clear-domains').addEventListener('click', () => {
this.clearAllDomains();
});
document.getElementById('clear-profiles').addEventListener('click', () => {
this.clearAllProfiles();
});
document.getElementById('clear-all').addEventListener('click', () => {
this.clearEverything();
});
document.getElementById('theme-select').addEventListener('change', async (e) => {
try {
await this.saveTheme(e.target.value);
this.showSuccess('Theme saved');
} catch (error) {
console.error('Failed to save theme:', error);
this.showError('Failed to save theme');
}
});
}
}
// Initialize on load
document.addEventListener('DOMContentLoaded', () => {
new OptionsManager();
});
+245
View File
@@ -0,0 +1,245 @@
:root {
--primary-color: #0066cc;
--secondary-color: #6c757d;
--background-color: #ffffff;
--border-color: #dee2e6;
--text-color: #212529;
--text-secondary: #6c757d;
--muted-background: #f8f9fa;
--input-background: #ffffff;
--input-text: #212529;
}
:root[data-theme="dark"] {
color-scheme: dark;
--background-color: #202124;
--border-color: #4a4d52;
--text-color: #f1f3f4;
--text-secondary: #bdc1c6;
--muted-background: #35363a;
--input-background: #303134;
--input-text: #f1f3f4;
}
@media (prefers-color-scheme: dark) {
:root[data-theme="system"] {
color-scheme: dark;
--background-color: #202124;
--border-color: #4a4d52;
--text-color: #f1f3f4;
--text-secondary: #bdc1c6;
--muted-background: #35363a;
--input-background: #303134;
--input-text: #f1f3f4;
}
}
select option {
background-color: var(--input-background);
color: var(--input-text);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
font-size: 14px;
color: var(--text-color);
min-width: 320px;
}
.popup-container {
padding: 16px;
background-color: var(--background-color);
}
.popup-header {
margin-bottom: 20px;
padding-bottom: 16px;
border-bottom: 1px solid var(--border-color);
}
.popup-header h2 {
font-size: 18px;
margin-bottom: 8px;
color: var(--primary-color);
}
.domain-display {
font-size: 12px;
color: var(--text-secondary);
font-family: monospace;
background-color: var(--muted-background);
padding: 4px 8px;
border-radius: 4px;
display: inline-block;
}
.toggle-section {
margin-bottom: 20px;
}
.toggle-label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.toggle-slider {
position: relative;
width: 40px;
height: 20px;
background-color: #ccc;
border-radius: 20px;
transition: background-color 0.3s;
}
.toggle-slider:after {
content: '';
position: absolute;
width: 16px;
height: 16px;
border-radius: 50%;
background-color: white;
top: 2px;
left: 2px;
transition: transform 0.3s;
}
input[type="checkbox"] {
display: none;
}
input[type="checkbox"]:checked + .toggle-slider {
background-color: var(--primary-color);
}
input[type="checkbox"]:checked + .toggle-slider:after {
transform: translateX(20px);
}
.toggle-text {
font-weight: 600;
}
.proxy-form {
margin-top: 20px;
}
.form-group {
margin-bottom: 16px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
}
label {
display: block;
margin-bottom: 4px;
font-weight: 600;
font-size: 12px;
}
input, select {
width: 100%;
padding: 8px 10px;
background-color: var(--input-background);
color: var(--input-text);
border: 1px solid var(--border-color);
border-radius: 4px;
font-size: 13px;
}
input:focus, select:focus {
outline: none;
border-color: var(--primary-color);
}
input::placeholder {
color: var(--text-secondary);
}
.password-input {
position: relative;
}
.toggle-password {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
background: none;
border: none;
cursor: pointer;
font-size: 14px;
padding: 2px;
}
.form-actions {
display: flex;
gap: 10px;
margin-top: 24px;
}
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
flex: 1;
}
.btn-primary {
background-color: var(--primary-color);
color: white;
}
.btn-primary:hover {
background-color: #0052a3;
}
.btn-secondary {
background-color: var(--secondary-color);
color: white;
}
.btn-secondary:hover {
background-color: #5a6268;
}
.info-message {
text-align: center;
padding: 20px;
color: var(--text-secondary);
font-size: 12px;
}
.info-message p {
margin-bottom: 8px;
}
.popup-footer {
margin-top: 20px;
padding-top: 16px;
border-top: 1px solid var(--border-color);
text-align: center;
}
.hint {
font-size: 11px;
color: var(--text-secondary);
}
.hidden {
display: none !important;
}
+97
View File
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Proxy Switcher</title>
<link rel="stylesheet" href="popup.css">
</head>
<body>
<div class="popup-container">
<header class="popup-header">
<h2>Proxy Settings</h2>
<p id="current-domain" class="domain-display">Loading...</p>
</header>
<main class="popup-main">
<div class="toggle-section">
<label class="toggle-label">
<input type="checkbox" id="enable-proxy">
<span class="toggle-slider"></span>
<span class="toggle-text">Use proxy for this domain</span>
</label>
</div>
<div id="proxy-form" class="proxy-form hidden">
<div class="form-group">
<label for="popup-proxy-type">Proxy Type</label>
<select id="popup-proxy-type">
<option value="http">HTTP</option>
<option value="socks">SOCKS5</option>
</select>
</div>
<div class="form-group">
<label for="popup-profile-select">Use Profile (optional)</label>
<select id="popup-profile-select">
<option value="">-- Select Profile --</option>
<!-- Profiles will be added dynamically -->
</select>
</div>
<div class="form-row">
<div class="form-group">
<label for="popup-proxy-host">Host</label>
<input type="text" id="popup-proxy-host"
placeholder="proxy.example.com">
</div>
<div class="form-group">
<label for="popup-proxy-port">Port</label>
<input type="number" id="popup-proxy-port"
min="1" max="65535"
placeholder="8080">
</div>
</div>
<div class="form-row">
<div class="form-group">
<label for="popup-proxy-username">Username</label>
<input type="text" id="popup-proxy-username"
placeholder="username">
</div>
<div class="form-group">
<label for="popup-proxy-password">Password</label>
<div class="password-input">
<input type="password" id="popup-proxy-password"
placeholder="••••••••">
<button type="button" class="toggle-password">
👁
</button>
</div>
</div>
</div>
<div class="form-actions">
<button id="apply-proxy" class="btn btn-primary">
Apply & Reload
</button>
<button id="open-options" class="btn btn-secondary">
Advanced Settings
</button>
</div>
</div>
<div id="no-proxy-info" class="info-message">
<p>No proxy configured for this domain.</p>
<p>Enable proxy to configure settings.</p>
</div>
</main>
<footer class="popup-footer">
<p class="hint">Changes take effect after reload</p>
</footer>
</div>
<script src="popup.js"></script>
</body>
</html>
+308
View File
@@ -0,0 +1,308 @@
class PopupManager {
constructor() {
this.currentTab = null;
this.currentDomain = null;
this.currentConfig = null;
this.init();
}
async init() {
await this.loadCurrentTab();
await this.loadConfig();
await this.loadProfiles();
this.setupUI();
this.setupEventListeners();
}
async loadCurrentTab() {
try {
const tabs = await browser.tabs.query({ active: true, currentWindow: true });
this.currentTab = tabs[0];
if (this.currentTab.url) {
this.currentDomain = this.extractDomain(this.currentTab.url);
document.getElementById('current-domain').textContent = this.currentDomain;
}
} catch (error) {
console.error('Failed to load current tab:', error);
document.getElementById('current-domain').textContent = 'Unknown domain';
}
}
extractDomain(url) {
try {
const urlObj = new URL(url);
return urlObj.hostname.replace(/^www\./, '');
} catch (e) {
return null;
}
}
async loadConfig() {
try {
const response = await browser.runtime.sendMessage({ action: 'getConfig' });
const config = response.config || { domains: {} };
this.applyTheme(config.settings && config.settings.theme);
this.currentConfig = this.currentDomain && config.domains
? config.domains[this.currentDomain] || null
: null;
} catch (error) {
console.error('Failed to load config:', error);
this.currentConfig = null;
}
}
applyTheme(theme) {
document.documentElement.dataset.theme = ['light', 'dark', 'system'].includes(theme) ? theme : 'system';
}
async loadProfiles() {
try {
const response = await browser.runtime.sendMessage({ action: 'getProfiles' });
const profiles = response.profiles || {};
this.updateProfileSelect(profiles);
} catch (error) {
console.error('Failed to load profiles:', error);
}
}
updateProfileSelect(profiles) {
const select = document.getElementById('popup-profile-select');
// Clear select, keeping only default option
select.innerHTML = '<option value="">-- Select Profile --</option>';
// Add profiles
Object.keys(profiles).sort().forEach(profileName => {
const option = document.createElement('option');
option.value = profileName;
option.textContent = profileName;
select.appendChild(option);
});
}
// Fill form with profile data
fillFormFromProfile(profileName) {
if (!profileName) {
// If empty profile selected, do nothing
return;
}
// Get profile data
browser.runtime.sendMessage({ action: 'getProfile', profileName })
.then(response => {
const profile = response.profile;
if (profile) {
// Fill form fields
document.getElementById('popup-proxy-type').value = profile.type || 'http';
document.getElementById('popup-proxy-host').value = profile.host || '';
document.getElementById('popup-proxy-port').value = profile.port || '';
document.getElementById('popup-proxy-username').value = profile.username || '';
document.getElementById('popup-proxy-password').value = profile.password || '';
}
})
.catch(error => {
console.error('Failed to load profile for form:', error);
});
}
setupUI() {
const enableCheckbox = document.getElementById('enable-proxy');
const proxyForm = document.getElementById('proxy-form');
const noProxyInfo = document.getElementById('no-proxy-info');
if (this.currentConfig) {
// Proxy configured for this domain
enableCheckbox.checked = Boolean(this.currentConfig.enabled);
enableCheckbox.disabled = false;
// Show/hide form based on checkbox state
this.updateFormVisibility(enableCheckbox.checked);
noProxyInfo.classList.add('hidden');
// Populate form
this.populateForm();
} else {
// Proxy not configured
enableCheckbox.checked = false;
enableCheckbox.disabled = !this.currentDomain;
proxyForm.classList.add('hidden');
noProxyInfo.classList.remove('hidden');
}
}
updateFormVisibility(showForm) {
const proxyForm = document.getElementById('proxy-form');
proxyForm.classList.toggle('hidden', !showForm);
}
populateForm() {
if (!this.currentConfig) return;
document.getElementById('popup-proxy-type').value = this.currentConfig.type || 'http';
document.getElementById('popup-proxy-host').value = this.currentConfig.host || '';
document.getElementById('popup-proxy-port').value = this.currentConfig.port || '';
document.getElementById('popup-proxy-username').value = this.currentConfig.username || '';
document.getElementById('popup-proxy-password').value = this.currentConfig.password || '';
}
async saveConfig() {
if (!this.currentDomain) return;
// Collect form data
const domainConfig = {
type: document.getElementById('popup-proxy-type').value,
host: document.getElementById('popup-proxy-host').value.trim(),
port: parseInt(document.getElementById('popup-proxy-port').value, 10),
username: document.getElementById('popup-proxy-username').value.trim() || null,
password: document.getElementById('popup-proxy-password').value || null,
enabled: document.getElementById('enable-proxy').checked
};
// Validation
if (!domainConfig.host || !domainConfig.port) {
this.showError('Host and port are required');
return false;
}
if (domainConfig.port < 1 || domainConfig.port > 65535) {
this.showError('Port must be between 1 and 65535');
return false;
}
try {
// Use centralized API for domain update
const response = await browser.runtime.sendMessage({
action: 'updateDomain',
domain: this.currentDomain,
domainConfig: domainConfig
});
return response.success;
} catch (error) {
console.error('Failed to save config:', error);
this.showError('Failed to save configuration');
return false;
}
}
async removeDomainConfig() {
try {
// Use centralized API for domain removal
const response = await browser.runtime.sendMessage({
action: 'removeDomain',
domain: this.currentDomain
});
return response.success;
} catch (error) {
console.error('Failed to remove config:', error);
return false;
}
}
async toggleProxy(enabled) {
if (!this.currentDomain) return;
try {
const response = await browser.runtime.sendMessage({
action: 'toggleProxy',
domain: this.currentDomain,
enabled: enabled
});
if (!response.success) {
// If domain not configured, show message
this.showError('Domain not configured. Use "Apply & Reload" to configure proxy first.');
// Return checkbox to previous state
document.getElementById('enable-proxy').checked = !enabled;
}
} catch (error) {
console.error('Failed to toggle proxy:', error);
this.showError('Failed to toggle proxy');
// Return checkbox to previous state
document.getElementById('enable-proxy').checked = !enabled;
}
}
async applyAndReload() {
const success = await this.saveConfig();
if (success && this.currentTab) {
// Reload tab
await browser.tabs.reload(this.currentTab.id);
// Close popup
window.close();
}
}
showError(message) {
// Simple error display implementation
const errorDiv = document.createElement('div');
errorDiv.className = 'error-message';
errorDiv.textContent = message;
errorDiv.style.cssText = `
background-color: #f8d7da;
color: #721c24;
padding: 10px;
margin: 10px 0;
border-radius: 4px;
border: 1px solid #f5c6cb;
`;
const main = document.querySelector('.popup-main');
main.insertBefore(errorDiv, main.firstChild);
setTimeout(() => errorDiv.remove(), 3000);
}
setupEventListeners() {
// Proxy toggle (quick enable/disable)
document.getElementById('enable-proxy').addEventListener('change', async (e) => {
const enabled = e.target.checked;
this.updateFormVisibility(enabled); // Show/hide form
// A new domain has no stored configuration yet; Apply & Reload saves it.
if (this.currentConfig) {
await this.toggleProxy(enabled);
}
});
// Apply settings
document.getElementById('apply-proxy').addEventListener('click', () => {
this.applyAndReload();
});
// Open settings
document.getElementById('open-options').addEventListener('click', () => {
browser.runtime.openOptionsPage();
window.close();
});
// Show/hide password
document.querySelector('.toggle-password').addEventListener('click', (e) => {
const input = document.getElementById('popup-proxy-password');
const type = input.type === 'password' ? 'text' : 'password';
input.type = type;
e.target.textContent = type === 'password' ? '👁' : '👁️‍🗨️';
});
// Handle Enter in form
document.querySelectorAll('input').forEach(input => {
input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
this.applyAndReload();
}
});
});
// Fill form from profile
document.getElementById('popup-profile-select').addEventListener('change', (e) => {
this.fillFormFromProfile(e.target.value);
});
}
}
// Initialization
document.addEventListener('DOMContentLoaded', () => {
new PopupManager();
});