TrixVPN

Global VPN Service Project

当前为 2025-11-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==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 = {
        '&': '&amp;',
        '<': '&lt;',
        '>': '&gt;',
        '"': '&quot;',
        "'": '&#039;'
      };
      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();
})();