Files
proxyswitcher/options.js
T
2026-10-09 15:53:24 +07:00

941 lines
33 KiB
JavaScript

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();
});