TrixVPN

Global VPN Service Project

目前為 2025-11-27 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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();
})();