Global VPN Service Project
当前为
// ==UserScript==
// @name TrixVPN
// @namespace https://greasyfork.org/en/users/1490385-courtesycoil
// @version 0.02
// @description Global VPN Service Project
// @author Painsel
// @license Copyright Painsel - All rights reserved
// @match *://*/*
// @icon data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="45" fill="%234CAF50"/><text x="50" y="65" font-size="40" fill="white" text-anchor="middle" font-weight="bold">VPN</text></svg>
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_addStyle
// @grant GM_notification
// @grant GM_xmlhttpRequest
// @run-at document-start
// ==/UserScript==
/*
TrixVPN
Copyright Painsel - All rights reserved
https://greasyfork.org/en/users/1490385-courtesycoil
Unauthorized copying, modification, or distribution of this script is prohibited.
*/
(function() {
'use strict';
// ==================== Configuration ====================
// Copyright Painsel - All rights reserved
const CONFIG = {
defaultProxyServer: 'direct',
proxyServers: [
{ name: 'Direct (No VPN)', address: 'direct' }
],
storageKey: 'vpnProxyControlState',
proxyListStorageKey: 'trixVpnProxyList',
autoConnectOnLoad: false,
// Free proxy list API from Geonode
proxyApiUrl: 'https://proxylist.geonode.com/api/proxy-list?limit=100&page=1&sort_by=lastChecked&sort_type=desc',
proxyApiRefreshInterval: 3600000 // 1 hour in milliseconds
};
// ==================== Proxy Fetcher ====================
class ProxyFetcher {
constructor() {
this.proxies = [];
this.lastFetchTime = null;
this.isFetching = false;
}
async fetchProxies() {
if (this.isFetching) return;
// Check if we have cached proxies and they're still fresh
const cached = this.getCachedProxies();
if (cached && cached.length > 0) {
this.proxies = cached;
return cached;
}
this.isFetching = true;
try {
const response = await this.makeRequest(CONFIG.proxyApiUrl);
if (response && response.data && Array.isArray(response.data)) {
this.proxies = response.data.map(proxy => ({
name: `${proxy.country} - ${proxy.ip}:${proxy.port}`,
address: `${proxy.ip}:${proxy.port}`
})).slice(0, 50); // Limit to 50 proxies
// Cache the proxies
this.cacheProxies(this.proxies);
console.log(`[TrixVPN] Fetched ${this.proxies.length} proxies from Geonode API`);
return this.proxies;
}
} catch (e) {
console.error('[TrixVPN] Error fetching proxies:', e);
} finally {
this.isFetching = false;
}
return this.proxies;
}
makeRequest(url) {
return new Promise((resolve, reject) => {
try {
GM_xmlhttpRequest({
method: 'GET',
url: url,
timeout: 10000,
onload: (response) => {
try {
const data = JSON.parse(response.responseText);
resolve(data);
} catch (e) {
reject(e);
}
},
onerror: (error) => {
reject(error);
},
ontimeout: () => {
reject(new Error('Request timeout'));
}
});
} catch (e) {
reject(e);
}
});
}
cacheProxies(proxies) {
GM_setValue(CONFIG.proxyListStorageKey, JSON.stringify({
proxies: proxies,
timestamp: Date.now()
}));
}
getCachedProxies() {
try {
const cached = GM_getValue(CONFIG.proxyListStorageKey, null);
if (cached) {
const data = JSON.parse(cached);
const now = Date.now();
// Use cache if less than 1 hour old
if (now - data.timestamp < CONFIG.proxyApiRefreshInterval) {
return data.proxies;
}
}
} catch (e) {
console.error('[TrixVPN] Error retrieving cached proxies:', e);
}
return null;
}
getProxies() {
return this.proxies;
}
}
// ==================== VPN State Management ====================
class VPNStateManager {
constructor() {
this.isConnected = false;
this.currentServer = null;
this.loadState();
}
loadState() {
const saved = GM_getValue(CONFIG.storageKey, null);
if (saved) {
try {
const state = JSON.parse(saved);
this.isConnected = state.isConnected || false;
this.currentServer = state.currentServer || CONFIG.defaultProxyServer;
} catch (e) {
console.error('[VPN Control] Error loading saved state:', e);
this.initialize();
}
} else {
this.initialize();
}
}
initialize() {
this.currentServer = CONFIG.defaultProxyServer;
this.isConnected = CONFIG.autoConnectOnLoad;
this.saveState();
}
saveState() {
GM_setValue(CONFIG.storageKey, JSON.stringify({
isConnected: this.isConnected,
currentServer: this.currentServer
}));
}
toggleVPN() {
this.isConnected = !this.isConnected;
this.saveState();
return this.isConnected;
}
setServer(serverAddress) {
this.currentServer = serverAddress;
this.saveState();
}
getStatus() {
return {
connected: this.isConnected,
server: this.currentServer
};
}
}
// ==================== UI Manager ====================
class VPNUIManager {
constructor(stateManager, proxyFetcher) {
this.state = stateManager;
this.proxyFetcher = proxyFetcher;
this.container = null;
this.statusIndicator = null;
this.toggleButton = null;
this.serverSelect = null;
this.loadingIndicator = null;
}
injectStyles() {
const styles = `
#vpn-control-widget {
position: fixed;
top: 20px;
right: 20px;
z-index: 10000;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
padding: 16px;
min-width: 280px;
color: white;
backdrop-filter: blur(10px);
}
#vpn-control-widget * {
box-sizing: border-box;
}
.vpn-widget-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 12px;
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
padding-bottom: 12px;
}
.vpn-widget-title {
font-size: 14px;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
}
.vpn-status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
background-color: #ff4757;
animation: pulse 2s infinite;
margin-right: 8px;
}
.vpn-status-indicator.connected {
background-color: #2ed573;
animation: pulse-green 2s infinite;
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.6;
transform: scale(1.1);
}
}
@keyframes pulse-green {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
.vpn-status-text {
font-size: 11px;
opacity: 0.9;
margin-top: 4px;
}
.vpn-status-text.connected {
color: #2ed573;
}
.vpn-status-text.disconnected {
color: #ff4757;
}
.vpn-toggle-button {
width: 100%;
padding: 10px 16px;
margin: 12px 0;
border: none;
border-radius: 8px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
text-transform: uppercase;
letter-spacing: 0.5px;
background-color: rgba(255, 255, 255, 0.2);
color: white;
}
.vpn-toggle-button:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: translateY(-2px);
}
.vpn-toggle-button:active {
transform: translateY(0);
}
.vpn-toggle-button.connected {
background-color: #2ed573;
color: #1a1a1a;
}
.vpn-toggle-button.connected:hover {
background-color: #26d063;
}
.vpn-server-select {
width: 100%;
padding: 8px 12px;
margin: 8px 0;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 6px;
background-color: rgba(255, 255, 255, 0.1);
color: white;
font-size: 12px;
font-family: inherit;
cursor: pointer;
transition: all 0.3s ease;
}
.vpn-server-select:hover {
background-color: rgba(255, 255, 255, 0.15);
border-color: rgba(255, 255, 255, 0.5);
}
.vpn-server-select option {
background-color: #333;
color: white;
}
.vpn-server-info {
font-size: 11px;
opacity: 0.85;
padding: 8px;
background-color: rgba(0, 0, 0, 0.2);
border-radius: 6px;
margin-top: 8px;
word-break: break-all;
}
.vpn-server-label {
font-weight: 600;
margin-bottom: 4px;
}
.vpn-widget-footer {
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid rgba(255, 255, 255, 0.2);
font-size: 10px;
opacity: 0.7;
text-align: center;
}
.vpn-minimize-btn {
background: none;
border: none;
color: white;
font-size: 16px;
cursor: pointer;
padding: 0;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
}
.vpn-minimize-btn:hover {
opacity: 0.8;
}
#vpn-control-widget.minimized {
min-width: auto;
padding: 8px;
}
#vpn-control-widget.minimized .vpn-widget-content {
display: none;
}
`;
GM_addStyle(styles);
}
createWidget() {
// Create main container
this.container = document.createElement('div');
this.container.id = 'vpn-control-widget';
const status = this.state.getStatus();
// Header with title and minimize button
const header = document.createElement('div');
header.className = 'vpn-widget-header';
const title = document.createElement('div');
title.className = 'vpn-widget-title';
title.textContent = '🛡️ VPN Control';
const minimizeBtn = document.createElement('button');
minimizeBtn.className = 'vpn-minimize-btn';
minimizeBtn.textContent = '−';
minimizeBtn.title = 'Minimize widget';
minimizeBtn.addEventListener('click', () => {
this.container.classList.toggle('minimized');
});
header.appendChild(title);
header.appendChild(minimizeBtn);
// Content wrapper
const content = document.createElement('div');
content.className = 'vpn-widget-content';
// Status indicator and text
const statusDiv = document.createElement('div');
this.statusIndicator = document.createElement('span');
this.statusIndicator.className = 'vpn-status-indicator' + (status.connected ? ' connected' : '');
const statusTextSpan = document.createElement('span');
statusTextSpan.className = 'vpn-status-text' + (status.connected ? ' connected' : ' disconnected');
statusTextSpan.textContent = status.connected ? '🔒 Connected' : '🔓 Disconnected';
statusDiv.appendChild(this.statusIndicator);
statusDiv.appendChild(statusTextSpan);
// Toggle button
this.toggleButton = document.createElement('button');
this.toggleButton.className = 'vpn-toggle-button' + (status.connected ? ' connected' : '');
this.toggleButton.textContent = status.connected ? '⏸ Disconnect' : '▶ Connect';
this.toggleButton.addEventListener('click', () => this.handleToggle());
// Server selection
const serverLabel = document.createElement('div');
serverLabel.style.fontSize = '11px';
serverLabel.style.fontWeight = '600';
serverLabel.style.marginTop = '8px';
serverLabel.style.marginBottom = '4px';
serverLabel.textContent = 'Select Server:';
this.serverSelect = document.createElement('select');
this.serverSelect.className = 'vpn-server-select';
this.serverSelect.addEventListener('change', (e) => this.handleServerChange(e));
// Add Direct option
const directOption = document.createElement('option');
directOption.value = 'direct';
directOption.textContent = 'Direct (No VPN)';
if ('direct' === status.server) {
directOption.selected = true;
}
this.serverSelect.appendChild(directOption);
// Add fetched proxies
const proxies = this.proxyFetcher.getProxies();
if (proxies && proxies.length > 0) {
proxies.forEach((server) => {
const option = document.createElement('option');
option.value = server.address;
option.textContent = server.name;
if (server.address === status.server) {
option.selected = true;
}
this.serverSelect.appendChild(option);
});
} else {
// Show loading indicator
const loadingOption = document.createElement('option');
loadingOption.value = '';
loadingOption.textContent = '⏳ Loading proxies...';
this.serverSelect.appendChild(loadingOption);
}
// Server info display
const serverInfo = document.createElement('div');
serverInfo.className = 'vpn-server-info';
serverInfo.innerHTML = `<div class="vpn-server-label">Current Server:</div>${this.escapeHtml(status.server)}`;
// Footer
const footer = document.createElement('div');
footer.className = 'vpn-widget-footer';
footer.textContent = 'TrixVPN v0.01 by Painsel';
// Assemble widget
content.appendChild(statusDiv);
content.appendChild(this.toggleButton);
content.appendChild(serverLabel);
content.appendChild(this.serverSelect);
content.appendChild(serverInfo);
this.container.appendChild(header);
this.container.appendChild(content);
this.container.appendChild(footer);
return this.container;
}
handleToggle() {
const newState = this.state.toggleVPN();
this.updateUI();
this.showNotification(
newState ? '✓ VPN Connected' : '✗ VPN Disconnected',
newState ? 'VPN proxy is now active' : 'VPN proxy has been disabled'
);
}
handleServerChange(event) {
const selectedServer = event.target.value;
this.state.setServer(selectedServer);
this.updateUI();
this.showNotification(
'🔄 Server Changed',
`Switched to: ${event.target.options[event.target.selectedIndex].text}`
);
}
updateUI() {
const status = this.state.getStatus();
// Update status indicator
if (status.connected) {
this.statusIndicator.classList.add('connected');
} else {
this.statusIndicator.classList.remove('connected');
}
// Update toggle button
if (status.connected) {
this.toggleButton.classList.add('connected');
this.toggleButton.textContent = '⏸ Disconnect';
} else {
this.toggleButton.classList.remove('connected');
this.toggleButton.textContent = '▶ Connect';
}
// Update server select
this.serverSelect.value = status.server;
// Update server info
const serverInfo = this.container.querySelector('.vpn-server-info');
serverInfo.innerHTML = `<div class="vpn-server-label">Current Server:</div>${this.escapeHtml(status.server)}`;
}
showNotification(title, message) {
try {
GM_notification({
title: title,
text: message,
highlight: true,
timeout: 5000
});
} catch (e) {
console.log('[VPN Control]', title, '-', message);
}
}
escapeHtml(text) {
const map = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": '''
};
return text.replace(/[&<>"']/g, m => map[m]);
}
inject() {
this.injectStyles();
const widget = this.createWidget();
document.documentElement.appendChild(widget);
// Fetch proxies in the background
this.proxyFetcher.fetchProxies().then(() => {
this.refreshServerOptions();
}).catch(e => {
console.error('[TrixVPN] Failed to fetch proxies:', e);
});
}
refreshServerOptions() {
if (!this.serverSelect) return;
// Clear existing options except Direct
while (this.serverSelect.options.length > 1) {
this.serverSelect.remove(this.serverSelect.options.length - 1);
}
// Add fetched proxies
const proxies = this.proxyFetcher.getProxies();
if (proxies && proxies.length > 0) {
proxies.forEach((server) => {
const option = document.createElement('option');
option.value = server.address;
option.textContent = server.name;
this.serverSelect.appendChild(option);
});
console.log(`[TrixVPN] Added ${proxies.length} proxies to server list`);
}
}
}
// ==================== Main Initialization ====================
function initialize() {
try {
const proxyFetcher = new ProxyFetcher();
const stateManager = new VPNStateManager();
const uiManager = new VPNUIManager(stateManager, proxyFetcher);
// Wait for DOM to be ready
if (document.documentElement) {
uiManager.inject();
console.log('[TrixVPN] Script initialized successfully');
} else {
document.addEventListener('DOMContentLoaded', () => {
uiManager.inject();
console.log('[TrixVPN] Script initialized successfully');
});
}
} catch (e) {
console.error('[TrixVPN] Initialization error:', e);
}
}
// Start the script
initialize();
})();