Text Explainer Settings

Settings module for Text Explainer

目前為 2025-03-04 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/528763/1547395/Text%20Explainer%20Settings.js

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

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

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

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

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

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

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==UserScript==
// @name         Text Explainer Settings
// @namespace    http://tampermonkey.net/
// @version      0.1.2
// @description  Settings module for Text Explainer
// @author       RoCry
// @license      MIT
// ==/UserScript==

class TextExplainerSettings {
  constructor(defaultConfig = {}) {
    this.defaultConfig = Object.assign({
      model: "gemini-2.0-flash",
      apiKey: null,
      baseUrl: "https://generativelanguage.googleapis.com",
      provider: "gemini",
      language: "Chinese",
      shortcut: {
        key: "d",
        ctrlKey: false,
        altKey: true,
        shiftKey: false,
        metaKey: false
      },
      floatingButton: {
        enabled: true,
        size: "medium", // small, medium, large
        position: "bottom-right" // top-left, top-right, bottom-left, bottom-right
      }
    }, defaultConfig);
    
    this.config = this.load();
  }
  
  /**
   * Load settings from storage
   */
  load() {
    try {
      const savedConfig = typeof GM_getValue === 'function' 
        ? GM_getValue('explainerConfig', {})
        : JSON.parse(localStorage.getItem('explainerConfig') || '{}');
      return Object.assign({}, this.defaultConfig, savedConfig);
    } catch (e) {
      console.error('Error loading settings:', e);
      return Object.assign({}, this.defaultConfig);
    }
  }
  
  /**
   * Save settings to storage
   */
  save() {
    try {
      if (typeof GM_setValue === 'function') {
        GM_setValue('explainerConfig', this.config);
      } else {
        localStorage.setItem('explainerConfig', JSON.stringify(this.config));
      }
      return true;
    } catch (e) {
      console.error('Error saving settings:', e);
      return false;
    }
  }
  
  /**
   * Get setting value
   */
  get(key) {
    return this.config[key];
  }
  
  /**
   * Set setting value
   */
  set(key, value) {
    this.config[key] = value;
    return this;
  }
  
  /**
   * Update multiple settings at once
   */
  update(settings) {
    Object.assign(this.config, settings);
    return this;
  }
  
  /**
   * Reset settings to defaults
   */
  reset() {
    this.config = Object.assign({}, this.defaultConfig);
    return this;
  }
  
  /**
   * Get all settings
   */
  getAll() {
    return Object.assign({}, this.config);
  }
  
