initial commit
No files matched your search
@@ -0,0 +1 @@
|
||||
web-ext-artifacts
|
||||
@@ -0,0 +1,7 @@
|
||||
default: build
|
||||
|
||||
build:
|
||||
web-ext build --overwrite-dest --filename proxyswitcherforkbywt.xpi
|
||||
|
||||
clean:
|
||||
rm -rvf web-ext-artifacts
|
||||
@@ -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
|
||||
@@ -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();
|
||||
@@ -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;
|
||||
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 763 B |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 121 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 9.6 KiB |
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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();
|
||||
});
|
||||