  /**
   * Open settings dialog
   */
  openDialog(onSave = null) {
    // First check if dialog already exists and remove it
    const existingDialog = document.getElementById('explainer-settings-dialog');
    if (existingDialog) existingDialog.remove();
    
    // Create dialog container
    const dialog = document.createElement('div');
    dialog.id = 'explainer-settings-dialog';
    dialog.style = `
      position: fixed;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      background: white;
      padding: 16px;
      border-radius: 8px;
      box-shadow: 0 2px 10px rgba(0,0,0,0.2);
      z-index: 10001;
      width: 400px;
      max-width: 90vw;
      font-family: system-ui, sans-serif;
    `;
    
    // Add dark mode support
    const styleElement = document.createElement('style');
    styleElement.textContent = `
      #explainer-settings-dialog {
        color: #333;
      }
      #explainer-settings-dialog label {
        display: block;
        margin: 12px 0 4px;
        font-weight: 500;
      }
      #explainer-settings-dialog input[type="text"],
      #explainer-settings-dialog select {
        width: 100%;
        padding: 8px;
        border: 1px solid #ccc;
        border-radius: 4px;
        box-sizing: border-box;
        font-size: 14px;
      }
      #explainer-settings-dialog .buttons {
        display: flex;
        justify-content: flex-end;
        gap: 8px;
        margin-top: 20px;
      }
      #explainer-settings-dialog button {
        padding: 8px 12px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 14px;
      }
      #explainer-settings-dialog button.primary {
        background-color: #4285f4;
        color: white;
      }
      #explainer-settings-dialog button.secondary {
        background-color: #f1f1f1;
        color: #333;
      }
      #explainer-settings-dialog .shortcut-section {
        display: flex;
        gap: 8px;
      }
      #explainer-settings-dialog .shortcut-section label {
        display: inline;
        margin-right: 4px;
      }
      #explainer-settings-dialog .shortcut-section input[type="checkbox"] {
        margin-right: 2px;
      }
      #explainer-settings-dialog .shortcut-key {
        width: 60px;
      }
      #explainer-settings-dialog .section-title {
        font-weight: 600;
        margin-top: 16px;
        border-bottom: 1px solid #ddd;
        padding-bottom: 4px;
      }
      @media (prefers-color-scheme: dark) {
        #explainer-settings-dialog {
          background: #333;
          color: #eee;
        }
        #explainer-settings-dialog input[type="text"],
        #explainer-settings-dialog select {
          background: #444;
          color: #eee;
          border-color: #555;
        }
        #explainer-settings-dialog button.secondary {
          background-color: #555;
          color: #eee;
        }
        #explainer-settings-dialog .section-title {
          border-bottom-color: #555;
        }
      }
    `;
    document.head.appendChild(styleElement);
    
    // Prepare shortcut configuration
    const shortcut = this.config.shortcut || this.defaultConfig.shortcut;
    const floatingButton = this.config.floatingButton || this.defaultConfig.floatingButton;
    
    // Create dialog content
    dialog.innerHTML = `
      <h3 style="margin-top:0;">Text Explainer Settings</h3>
      
      <div class="section-title">Language & API Settings</div>
      <div>
        <label for="explainer-language">Language</label>
        <select id="explainer-language">
          <option value="Chinese" ${this.config.language === 'Chinese' ? 'selected' : ''}>Chinese</option>
          <option value="English" ${this.config.language === 'English' ? 'selected' : ''}>English</option>
          <option value="Japanese" ${this.config.language === 'Japanese' ? 'selected' : ''}>Japanese</option>
        </select>
      </div>
      <div>
        <label for="explainer-provider">Provider</label>
        <select id="explainer-provider">
          <option value="gemini" ${this.config.provider === 'gemini' ? 'selected' : ''}>Gemini</option>
          <option value="openai" ${this.config.provider === 'openai' ? 'selected' : ''}>OpenAI</option>
          <option value="anthropic" ${this.config.provider === 'anthropic' ? 'selected' : ''}>Anthropic</option>
        </select>
      </div>
      <div>
        <label for="explainer-model">Model</label>
        <input id="explainer-model" type="text" value="${this.config.model}">
      </div>
      <div>
        <label for="explainer-api-key">API Key</label>
        <input id="explainer-api-key" type="text" value="${this.config.apiKey || ''}">
      </div>
      <div>
        <label for="explainer-base-url">API Base URL</label>
        <input id="explainer-base-url" type="text" value="${this.config.baseUrl}">
      </div>
      
      <div class="section-title">Shortcut Settings</div>
      <div class="shortcut-section">
        <div>
          <label for="explainer-shortcut-key">Key</label>
          <input id="explainer-shortcut-key" class="shortcut-key" type="text" maxlength="1" value="${shortcut.key}">
        </div>
        <div>
          <input type="checkbox" id="explainer-shortcut-ctrl" ${shortcut.ctrlKey ? 'checked' : ''}>
          <label for="explainer-shortcut-ctrl">Ctrl</label>
        </div>
        <div>
          <input type="checkbox" id="explainer-shortcut-alt" ${shortcut.altKey ? 'checked' : ''}>
          <label for="explainer-shortcut-alt">Alt/Option</label>
        </div>
        <div>
          <input type="checkbox" id="explainer-shortcut-shift" ${shortcut.shiftKey ? 'checked' : ''}>
          <label for="explainer-shortcut-shift">Shift</label>
        </div>
        <div>
          <input type="checkbox" id="explainer-shortcut-meta" ${shortcut.metaKey ? 'checked' : ''}>
          <label for="explainer-shortcut-meta">Meta/Cmd</label>
        </div>
      </div>
      <p class="shortcut-note" style="font-size: 12px; color: #666; margin-top: 5px;">
        Note: Choose a letter key (a-z) for best cross-platform compatibility. At least one modifier key is recommended.
      </p>
      
      <div class="section-title">Touch Device Settings</div>
      <div>
        <input type="checkbox" id="explainer-floating-enabled" ${floatingButton.enabled ? 'checked' : ''}>
        <label for="explainer-floating-enabled">Show floating button on touch devices</label>
      </div>
      <div>
        <label for="explainer-floating-size">Button Size</label>
        <select id="explainer-floating-size">
          <option value="small" ${floatingButton.size === 'small' ? 'selected' : ''}>Small</option>
          <option value="medium" ${floatingButton.size === 'medium' ? 'selected' : ''}>Medium</option>
          <option value="large" ${floatingButton.size === 'large' ? 'selected' : ''}>Large</option>
        </select>
      </div>
      <div>
        <label for="explainer-floating-position">Button Position</label>
        <select id="explainer-floating-position">
          <option value="top-left" ${floatingButton.position === 'top-left' ? 'selected' : ''}>Top Left</option>
          <option value="top-right" ${floatingButton.position === 'top-right' ? 'selected' : ''}>Top Right</option>
          <option value="bottom-left" ${floatingButton.position === 'bottom-left' ? 'selected' : ''}>Bottom Left</option>
          <option value="bottom-right" ${floatingButton.position === 'bottom-right' ? 'selected' : ''}>Bottom Right</option>
        </select>
      </div>
      
      <div class="buttons">
        <button id="explainer-settings-cancel" class="secondary">Cancel</button>
        <button id="explainer-settings-save" class="primary">Save</button>
      </div>
    `;
    
    document.body.appendChild(dialog);
    
    // Add event listeners
    document.getElementById('explainer-settings-save').addEventListener('click', () => {
      // Get shortcut settings
      const shortcutSettings = {
        key: document.getElementById('explainer-shortcut-key').value.toLowerCase(),
        ctrlKey: document.getElementById('explainer-shortcut-ctrl').checked,
        altKey: document.getElementById('explainer-shortcut-alt').checked,
        shiftKey: document.getElementById('explainer-shortcut-shift').checked,
        metaKey: document.getElementById('explainer-shortcut-meta').checked
      };
      
      // Get floating button settings
      const floatingButtonSettings = {
        enabled: document.getElementById('explainer-floating-enabled').checked,
        size: document.getElementById('explainer-floating-size').value,
        position: document.getElementById('explainer-floating-position').value
      };
      
      // Update config with all form values
      this.update({
        language: document.getElementById('explainer-language').value,
        model: document.getElementById('explainer-model').value,
        apiKey: document.getElementById('explainer-api-key').value,
        baseUrl: document.getElementById('explainer-base-url').value,
        provider: document.getElementById('explainer-provider').value,
        shortcut: shortcutSettings,
        floatingButton: floatingButtonSettings
      });
      
      // Save to storage
      this.save();
      
      // Remove dialog
      dialog.remove();
      styleElement.remove();
      
      // Call save callback if provided
      if (typeof onSave === 'function') {
        onSave(this.config);
      }
    });
    
    document.getElementById('explainer-settings-cancel').addEventListener('click', () => {
      dialog.remove();
      styleElement.remove();
    });
    
    // Focus first field
    document.getElementById('explainer-language').focus();

    // Add validation for the shortcut key
    const keyInput = document.getElementById('explainer-shortcut-key');
    keyInput.addEventListener('input', () => {
      // Ensure it's a single character and convert to lowercase
      if (keyInput.value.length > 0) {
        keyInput.value = keyInput.value.charAt(0).toLowerCase();
      }
    });
  }
}

// Make available globally and as a module if needed
window.TextExplainerSettings = TextExplainerSettings;

if (typeof module !== 'undefined') {
  module.exports = TextExplainerSettings;
}