GGn Upload Templator

Auto-fill upload forms using torrent file data with configurable templates

Verze ze dne 30. 09. 2025. Zobrazit nejnovější verzi.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name        GGn Upload Templator
// @namespace   https://greatest.deepsurf.us/
// @version     0.6
// @description Auto-fill upload forms using torrent file data with configurable templates
// @author      leveldesigner
// @license     Unlicense
// @source      https://github.com/lvldesigner/userscripts/tree/main/ggn-upload-templator
// @supportURL  https://github.com/lvldesigner/userscripts/tree/main/ggn-upload-templator
// @icon        https://gazellegames.net/favicon.ico
// @match       https://*.gazellegames.net/upload.php*
// @grant       GM_addStyle
// ==/UserScript==

(function() {
  "use strict";
  const DEFAULT_CONFIG = {
    TARGET_FORM_SELECTOR: "#upload_table",
    SUBMIT_KEYBINDING: true,
    CUSTOM_SUBMIT_KEYBINDING: "Ctrl+Enter",
    APPLY_KEYBINDING: true,
    CUSTOM_APPLY_KEYBINDING: "Ctrl+Shift+A",
    CUSTOM_FIELD_SELECTORS: [],
    IGNORED_FIELDS_BY_DEFAULT: [
      "linkgroup",
      "groupid",
      "apikey",
      "type",
      "amazonuri",
      "googleplaybooksuri",
      "goodreadsuri",
      "isbn",
      "scan_dpi",
      "other_dpi",
      "release_desc",
      "anonymous",
      "dont_check_rules",
      "title",
      "tags",
      "image",
      "gameswebsiteuri",
      "wikipediauri",
      "album_desc",
      "submit_upload"
    ]
  };
  const logDebug = (...messages) => {
    const css = "color: #4dd0e1; font-weight: 900;";
    console.debug("%c[GGn Upload Templator]", css, ...messages);
  };
  function getCurrentFormData(config) {
    const formData = {};
    const formSelector = config.TARGET_FORM_SELECTOR || "form";
    const targetForm = document.querySelector(formSelector);
    const defaultSelector = "input[name], select[name], textarea[name]";
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    const fieldSelector = customSelectors.length > 0 ? `${defaultSelector}, ${customSelectors.join(", ")}` : defaultSelector;
    const inputs = targetForm ? targetForm.querySelectorAll(fieldSelector) : document.querySelectorAll(fieldSelector);
    inputs.forEach((input) => {
      const isCustomField = isElementMatchedByCustomSelector(input, config);
      const hasValidIdentifier = isCustomField ? input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name") : input.name;
      if (!hasValidIdentifier) return;
      if (!isCustomField && (input.type === "file" || input.type === "button" || input.type === "submit")) {
        return;
      }
      const fieldName = input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name");
      if (fieldName) {
        if (input.type === "radio" && formData[fieldName]) {
          return;
        }
        const fieldInfo = {
          value: isCustomField ? input.value || input.textContent || input.getAttribute("data-value") || "" : input.type === "checkbox" || input.type === "radio" ? input.checked : input.value || "",
          label: getFieldLabel(input, config),
          type: input.tagName.toLowerCase(),
          inputType: input.type || "custom"
        };
        if (input.type === "radio") {
          const radioGroup = document.querySelectorAll(
            `input[name="${fieldName}"][type="radio"]`
          );
          fieldInfo.radioOptions = Array.from(radioGroup).map((radio) => ({
            value: radio.value,
            checked: radio.checked,
            label: getFieldLabel(radio, config) || radio.value
          }));
          const selectedRadio = Array.from(radioGroup).find(
            (radio) => radio.checked
          );
          fieldInfo.selectedValue = selectedRadio ? selectedRadio.value : "";
          fieldInfo.value = fieldInfo.selectedValue;
        }
        if (input.tagName.toLowerCase() === "select") {
          fieldInfo.options = Array.from(input.options).map((option) => ({
            value: option.value,
            text: option.textContent.trim(),
            selected: option.selected
          }));
          fieldInfo.selectedValue = input.value;
        }
        formData[fieldName] = fieldInfo;
      }
    });
    return formData;
  }
  function isElementMatchedByCustomSelector(element, config) {
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    if (customSelectors.length === 0) return false;
    return customSelectors.some((selector) => {
      try {
        return element.matches(selector);
      } catch (e) {
        console.warn(`Invalid custom selector: ${selector}`, e);
        return false;
      }
    });
  }
  function cleanLabelText(text) {
    if (!text) return text;
    const tempElement = document.createElement("div");
    tempElement.innerHTML = text;
    const linkElements = tempElement.querySelectorAll("a");
    linkElements.forEach((link) => {
      link.remove();
    });
    let cleanedText = tempElement.textContent || tempElement.innerText || "";
    cleanedText = cleanedText.trim();
    if (cleanedText.endsWith(":")) {
      cleanedText = cleanedText.slice(0, -1).trim();
    }
    return cleanedText;
  }
  function getFieldLabel(input, config) {
    const isCustomField = isElementMatchedByCustomSelector(input, config);
    if (isCustomField) {
      const parent = input.parentElement;
      if (parent) {
        const labelElement = parent.querySelector("label");
        if (labelElement) {
          const rawText = labelElement.innerHTML || labelElement.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.id || input.name || "Custom Field";
        }
        const labelClassElement = parent.querySelector('*[class*="label"]');
        if (labelClassElement) {
          const rawText = labelClassElement.innerHTML || labelClassElement.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.id || input.name || "Custom Field";
        }
      }
      return input.id || input.name || "Custom Field";
    }
    if (input.type === "radio" && input.id) {
      const parentTd = input.closest("td");
      if (parentTd) {
        const associatedLabel = parentTd.querySelector(
          `label[for="${input.id}"]`
        );
        if (associatedLabel) {
          const rawText = associatedLabel.innerHTML || associatedLabel.textContent || "";
          const cleanedText = cleanLabelText(rawText);
          return cleanedText || input.value;
        }
      }
    }
    const parentRow = input.closest("tr");
    if (parentRow) {
      const labelCell = parentRow.querySelector("td.label");
      if (labelCell) {
        const rawText = labelCell.innerHTML || labelCell.textContent || "";
        const cleanedText = cleanLabelText(rawText);
        return cleanedText ? `${cleanedText} (${input.name})` : input.name;
      }
    }
    return input.name;
  }
  function findElementByFieldName(fieldName, config) {
    config.TARGET_FORM_SELECTOR ? `${config.TARGET_FORM_SELECTOR} ` : "";
    const defaultSelector = "input[name], select[name], textarea[name]";
    const customSelectors = config.CUSTOM_FIELD_SELECTORS || [];
    const fieldSelector = customSelectors.length > 0 ? `${defaultSelector}, ${customSelectors.join(", ")}` : defaultSelector;
    const targetForm = config.TARGET_FORM_SELECTOR ? document.querySelector(config.TARGET_FORM_SELECTOR) : null;
    const inputs = targetForm ? targetForm.querySelectorAll(fieldSelector) : document.querySelectorAll(fieldSelector);
    for (const input of inputs) {
      const isCustomField = isElementMatchedByCustomSelector(input, config);
      const hasValidIdentifier = isCustomField ? input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name") : input.name;
      if (!hasValidIdentifier) continue;
      if (!isCustomField && (input.type === "file" || input.type === "button" || input.type === "submit")) {
        continue;
      }
      const elementFieldName = input.name || input.id || input.getAttribute("data-field") || input.getAttribute("data-name");
      if (elementFieldName === fieldName) {
        return input;
      }
    }
    return null;
  }
  class TorrentUtils {
    // Parse torrent file for metadata
    static async parseTorrentFile(file) {
      const arrayBuffer = await file.arrayBuffer();
      const data = new Uint8Array(arrayBuffer);
      try {
        const [torrent] = TorrentUtils.decodeBencode(data);
        return {
          name: torrent.info?.name || file.name,
          comment: torrent.comment || "",
          files: torrent.info?.files?.map((f) => ({
            path: f.path.join("/"),
            length: f.length
          })) || [
            {
              path: torrent.info?.name || file.name,
              length: torrent.info?.length
            }
          ]
        };
      } catch (e) {
        console.warn("Could not parse torrent file:", e);
        return { name: file.name, comment: "", files: [] };
      }
    }
    static parseCommentVariables(comment) {
      if (!comment || typeof comment !== "string") return {};
      const variables = {};
      const pairs = comment.split(";");
      for (const pair of pairs) {
        const trimmedPair = pair.trim();
        if (!trimmedPair) continue;
        const eqIndex = trimmedPair.indexOf("=");
        if (eqIndex === -1) continue;
        const key = trimmedPair.substring(0, eqIndex).trim();
        const value = trimmedPair.substring(eqIndex + 1).trim();
        if (key) {
          variables[`_${key}`] = value;
        }
      }
      return variables;
    }
    // Simple bencode decoder
    static decodeBencode(data, offset = 0) {
      const char = String.fromCharCode(data[offset]);
      if (char === "d") {
        const dict = {};
        offset++;
        while (data[offset] !== 101) {
          const [key, newOffset1] = TorrentUtils.decodeBencode(data, offset);
          const [value, newOffset2] = TorrentUtils.decodeBencode(
            data,
            newOffset1
          );
          dict[key] = value;
          offset = newOffset2;
        }
        return [dict, offset + 1];
      }
      if (char === "l") {
        const list = [];
        offset++;
        while (data[offset] !== 101) {
          const [value, newOffset] = TorrentUtils.decodeBencode(data, offset);
          list.push(value);
          offset = newOffset;
        }
        return [list, offset + 1];
      }
      if (char === "i") {
        offset++;
        let num = "";
        while (data[offset] !== 101) {
          num += String.fromCharCode(data[offset]);
          offset++;
        }
        return [parseInt(num), offset + 1];
      }
      if (char >= "0" && char <= "9") {
        let lengthStr = "";
        while (data[offset] !== 58) {
          lengthStr += String.fromCharCode(data[offset]);
          offset++;
        }
        const length = parseInt(lengthStr);
        offset++;
        const str = new TextDecoder("utf-8", { fatal: false }).decode(
          data.slice(offset, offset + length)
        );
        return [str, offset + length];
      }
      throw new Error("Invalid bencode data");
    }
  }
  function parseTemplate(mask, torrentName, greedyMatching = true) {
    if (!mask || !torrentName) return {};
    let regexPattern = mask.replace(/\\\$/g, "___ESCAPED_DOLLAR___").replace(/\\\{/g, "___ESCAPED_LBRACE___").replace(/\\\}/g, "___ESCAPED_RBRACE___").replace(/\\\\/g, "___ESCAPED_BACKSLASH___").replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(/\\\$\\\{([^}]+)\\\}/g, (match, varName, offset, string) => {
      if (greedyMatching) {
        return `(?<${varName}>.+)`;
      } else {
        const remainingString = string.slice(offset + match.length);
        const hasMoreVariables = /\\\$\\\{[^}]+\\\}/.test(remainingString);
        if (hasMoreVariables) {
          return `(?<${varName}>.*?)`;
        } else {
          return `(?<${varName}>.+)`;
        }
      }
    }).replace(/___ESCAPED_DOLLAR___/g, "\\$").replace(/___ESCAPED_LBRACE___/g, "\\{").replace(/___ESCAPED_RBRACE___/g, "\\}").replace(/___ESCAPED_BACKSLASH___/g, "\\\\");
    try {
      const regex = new RegExp(regexPattern, "i");
      const match = torrentName.match(regex);
      return match?.groups || {};
    } catch (e) {
      console.warn("Invalid template regex:", e);
      return {};
    }
  }
  function validateMaskVariables(mask) {
    if (!mask) return { valid: true, invalidVars: [] };
    const invalidVars = [];
    const varPattern = /\$\{([^}]+)\}/g;
    let match;
    while ((match = varPattern.exec(mask)) !== null) {
      const varName = match[1];
      if (varName.startsWith("_")) {
        invalidVars.push(varName);
      }
    }
    return {
      valid: invalidVars.length === 0,
      invalidVars
    };
  }
  function interpolate(template, data, commentVariables = {}) {
    if (!template) return template;
    const allData = { ...data, ...commentVariables };
    return template.replace(/\$\{([^}]+)\}/g, (match, key) => allData[key] || match);
  }
  function findMatchingOption(options, variableValue, matchType) {
    if (!options || !variableValue) return null;
    const normalizedValue = variableValue.toLowerCase();
    for (const option of options) {
      const optionText = option.textContent ? option.textContent.toLowerCase() : option.text.toLowerCase();
      const optionValue = option.value.toLowerCase();
      let matches = false;
      switch (matchType) {
        case "exact":
          matches = optionText === normalizedValue || optionValue === normalizedValue;
          break;
        case "contains":
          matches = optionText.includes(normalizedValue) || optionValue.includes(normalizedValue);
          break;
        case "starts":
          matches = optionText.startsWith(normalizedValue) || optionValue.startsWith(normalizedValue);
          break;
        case "ends":
          matches = optionText.endsWith(normalizedValue) || optionValue.endsWith(normalizedValue);
          break;
      }
      if (matches) {
        return {
          value: option.value,
          text: option.textContent || option.text
        };
      }
    }
    return null;
  }
  const MODAL_HTML = (instance) => `
  <div class="gut-modal-content">
    <div class="gut-modal-tabs">
      <button class="gut-tab-btn active" data-tab="templates">Templates</button>
      <button class="gut-tab-btn" data-tab="settings">Settings</button>
    </div>

    <div class="gut-tab-content active" id="templates-tab">
      ${Object.keys(instance.templates).length === 0 ? '<div style="padding: 20px; text-align: center; color: #888;">No templates found. Create a template first.</div>' : `<div class="gut-template-list">
            ${Object.keys(instance.templates).map(
    (name) => `
                <div class="gut-template-item">
                  <span class="gut-template-name">${instance.escapeHtml(name)}</span>
                  <div class="gut-template-actions">
                    <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="edit" data-template="${instance.escapeHtml(name)}">Edit</button>
                    <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="clone" data-template="${instance.escapeHtml(name)}">Clone</button>
                    <button class="gut-btn gut-btn-danger gut-btn-small" data-action="delete" data-template="${instance.escapeHtml(name)}">Delete</button>
                  </div>
                </div>
              `
  ).join("")}
          </div>`}
    </div>

    <div class="gut-tab-content" id="settings-tab">
      <div class="gut-form-group">
        <label for="setting-form-selector">Target Form Selector:</label>
        <input type="text" id="setting-form-selector" value="${instance.escapeHtml(instance.config.TARGET_FORM_SELECTOR)}" placeholder="#upload_table">
      </div>

       <div class="gut-form-group">
         <div class="gut-keybinding-controls">
           <label class="gut-checkbox-label">
             <input type="checkbox" id="setting-submit-keybinding" ${instance.config.SUBMIT_KEYBINDING ? "checked" : ""}>
             <span class="gut-checkbox-text">\u26A1 Enable form submission keybinding: <span class="gut-keybinding-text">${instance.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter"}</span></span>
           </label>
           <button type="button" id="record-submit-keybinding-btn" class="gut-btn gut-btn-secondary gut-btn-small">Record</button>
         </div>
         <input type="hidden" id="custom-submit-keybinding-input" value="${instance.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter"}">
       </div>

       <div class="gut-form-group">
         <div class="gut-keybinding-controls">
           <label class="gut-checkbox-label">
             <input type="checkbox" id="setting-apply-keybinding" ${instance.config.APPLY_KEYBINDING ? "checked" : ""}>
             <span class="gut-checkbox-text">\u26A1 Enable apply template keybinding: <span class="gut-keybinding-text">${instance.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A"}</span></span>
           </label>
           <button type="button" id="record-apply-keybinding-btn" class="gut-btn gut-btn-secondary gut-btn-small">Record</button>
         </div>
         <input type="hidden" id="custom-apply-keybinding-input" value="${instance.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A"}">
       </div>

      <div class="gut-form-group">
        <label for="setting-custom-selectors">Custom Field Selectors (one per line):</label>
        <textarea id="setting-custom-selectors" rows="4" placeholder="div[data-field]
.custom-input[name]
button[data-value]">${(instance.config.CUSTOM_FIELD_SELECTORS || []).join("\n")}</textarea>
        <div style="font-size: 12px; color: #888; margin-top: 5px;">
          Additional CSS selectors to find form fields. e.g: <a href="#" id="ggn-infobox-link" class="gut-link">GGn Infobox</a>
        </div>
      </div>

      <div class="gut-form-group" id="custom-selectors-preview-group" style="display: none;">
        <label id="matched-elements-label">Matched Elements:</label>
        <div id="custom-selectors-matched" class="gut-extracted-vars">
          <div class="gut-no-variables">No elements matched by custom selectors.</div>
        </div>
      </div>

      <div class="gut-form-group">
        <label for="setting-ignored-fields">Ignored Fields (one per line):</label>
        <textarea id="setting-ignored-fields" rows="6" placeholder="linkgroup
groupid
apikey">${instance.config.IGNORED_FIELDS_BY_DEFAULT.join("\n")}</textarea>
      </div>

      <div class="gut-form-group">
        <div style="display: flex; justify-content: space-between; align-items: center;">
          <div style="display: flex; gap: 10px;">
            <button class="gut-btn gut-btn-primary" id="save-settings">Save Settings</button>
            <button class="gut-btn gut-btn-secondary" id="reset-settings">Reset to Defaults</button>
          </div>
          <button class="gut-btn gut-btn-danger" id="delete-all-config">Delete All Local Config</button>
        </div>
      </div>
    </div>

    <div class="gut-modal-actions">
      <button class="gut-btn" id="close-manager">Close</button>
    </div>
  </div>
`;
  const VARIABLES_MODAL_HTML = (variables) => `
  <div class="gut-modal-content">
    <h2>Available Variables</h2>
    
    <div class="gut-form-group">
      <div class="gut-extracted-vars">
        ${Object.keys(variables).length === 0 ? '<div class="gut-no-variables">No variables available. Select a template with a torrent name mask to see extracted variables.</div>' : Object.entries(variables).map(
    ([name, value]) => `
                  <div class="gut-variable-item">
                    <span class="gut-variable-name">\${${name}}</span>
                    <span class="gut-variable-value">${value || '<em style="color: #666;">empty</em>'}</span>
                  </div>
                `
  ).join("")}
      </div>
    </div>

    <div class="gut-modal-actions">
      <button class="gut-btn" id="close-variables-modal">Close</button>
    </div>
  </div>
`;
  const TEMPLATE_SELECTOR_HTML = (instance) => `
  <option value="">Select Template</option>
  ${Object.keys(instance.templates).map(
    (name) => `<option value="${name}" ${name === instance.selectedTemplate ? "selected" : ""}>${name}</option>`
  ).join("")}
`;
  const TEMPLATE_LIST_HTML = (instance) => Object.keys(instance.templates).length === 0 ? '<div style="padding: 20px; text-align: center; color: #888;">No templates found. Close this dialog and create a template first.</div>' : `<div class="gut-template-list">
         ${Object.keys(instance.templates).map(
    (name) => `
             <div class="gut-template-item">
               <span class="gut-template-name">${instance.escapeHtml(name)}</span>
               <div class="gut-template-actions">
                 <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="edit" data-template="${instance.escapeHtml(name)}">Edit</button>
                 <button class="gut-btn gut-btn-secondary gut-btn-small" data-action="clone" data-template="${instance.escapeHtml(name)}">Clone</button>
                 <button class="gut-btn gut-btn-danger gut-btn-small" data-action="delete" data-template="${instance.escapeHtml(name)}">Delete</button>
               </div>
             </div>
           `
  ).join("")}
       </div>`;
  const TEMPLATE_CREATOR_HTML = (formData, instance, editTemplateName, editTemplate, selectedTorrentName) => `
  <div class="gut-modal-content">
    <h2>
      ${editTemplateName ? '<button class="gut-modal-back-btn" id="back-to-manager" title="Back to Template Manager">&lt;</button>' : ""}
      ${editTemplateName ? "Edit Template" : "Create Template"}
    </h2>

    <div class="gut-form-group">
      <label for="template-name">Template Name:</label>
      <input type="text" id="template-name" placeholder="e.g., Magazine Template" value="${editTemplateName ? instance.escapeHtml(editTemplateName) : ""}">
    </div>

    <div class="gut-form-group">
      <label for="sample-torrent">Sample Torrent Name (for preview):</label>
      <input type="text" id="sample-torrent" value="${instance.escapeHtml(selectedTorrentName)}" placeholder="e.g., PCWorld - Issue 05 - 01-2024.zip">
    </div>

    <div class="gut-form-group" style="margin-bottom: 8px;">
      <label for="torrent-mask">Torrent Name Mask:</label>
      <input type="text" id="torrent-mask" placeholder="e.g., \${magazine} - Issue \${issue} - \${month}-\${year}.\${ext}" value="${editTemplate ? instance.escapeHtml(editTemplate.mask) : ""}">
      <div id="mask-validation-warning"></div>
    </div>

    <div class="gut-form-group">
      <label style="display: inline-flex; align-items: center; gap: 8px; margin: 0; font-size: 13px; color: #888888; font-weight: normal;" title="When enabled, patterns capture as much text as possible. When disabled, uses smart matching that's usually more precise.">
        <input type="checkbox" id="greedy-matching" ${editTemplate ? editTemplate.greedyMatching !== false ? "checked" : "" : "checked"} style="margin: 0; accent-color: #0d7377; width: auto;">
        <span>Greedy matching</span>
      </label>
    </div>

    <div class="gut-form-group">
      <label>Extracted Variables:</label>
      <div id="extracted-variables" class="gut-extracted-vars">
        <div class="gut-no-variables">No variables defined yet. Add variables like \${name} to your mask.</div>
      </div>
    </div>

    <div class="gut-form-group">
      <div style="display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-bottom: 10px;">
        <label style="margin: 0;">Form Fields:</label>
        <div style="display: flex; align-items: center; gap: 10px;">
          <input type="text" id="field-filter" placeholder="Filter fields..." style="padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #2a2a2a; color: #e0e0e0; font-size: 12px; min-width: 150px;">
          <button type="button" class="gut-btn gut-btn-secondary" id="toggle-unselected" style="padding: 6px 12px; font-size: 12px; white-space: nowrap;">Show Unselected</button>
        </div>
      </div>
      <div class="gut-field-list">
        ${Object.entries(formData).map(([name, fieldData]) => {
    const isIgnoredByDefault = instance.config.IGNORED_FIELDS_BY_DEFAULT.includes(
      name.toLowerCase()
    );
    const isInTemplate = editTemplate && editTemplate.fieldMappings.hasOwnProperty(name);
    const templateValue = isInTemplate ? editTemplate.fieldMappings[name] : null;
    let shouldBeChecked = isInTemplate || !isIgnoredByDefault;
    if (editTemplate && editTemplate.customUnselectedFields) {
      const customField = editTemplate.customUnselectedFields.find(
        (f) => f.field === name
      );
      if (customField) {
        shouldBeChecked = customField.selected;
      }
    }
    return `
               <div class="gut-field-row ${isIgnoredByDefault && !isInTemplate && !shouldBeChecked ? "gut-hidden" : ""}">
                 ${fieldData.type === "select" ? (() => {
      const hasVariableMatching = editTemplate && editTemplate.variableMatching && editTemplate.variableMatching[name];
      hasVariableMatching ? editTemplate.variableMatching[name] : null;
      const isVariableMode = hasVariableMatching;
      return `<div style="display: flex; align-items: flex-start; width: 100%;">
                           <a href="#" class="gut-link gut-variable-toggle" data-field="${name}" data-state="${isVariableMode ? "on" : "off"}">Match from variable: ${isVariableMode ? "ON" : "OFF"}</a>
                         </div>`;
    })() : ""}
                 <input type="checkbox" ${shouldBeChecked ? "checked" : ""} data-field="${name}">
                 <label title="${name}">${fieldData.label}:</label>
                 ${fieldData.type === "select" ? (() => {
      const hasVariableMatching = editTemplate && editTemplate.variableMatching && editTemplate.variableMatching[name];
      const variableConfig = hasVariableMatching ? editTemplate.variableMatching[name] : null;
      const isVariableMode = hasVariableMatching;
      return `<div class="gut-select-container" style="display: flex; flex-direction: column; gap: 4px; flex: 1;">
                             <div style="display: flex; flex-direction: column; align-items: flex-end;">
                               <select data-template="${name}" class="template-input gut-select select-static-mode" style="width: 100%; ${isVariableMode ? "display: none;" : ""}">
                                 ${fieldData.options.map((option) => {
        let selected = option.selected;
        if (templateValue && templateValue === option.value) {
          selected = true;
        }
        return `<option value="${instance.escapeHtml(option.value)}" ${selected ? "selected" : ""}>${instance.escapeHtml(option.text)}</option>`;
      }).join("")}
                               </select>
                             </div>
                            <div class="gut-variable-controls" data-field="${name}" style="display: ${isVariableMode ? "flex" : "none"}; gap: 8px;">
                              <select class="gut-match-type" data-field="${name}" style="padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #1a1a1a; color: #e0e0e0; font-size: 12px;">
                              <option value="exact" ${variableConfig && variableConfig.matchType === "exact" ? "selected" : ""}>Is exactly</option>
                              <option value="contains" ${variableConfig && variableConfig.matchType === "contains" ? "selected" : ""}>Contains</option>
                              <option value="starts" ${variableConfig && variableConfig.matchType === "starts" ? "selected" : ""}>Starts with</option>
                              <option value="ends" ${variableConfig && variableConfig.matchType === "ends" ? "selected" : ""}>Ends with</option>
                            </select>
                            <input type="text" class="gut-variable-input" data-field="${name}" placeholder="\${variable_name}" value="${variableConfig ? instance.escapeHtml(variableConfig.variableName) : ""}" style="flex: 1; padding: 6px 8px; border: 1px solid #404040; border-radius: 3px; background: #1a1a1a; color: #e0e0e0; font-size: 12px;">
                            </div>
                          </div>`;
    })() : fieldData.inputType === "checkbox" ? `<input type="checkbox" ${templateValue !== null ? templateValue ? "checked" : "" : fieldData.value ? "checked" : ""} data-template="${name}" class="template-input">` : fieldData.inputType === "radio" ? `<select data-template="${name}" class="template-input gut-select">
                              ${fieldData.radioOptions.map((option) => {
      let selected = option.checked;
      if (templateValue && templateValue === option.value) {
        selected = true;
      }
      return `<option value="${instance.escapeHtml(option.value)}" ${selected ? "selected" : ""}>${instance.escapeHtml(option.label)}</option>`;
    }).join("")}
                            </select>` : fieldData.type === "textarea" ? `<textarea data-template="${name}" class="template-input" rows="4" style="resize: vertical; width: 100%;">${templateValue !== null ? instance.escapeHtml(String(templateValue)) : instance.escapeHtml(String(fieldData.value))}</textarea>` : `<input type="text" value="${templateValue !== null ? instance.escapeHtml(String(templateValue)) : instance.escapeHtml(String(fieldData.value))}" data-template="${name}" class="template-input">`}
                 <span class="gut-preview" data-preview="${name}"></span>
               </div>
             `;
  }).join("")}
      </div>
    </div>

    <div class="gut-modal-actions">
      <button class="gut-btn" id="cancel-template">Cancel</button>
      <button class="gut-btn gut-btn-primary" id="save-template">${editTemplateName ? "Update Template" : "Save Template"}</button>
    </div>
  </div>
`;
  const MAIN_UI_HTML = (instance) => `
  <div id="ggn-upload-templator-controls" class="ggn-upload-templator-controls" style="align-items: flex-end;">
    <div style="display: flex; flex-direction: column; gap: 5px;">
      <div style="display: flex; justify-content: space-between; align-items: center;">
        <label for="template-selector" style="font-size: 12px; color: #b0b0b0; margin: 0;">Select template</label>
        <a href="#" id="edit-selected-template-btn" class="gut-link" style="${instance.selectedTemplate && instance.selectedTemplate !== "none" && instance.templates[instance.selectedTemplate] ? "" : "display: none;"}">Edit</a>
      </div>
       <div style="display: flex; gap: 10px; align-items: center;">
         <select id="template-selector" class="gut-select">
           <option value="">Select Template</option>
           ${Object.keys(instance.templates).map(
    (name) => `<option value="${name}" ${name === instance.selectedTemplate ? "selected" : ""}>${name}</option>`
  ).join("")}
         </select>
       </div>
    </div>
    <button type="button" id="apply-template-btn" class="gut-btn gut-btn-primary">Apply Template</button>
    <button type="button" id="create-template-btn" class="gut-btn gut-btn-primary">+ Create Template</button>
    <button id="manage-templates-btn" type="button" class="gut-btn gut-btn-secondary" title="Manage Templates & Settings">Settings</button>
  </div>
  <div id="variables-row" style="display: none; padding: 10px 0; font-size: 12px; cursor: pointer; user-select: none;"></div>
`;
  function injectUI(instance) {
    const fileInput = document.querySelector('input[type="file"]');
    if (!fileInput) {
      console.warn("No file input found on page, UI injection aborted");
      return;
    }
    const existingUI = document.getElementById("ggn-upload-templator-ui");
    if (existingUI) {
      existingUI.remove();
    }
    const uiContainer = document.createElement("div");
    uiContainer.id = "ggn-upload-templator-ui";
    uiContainer.innerHTML = MAIN_UI_HTML(instance);
    try {
      fileInput.parentNode.insertBefore(uiContainer, fileInput);
    } catch (error) {
      console.error("Failed to insert UI container:", error);
      return;
    }
    try {
      const createBtn = document.getElementById("create-template-btn");
      const templateSelector = document.getElementById("template-selector");
      const manageBtn = document.getElementById("manage-templates-btn");
      const editBtn = document.getElementById("edit-selected-template-btn");
      const applyBtn = document.getElementById("apply-template-btn");
      if (createBtn) {
        createBtn.addEventListener(
          "click",
          async () => await instance.showTemplateCreator()
        );
      }
      if (templateSelector) {
        templateSelector.addEventListener(
          "change",
          (e) => instance.selectTemplate(e.target.value)
        );
      }
      if (manageBtn) {
        manageBtn.addEventListener(
          "click",
          () => instance.showTemplateAndSettingsManager()
        );
      }
      if (editBtn) {
        editBtn.addEventListener("click", (e) => {
          e.preventDefault();
          instance.editTemplate(instance.selectedTemplate);
        });
      }
      if (applyBtn) {
        applyBtn.addEventListener(
          "click",
          () => instance.applyTemplateToCurrentTorrent()
        );
      }
      const variablesRow = document.getElementById("variables-row");
      if (variablesRow) {
        variablesRow.addEventListener("click", () => {
          instance.showVariablesModal();
        });
      }
    } catch (error) {
      console.error("Failed to bind UI events:", error);
    }
  }
  async function showTemplateCreator(instance, editTemplateName = null, editTemplate = null) {
    const formData = getCurrentFormData(instance.config);
    if (Object.keys(formData).length === 0) {
      alert("No form fields found on this page.");
      return;
    }
    let selectedTorrentName = "";
    let commentVariables = {};
    const fileInputs = instance.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
      `${instance.config.TARGET_FORM_SELECTOR} input[type="file"]`
    ) : document.querySelectorAll('input[type="file"]');
    for (const input of fileInputs) {
      if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
        try {
          const torrentData = await TorrentUtils.parseTorrentFile(input.files[0]);
          selectedTorrentName = torrentData.name || "";
          commentVariables = TorrentUtils.parseCommentVariables(torrentData.comment);
          break;
        } catch (error) {
          console.warn("Could not parse selected torrent file:", error);
        }
      }
    }
    const modal = document.createElement("div");
    modal.className = "gut-modal";
    modal.innerHTML = TEMPLATE_CREATOR_HTML(
      formData,
      instance,
      editTemplateName,
      editTemplate,
      selectedTorrentName
    );
    document.body.appendChild(modal);
    const maskInput = modal.querySelector("#torrent-mask");
    const sampleInput = modal.querySelector("#sample-torrent");
    const templateInputs = modal.querySelectorAll(".template-input");
    const toggleBtn = modal.querySelector("#toggle-unselected");
    const filterInput = modal.querySelector("#field-filter");
    const filterFields = () => {
      const filterValue = filterInput.value.toLowerCase();
      const fieldRows = modal.querySelectorAll(".gut-field-row");
      const fieldList = modal.querySelector(".gut-field-list");
      let visibleCount = 0;
      const existingMessage = fieldList.querySelector(".gut-no-results");
      if (existingMessage) {
        existingMessage.remove();
      }
      fieldRows.forEach((row) => {
        const checkbox = row.querySelector('input[type="checkbox"]');
        const label = row.querySelector("label");
        const fieldName = checkbox.dataset.field.toLowerCase();
        const labelText = label.textContent.toLowerCase();
        const matchesFilter = !filterValue || fieldName.includes(filterValue) || labelText.includes(filterValue);
        const shouldShowBasedOnSelection = checkbox.checked || !instance.hideUnselectedFields;
        const shouldShow = matchesFilter && shouldShowBasedOnSelection;
        if (shouldShow) {
          row.classList.remove("gut-hidden");
          visibleCount++;
        } else {
          row.classList.add("gut-hidden");
        }
      });
      if (filterValue && visibleCount === 0) {
        const noResultsMessage = document.createElement("div");
        noResultsMessage.className = "gut-no-results";
        noResultsMessage.style.cssText = "padding: 20px; text-align: center; color: #888; font-style: italic;";
        noResultsMessage.textContent = `No fields found matching "${filterValue}"`;
        fieldList.appendChild(noResultsMessage);
      }
    };
    const toggleUnselectedFields = () => {
      instance.hideUnselectedFields = !instance.hideUnselectedFields;
      localStorage.setItem(
        "ggn-upload-templator-hide-unselected",
        JSON.stringify(instance.hideUnselectedFields)
      );
      toggleBtn.textContent = instance.hideUnselectedFields ? "Show Unselected" : "Hide Unselected";
      filterFields();
    };
    toggleBtn.textContent = instance.hideUnselectedFields ? "Show Unselected" : "Hide Unselected";
    filterFields();
    toggleBtn.addEventListener("click", toggleUnselectedFields);
    filterInput.addEventListener("input", filterFields);
    const updatePreviews = () => {
      const mask = maskInput.value;
      const sample = sampleInput.value;
      const greedyMatching = modal.querySelector("#greedy-matching").checked;
      const saveButton = modal.querySelector("#save-template");
      const validation = validateMaskVariables(mask);
      const validationWarning = modal.querySelector("#mask-validation-warning");
      if (!validation.valid) {
        validationWarning.classList.add("visible");
        validationWarning.textContent = `Invalid variable names: ${validation.invalidVars.map((v) => `\${${v}}`).join(", ")}. Variable names starting with "_" are reserved for comment variables.`;
        saveButton.disabled = true;
      } else {
        validationWarning.classList.remove("visible");
        saveButton.disabled = false;
      }
      const maskExtracted = parseTemplate(mask, sample, greedyMatching);
      const allVariables = { ...commentVariables, ...maskExtracted };
      const extractedVarsContainer = modal.querySelector("#extracted-variables");
      if (Object.keys(allVariables).length === 0) {
        extractedVarsContainer.innerHTML = '<div class="gut-no-variables">No variables defined yet. Add variables like ${name} to your mask.</div>';
      } else {
        extractedVarsContainer.innerHTML = Object.entries(allVariables).map(
          ([varName, varValue]) => `
            <div class="gut-variable-item">
              <span class="gut-variable-name">\${${escapeHtml(varName)}}</span>
              <span class="gut-variable-value ${varValue ? "" : "empty"}">${varValue ? escapeHtml(varValue) : "(empty)"}</span>
            </div>
          `
        ).join("");
      }
      templateInputs.forEach((input) => {
        const fieldName = input.dataset.template;
        const preview = modal.querySelector(`[data-preview="${fieldName}"]`);
        if (input.type === "checkbox") {
          preview.textContent = input.checked ? "\u2713 checked" : "\u2717 unchecked";
          preview.className = "gut-preview";
        } else if (input.tagName.toLowerCase() === "select") {
          const variableToggle = modal.querySelector(
            `.gut-variable-toggle[data-field="${fieldName}"]`
          );
          const isVariableMode = variableToggle && variableToggle.dataset.state === "on";
          if (isVariableMode) {
            const variableInput = modal.querySelector(
              `.gut-variable-input[data-field="${fieldName}"]`
            );
            const matchTypeSelect = modal.querySelector(
              `.gut-match-type[data-field="${fieldName}"]`
            );
            const variableName = variableInput ? variableInput.value.trim() : "";
            const matchType = matchTypeSelect ? matchTypeSelect.value : "exact";
            if (variableName && allVariables[variableName.replace(/^\$\{|\}$/g, "")]) {
              const variableValue = allVariables[variableName.replace(/^\$\{|\}$/g, "")];
              const matchedOption = findMatchingOption(
                input.options,
                variableValue,
                matchType
              );
              if (matchedOption) {
                preview.textContent = `\u2192 "${matchedOption.text}" (matched "${variableValue}" using ${matchType})`;
                preview.className = "gut-preview active visible";
              } else {
                preview.textContent = `\u2192 No match found for "${variableValue}" using ${matchType}`;
                preview.className = "gut-preview visible";
              }
            } else if (variableName) {
              preview.textContent = `\u2192 Variable ${variableName} not found in extracted data`;
              preview.className = "gut-preview visible";
            } else {
              preview.textContent = "";
              preview.className = "gut-preview";
            }
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
          }
        } else {
          const inputValue = input.value || "";
          const interpolated = interpolate(inputValue, allVariables);
          if (inputValue.includes("${") && Object.keys(allVariables).length > 0) {
            preview.textContent = `\u2192 ${interpolated}`;
            preview.className = "gut-preview active visible";
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
          }
        }
      });
    };
    [maskInput, sampleInput, ...templateInputs].forEach((input) => {
      input.addEventListener("input", updatePreviews);
      input.addEventListener("change", updatePreviews);
    });
    updatePreviews();
    modal.addEventListener("change", (e) => {
      if (e.target.type === "checkbox") {
        if (e.target.id === "greedy-matching") {
          updatePreviews();
        } else {
          filterFields();
        }
      }
    });
    modal.querySelector("#cancel-template").addEventListener("click", () => {
      document.body.removeChild(modal);
    });
    modal.querySelector("#save-template").addEventListener("click", () => {
      instance.saveTemplate(modal, editTemplateName);
    });
    modal.addEventListener("click", (e) => {
      if (e.target === modal) {
        document.body.removeChild(modal);
      }
    });
    const handleEscKey = (e) => {
      if (e.key === "Escape" && document.body.contains(modal)) {
        document.body.removeChild(modal);
        document.removeEventListener("keydown", handleEscKey);
      }
    };
    document.addEventListener("keydown", handleEscKey);
    modal.addEventListener("click", (e) => {
      if (e.target.classList.contains("gut-variable-toggle")) {
        e.preventDefault();
        const fieldName = e.target.dataset.field;
        const currentState = e.target.dataset.state;
        const newState = currentState === "off" ? "on" : "off";
        e.target.dataset.state = newState;
        e.target.textContent = `Match from variable: ${newState.toUpperCase()}`;
        const staticSelect = modal.querySelector(
          `select.select-static-mode[data-template="${fieldName}"]`
        );
        const variableControls = modal.querySelector(
          `.gut-variable-controls[data-field="${fieldName}"]`
        );
        if (newState === "on") {
          staticSelect.classList.add("hidden");
          variableControls.classList.add("visible");
        } else {
          staticSelect.classList.remove("hidden");
          variableControls.classList.remove("visible");
        }
        updatePreviews();
      }
    });
    const variableInputs = modal.querySelectorAll(
      ".gut-variable-input, .gut-match-type"
    );
    variableInputs.forEach((input) => {
      input.addEventListener("input", updatePreviews);
      input.addEventListener("change", updatePreviews);
    });
    const backBtn = modal.querySelector("#back-to-manager");
    if (backBtn) {
      backBtn.addEventListener("click", () => {
        document.body.removeChild(modal);
        instance.showTemplateAndSettingsManager();
      });
    }
  }
  function showVariablesModal(instance, variables) {
    const modal = document.createElement("div");
    modal.className = "gut-modal";
    modal.innerHTML = VARIABLES_MODAL_HTML(variables);
    document.body.appendChild(modal);
    modal.querySelector("#close-variables-modal").addEventListener("click", () => {
      document.body.removeChild(modal);
    });
    modal.addEventListener("click", (e) => {
      if (e.target === modal) {
        document.body.removeChild(modal);
      }
    });
    const handleEscKey = (e) => {
      if (e.key === "Escape" && document.body.contains(modal)) {
        document.body.removeChild(modal);
        document.removeEventListener("keydown", handleEscKey);
      }
    };
    document.addEventListener("keydown", handleEscKey);
  }
  function escapeHtml(text) {
    const div = document.createElement("div");
    div.textContent = text;
    return div.innerHTML;
  }
  function parseKeybinding(keybinding) {
    const parts = keybinding.split("+").map((k) => k.trim().toLowerCase());
    return {
      ctrl: parts.includes("ctrl"),
      meta: parts.includes("cmd") || parts.includes("meta"),
      shift: parts.includes("shift"),
      alt: parts.includes("alt"),
      key: parts.find((k) => !["ctrl", "cmd", "meta", "shift", "alt"].includes(k)) || "enter"
    };
  }
  function matchesKeybinding(event, keys) {
    return event.key.toLowerCase() === keys.key && !!event.ctrlKey === keys.ctrl && !!event.metaKey === keys.meta && !!event.shiftKey === keys.shift && !!event.altKey === keys.alt;
  }
  function buildKeybindingFromEvent(event) {
    const keys = [];
    if (event.ctrlKey) keys.push("Ctrl");
    if (event.metaKey) keys.push("Cmd");
    if (event.shiftKey) keys.push("Shift");
    if (event.altKey) keys.push("Alt");
    keys.push(event.key.charAt(0).toUpperCase() + event.key.slice(1));
    return keys.join("+");
  }
  const style = '#ggn-upload-templator-ui {\n    background: #1a1a1a;\n    border: 1px solid #404040;\n    border-radius: 6px;\n    padding: 15px;\n    margin: 15px 0;\n    font-family:\n        -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;\n    color: #e0e0e0;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n}\n\n.ggn-upload-templator-controls {\n    display: flex;\n    gap: 10px;\n    align-items: center;\n    flex-wrap: wrap;\n}\n\n.gut-btn {\n    padding: 8px 16px;\n    border: none;\n    border-radius: 4px;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: 500;\n    transition: all 0.2s ease;\n    text-decoration: none;\n    outline: none;\n    box-sizing: border-box;\n    height: auto;\n}\n\n.gut-btn-primary {\n    background: #0d7377;\n    color: #ffffff;\n    border: 1px solid #0d7377;\n}\n\n.gut-btn-primary:hover {\n    background: #0a5d61;\n    border-color: #0a5d61;\n    transform: translateY(-1px);\n}\n\n.gut-btn-danger {\n    background: #d32f2f;\n    color: #ffffff;\n    border: 1px solid #d32f2f;\n}\n\n.gut-btn-danger:hover:not(:disabled) {\n    background: #b71c1c;\n    border-color: #b71c1c;\n    transform: translateY(-1px);\n}\n\n.gut-btn:disabled {\n    opacity: 0.5;\n    cursor: not-allowed;\n    transform: none;\n}\n\n.gut-btn:not(:disabled):active {\n    transform: translateY(0);\n}\n\n.gut-select {\n    padding: 8px 12px;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    font-size: 14px;\n    min-width: 200px;\n    background: #2a2a2a;\n    color: #e0e0e0;\n    box-sizing: border-box;\n    outline: none;\n    height: auto;\n    margin: 0 !important;\n}\n\n.gut-select:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 2px rgba(13, 115, 119, 0.2);\n}\n\n.gut-modal {\n    position: fixed;\n    top: 0;\n    left: 0;\n    width: 100%;\n    height: 100%;\n    background: rgba(0, 0, 0, 0.8);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    z-index: 10000;\n    padding: 20px;\n    box-sizing: border-box;\n}\n\n.gut-modal-content {\n    background: #1a1a1a;\n    border: 1px solid #404040;\n    border-radius: 8px;\n    padding: 24px;\n    max-width: 800px;\n    max-height: 80vh;\n    overflow-y: auto;\n    width: 90%;\n    color: #e0e0e0;\n    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5);\n    box-sizing: border-box;\n}\n\n.gut-modal h2 {\n    margin: 0 0 20px 0;\n    color: #ffffff;\n    font-size: 24px;\n    font-weight: 600;\n    text-align: left;\n    position: relative;\n    display: flex;\n    align-items: center;\n    gap: 10px;\n}\n\n.gut-modal-back-btn {\n    background: none;\n    border: none;\n    color: #e0e0e0;\n    font-size: 16px;\n    cursor: pointer;\n    padding: 8px;\n    border-radius: 4px;\n    transition:\n        color 0.2s ease,\n        background-color 0.2s ease;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    width: 40px;\n    height: 40px;\n    flex-shrink: 0;\n    font-family: monospace;\n    font-weight: bold;\n}\n\n.gut-modal-back-btn:hover {\n    color: #ffffff;\n    background-color: #333333;\n}\n\n.gut-form-group {\n    margin-bottom: 15px;\n}\n\n.gut-form-group label {\n    display: block;\n    margin-bottom: 5px;\n    font-weight: 500;\n    color: #b0b0b0;\n    font-size: 14px;\n}\n\n.gut-form-group input,\n.gut-form-group textarea {\n    width: 100%;\n    padding: 8px 12px;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    font-size: 14px;\n    box-sizing: border-box;\n    background: #2a2a2a;\n    color: #e0e0e0;\n    outline: none;\n    transition: border-color 0.2s ease;\n    height: auto;\n}\n\n.gut-form-group input:focus,\n.gut-form-group textarea:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 2px rgba(13, 115, 119, 0.2);\n}\n\n.gut-form-group input::placeholder,\n.gut-form-group textarea::placeholder {\n    color: #666666;\n}\n\n.gut-field-list {\n    max-height: 300px;\n    overflow-y: auto;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    padding: 10px;\n    background: #0f0f0f;\n}\n\n.gut-field-row {\n    display: flex;\n    align-items: center;\n    gap: 10px;\n    margin-bottom: 8px;\n    padding: 8px;\n    background: #2a2a2a;\n    border-radius: 4px;\n    border: 1px solid #404040;\n    flex-wrap: wrap;\n}\n\n.gut-field-row:hover {\n    background: #333333;\n}\n\n.gut-field-row:not(:has(input[type="checkbox"]:checked)) {\n    opacity: 0.6;\n}\n\n.gut-field-row.gut-hidden {\n    display: none;\n}\n\n.gut-field-row input[type="checkbox"] {\n    width: auto;\n    margin: 0;\n    accent-color: #0d7377;\n    cursor: pointer;\n}\n\n.gut-field-row label {\n    min-width: 150px;\n    margin: 0;\n    font-size: 13px;\n    color: #b0b0b0;\n    cursor: help;\n    overflow: hidden;\n    text-overflow: ellipsis;\n    white-space: nowrap;\n}\n\n.gut-field-row input[type="text"],\n.gut-field-row select {\n    flex: 1;\n    margin: 0;\n    padding: 6px 8px;\n    border: 1px solid #404040;\n    border-radius: 3px;\n    background: #1a1a1a;\n    color: #e0e0e0;\n    font-size: 12px;\n    outline: none;\n    height: auto;\n}\n\n.gut-field-row input[type="text"]:focus {\n    border-color: #0d7377;\n    box-shadow: 0 0 0 1px rgba(13, 115, 119, 0.3);\n}\n\n.gut-preview {\n    color: #888888;\n    font-style: italic;\n    font-size: 11px;\n    word-break: break-all;\n    flex-basis: 100%;\n    margin-top: 4px;\n    padding-left: 20px;\n    white-space: pre-wrap;\n    display: none;\n}\n\n.gut-preview.active {\n    color: #4dd0e1;\n    font-weight: bold;\n    font-style: normal;\n}\n\n.gut-preview.visible {\n    display: block;\n}\n\n.gut-modal-actions {\n    display: flex;\n    gap: 10px;\n    justify-content: flex-end;\n    margin-top: 20px;\n    padding-top: 20px;\n    border-top: 1px solid #404040;\n}\n\n.gut-status {\n    position: fixed;\n    top: 20px;\n    right: 20px;\n    background: #2e7d32;\n    color: #ffffff;\n    padding: 12px 20px;\n    border-radius: 6px;\n    z-index: 10001;\n    font-size: 14px;\n    font-weight: 500;\n    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n    border: 1px solid #4caf50;\n    animation: slideInRight 0.3s ease-out;\n}\n\n.gut-status.error {\n    background: #d32f2f;\n    border-color: #f44336;\n}\n\n@keyframes slideInRight {\n    from {\n        transform: translateX(100%);\n        opacity: 0;\n    }\n    to {\n        transform: translateX(0);\n        opacity: 1;\n    }\n}\n\n.gut-template-list {\n    max-height: 400px;\n    overflow-y: auto;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    background: #0f0f0f;\n}\n\n.gut-template-item {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    padding: 12px 16px;\n    border-bottom: 1px solid #404040;\n    background: #2a2a2a;\n    transition: background-color 0.2s ease;\n}\n\n.gut-template-item:hover {\n    background: #333333;\n}\n\n.gut-template-item:last-child {\n    border-bottom: none;\n}\n\n.gut-template-name {\n    font-weight: 500;\n    color: #e0e0e0;\n    flex: 1;\n    margin-right: 10px;\n}\n\n.gut-template-actions {\n    display: flex;\n    gap: 8px;\n}\n\n.gut-btn-small {\n    padding: 6px 12px;\n    font-size: 12px;\n    min-width: auto;\n}\n\n.gut-btn-secondary {\n    background: #555555;\n    color: #ffffff;\n    border: 1px solid #555555;\n}\n\n.gut-btn-secondary:hover:not(:disabled) {\n    background: #666666;\n    border-color: #666666;\n    transform: translateY(-1px);\n}\n\n/* Tab styles for modal */\n.gut-modal-tabs {\n    display: flex;\n    border-bottom: 1px solid #404040;\n    margin-bottom: 20px;\n}\n\n.gut-tab-btn {\n    padding: 12px 20px;\n    background: transparent;\n    border: none;\n    color: #b0b0b0;\n    cursor: pointer;\n    font-size: 14px;\n    font-weight: 500;\n    border-bottom: 2px solid transparent;\n    transition: all 0.2s ease;\n    height: auto;\n}\n\n.gut-tab-btn:hover {\n    color: #e0e0e0;\n    background: #2a2a2a;\n}\n\n.gut-tab-btn.active {\n    color: #ffffff;\n    border-bottom-color: #0d7377;\n}\n\n.gut-tab-content {\n    display: none;\n}\n\n.gut-tab-content.active {\n    display: block;\n}\n\n/* Keybinding controls styling */\n.gut-keybinding-controls {\n    display: flex !important;\n    align-items: center !important;\n    gap: 10px !important;\n    padding: 8px 12px !important;\n    background: #2a2a2a !important;\n    border: 1px solid #404040 !important;\n    border-radius: 4px !important;\n    transition: border-color 0.2s ease !important;\n    margin: 0 !important;\n}\n\n.gut-keybinding-controls:hover {\n    border-color: #0d7377 !important;\n}\n\n/* Checkbox label styling */\n.gut-checkbox-label {\n    display: flex !important;\n    align-items: center !important;\n    gap: 10px !important;\n    cursor: pointer !important;\n    margin: 0 !important;\n}\n\n.gut-checkbox-label input[type="checkbox"] {\n    width: auto !important;\n    margin: 0 !important;\n    accent-color: #0d7377 !important;\n    cursor: pointer !important;\n}\n\n.gut-checkbox-text {\n    font-size: 14px !important;\n    font-weight: 500 !important;\n    color: #b0b0b0 !important;\n    user-select: none !important;\n}\n\n.gut-keybinding-text {\n    color: #4dd0e1 !important;\n    font-family: monospace !important;\n}\n\n.gut-variable-toggle {\n    font-size: 11px !important;\n    padding: 2px 6px !important;\n    white-space: nowrap !important;\n}\n\n/* Scrollbar styling for webkit browsers */\n.gut-field-list::-webkit-scrollbar,\n.gut-modal-content::-webkit-scrollbar {\n    width: 8px;\n}\n\n.gut-field-list::-webkit-scrollbar-track,\n.gut-modal-content::-webkit-scrollbar-track {\n    background: #0f0f0f;\n    border-radius: 4px;\n}\n\n.gut-field-list::-webkit-scrollbar-thumb,\n.gut-modal-content::-webkit-scrollbar-thumb {\n    background: #404040;\n    border-radius: 4px;\n}\n\n.gut-field-list::-webkit-scrollbar-thumb:hover,\n.gut-modal-content::-webkit-scrollbar-thumb:hover {\n    background: #555555;\n}\n\n/* Extracted variables section */\n.gut-extracted-vars {\n    border: 1px solid #404040;\n    border-radius: 4px;\n    background: #0f0f0f;\n    padding: 12px;\n    min-height: 80px;\n    max-height: 300px;\n    overflow-y: auto;\n}\n\n.gut-extracted-vars:has(.gut-no-variables) {\n    display: flex;\n    align-items: center;\n    justify-content: center;\n}\n\n.gut-no-variables {\n    color: #666666;\n    font-style: italic;\n    text-align: center;\n    padding: 20px 10px;\n}\n\n.gut-variable-item {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    padding: 8px 12px;\n    margin-bottom: 6px;\n    background: #2a2a2a;\n    border: 1px solid #404040;\n    border-radius: 4px;\n    transition: background-color 0.2s ease;\n}\n\n.gut-variable-item:last-child {\n    margin-bottom: 0;\n}\n\n.gut-variable-item:hover {\n    background: #333333;\n}\n\n.gut-variable-name {\n    font-weight: 500;\n    color: #4dd0e1;\n    font-family: monospace;\n    font-size: 13px;\n}\n\n.gut-variable-value {\n    color: #e0e0e0;\n    font-size: 12px;\n    max-width: 60%;\n    word-break: break-all;\n    text-align: right;\n}\n\n.gut-variable-value.empty {\n    color: #888888;\n    font-style: italic;\n}\n\n/* Generic hyperlink style for secondary links */\n.gut-link {\n    font-size: 12px !important;\n    color: #b0b0b0 !important;\n    text-decoration: underline !important;\n    text-underline-offset: 2px !important;\n    cursor: pointer !important;\n    transition: color 0.2s ease !important;\n}\n\n.gut-link:hover {\n    color: #4dd0e1 !important;\n}\n\n.gut-variable-toggle {\n    font-size: 11px !important;\n    padding: 2px 6px !important;\n    margin-left: auto !important;\n    align-self: flex-start !important;\n    white-space: nowrap !important;\n}\n\n#variables-row {\n    cursor: pointer;\n    color: #b0b0b0;\n    transition: color 0.2s ease;\n    display: inline-block;\n}\n\n#variables-row:hover {\n    color: #4dd0e1;\n}\n\n#mask-validation-warning {\n    display: none;\n    background: #b63535;\n    color: #ffffff;\n    padding: 10px 12px;\n    border-radius: 4px;\n    margin-top: 8px;\n    font-size: 13px;\n    border: 1px solid #b71c1c;\n}\n\n#mask-validation-warning.visible {\n    display: block;\n}\n\n.gut-variable-controls {\n    display: none;\n    gap: 8px;\n    align-items: center;\n    flex: 1;\n}\n\n.gut-variable-controls.visible {\n    display: flex;\n}\n\n.gut-variable-input {\n    flex: 1;\n    min-width: 120px;\n}\n\n.gut-match-type {\n    min-width: 100px;\n}\n\n.select-static-mode {\n    display: block;\n}\n\n.select-static-mode.hidden {\n    display: none;\n}\n';
  GM_addStyle(style);
  class GGnUploadTemplator {
    constructor() {
      try {
        this.templates = JSON.parse(
          localStorage.getItem("ggn-upload-templator-templates") || "{}"
        );
      } catch (error) {
        console.error("Failed to load templates:", error);
        this.templates = {};
      }
      try {
        this.selectedTemplate = localStorage.getItem("ggn-upload-templator-selected") || null;
      } catch (error) {
        console.error("Failed to load selected template:", error);
        this.selectedTemplate = null;
      }
      try {
        this.hideUnselectedFields = JSON.parse(
          localStorage.getItem("ggn-upload-templator-hide-unselected") || "true"
        );
      } catch (error) {
        console.error("Failed to load hide unselected setting:", error);
        this.hideUnselectedFields = true;
      }
      try {
        this.config = {
          ...DEFAULT_CONFIG,
          ...JSON.parse(
            localStorage.getItem("ggn-upload-templator-settings") || "{}"
          )
        };
      } catch (error) {
        console.error("Failed to load config:", error);
        this.config = { ...DEFAULT_CONFIG };
      }
      logDebug("Initialized core state", {
        templates: Object.keys(this.templates),
        selectedTemplate: this.selectedTemplate,
        hideUnselectedFields: this.hideUnselectedFields,
        config: this.config
      });
      this.init();
    }
    init() {
      logDebug("Initializing...");
      try {
        injectUI(this);
      } catch (error) {
        console.error("UI injection failed:", error);
      }
      try {
        this.watchFileInputs();
      } catch (error) {
        console.error("File input watching setup failed:", error);
      }
      if (this.config.SUBMIT_KEYBINDING) {
        try {
          this.setupSubmitKeybinding();
        } catch (error) {
          console.error("Submit keybinding setup failed:", error);
        }
      }
      if (this.config.APPLY_KEYBINDING) {
        try {
          this.setupApplyKeybinding();
        } catch (error) {
          console.error("Apply keybinding setup failed:", error);
        }
      }
      logDebug("Initialized");
    }
    // Show template creation modal
    async showTemplateCreator(editTemplateName = null, editTemplate = null) {
      await showTemplateCreator(this, editTemplateName, editTemplate);
    }
    // Get current variables (mask + comment)
    async getCurrentVariables() {
      const commentVariables = {};
      const maskVariables = {};
      if (this.selectedTemplate && this.selectedTemplate !== "none") {
        const template = this.templates[this.selectedTemplate];
        if (template) {
          const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
            `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
          ) : document.querySelectorAll('input[type="file"]');
          for (const input of fileInputs) {
            if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
              try {
                const torrentData = await TorrentUtils.parseTorrentFile(
                  input.files[0]
                );
                Object.assign(commentVariables, TorrentUtils.parseCommentVariables(torrentData.comment));
                Object.assign(maskVariables, parseTemplate(
                  template.mask,
                  torrentData.name,
                  template.greedyMatching !== false
                ));
                break;
              } catch (error) {
                console.warn("Could not parse torrent file:", error);
              }
            }
          }
        }
      }
      return {
        all: { ...commentVariables, ...maskVariables },
        comment: commentVariables,
        mask: maskVariables
      };
    }
    // Show variables modal
    async showVariablesModal() {
      const variables = await this.getCurrentVariables();
      showVariablesModal(this, variables.all);
    }
    // Update variable count display
    async updateVariableCount() {
      const variables = await this.getCurrentVariables();
      const commentCount = Object.keys(variables.comment).length;
      const maskCount = Object.keys(variables.mask).length;
      const totalCount = commentCount + maskCount;
      const variablesRow = document.getElementById("variables-row");
      if (variablesRow) {
        if (totalCount === 0) {
          variablesRow.style.display = "none";
        } else {
          variablesRow.style.display = "";
          const parts = [];
          if (commentCount > 0) {
            parts.push(`Comment [${commentCount}]`);
          }
          if (maskCount > 0) {
            parts.push(`Mask [${maskCount}]`);
          }
          variablesRow.innerHTML = `Available variables: ${parts.join(", ")}`;
        }
      }
    }
    // Save template from modal
    saveTemplate(modal, editingTemplateName = null) {
      const name = modal.querySelector("#template-name").value.trim();
      const mask = modal.querySelector("#torrent-mask").value.trim();
      if (!name || !mask) {
        alert("Please provide both template name and torrent mask.");
        return;
      }
      if (editingTemplateName && name !== editingTemplateName && this.templates[name] || !editingTemplateName && this.templates[name]) {
        if (!confirm(`Template "${name}" already exists. Overwrite?`)) {
          return;
        }
      }
      const fieldMappings = {};
      const variableMatchingConfig = {};
      const checkedFields = modal.querySelectorAll(
        '.gut-field-row input[type="checkbox"]:checked'
      );
      checkedFields.forEach((checkbox) => {
        const fieldName = checkbox.dataset.field;
        const templateInput = modal.querySelector(
          `[data-template="${fieldName}"]`
        );
        if (templateInput) {
          if (templateInput.type === "checkbox") {
            fieldMappings[fieldName] = templateInput.checked;
          } else if (templateInput.tagName.toLowerCase() === "select") {
            const variableToggle = modal.querySelector(
              `.gut-variable-toggle[data-field="${fieldName}"]`
            );
            const isVariableMode = variableToggle && variableToggle.dataset.state === "on";
            if (isVariableMode) {
              const variableInput = modal.querySelector(
                `.gut-variable-input[data-field="${fieldName}"]`
              );
              const matchTypeSelect = modal.querySelector(
                `.gut-match-type[data-field="${fieldName}"]`
              );
              variableMatchingConfig[fieldName] = {
                variableName: variableInput ? variableInput.value.trim() : "",
                matchType: matchTypeSelect ? matchTypeSelect.value : "exact"
              };
              fieldMappings[fieldName] = variableInput ? variableInput.value.trim() : "";
            } else {
              fieldMappings[fieldName] = templateInput.value;
            }
          } else {
            fieldMappings[fieldName] = templateInput.value;
          }
        }
      });
      const allFieldRows = modal.querySelectorAll(".gut-field-row");
      const customUnselectedFields = [];
      allFieldRows.forEach((row) => {
        const checkbox = row.querySelector('input[type="checkbox"]');
        if (checkbox) {
          const fieldName = checkbox.dataset.field;
          const isDefaultIgnored = this.config.IGNORED_FIELDS_BY_DEFAULT.includes(
            fieldName.toLowerCase()
          );
          const isCurrentlyChecked = checkbox.checked;
          if (isDefaultIgnored && isCurrentlyChecked || !isDefaultIgnored && !isCurrentlyChecked) {
            customUnselectedFields.push({
              field: fieldName,
              selected: isCurrentlyChecked
            });
          }
        }
      });
      if (editingTemplateName && name !== editingTemplateName) {
        delete this.templates[editingTemplateName];
        if (this.selectedTemplate === editingTemplateName) {
          this.selectedTemplate = name;
          localStorage.setItem("ggn-upload-templator-selected", name);
        }
      }
      const greedyMatching = modal.querySelector("#greedy-matching").checked;
      this.templates[name] = {
        mask,
        fieldMappings,
        greedyMatching,
        customUnselectedFields: customUnselectedFields.length > 0 ? customUnselectedFields : void 0,
        variableMatching: Object.keys(variableMatchingConfig).length > 0 ? variableMatchingConfig : void 0
      };
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      this.updateTemplateSelector();
      this.updateVariableCount();
      const action = editingTemplateName ? "updated" : "saved";
      this.showStatus(`Template "${name}" ${action} successfully!`);
      document.body.removeChild(modal);
    }
    // Update template selector dropdown
    updateTemplateSelector() {
      const selector = document.getElementById("template-selector");
      if (!selector) return;
      selector.innerHTML = TEMPLATE_SELECTOR_HTML(this);
      this.updateEditButtonVisibility();
    }
    // Update edit button visibility based on selected template
    updateEditButtonVisibility() {
      const editBtn = document.getElementById("edit-selected-template-btn");
      if (!editBtn) return;
      const shouldShow = this.selectedTemplate && this.selectedTemplate !== "none" && this.templates[this.selectedTemplate];
      editBtn.style.display = shouldShow ? "" : "none";
    }
    // Select template
    selectTemplate(templateName) {
      this.selectedTemplate = templateName || null;
      if (templateName) {
        localStorage.setItem("ggn-upload-templator-selected", templateName);
      } else {
        localStorage.removeItem("ggn-upload-templator-selected");
      }
      this.updateEditButtonVisibility();
      this.updateVariableCount();
      if (templateName === "none") {
        this.showStatus("No template selected - auto-fill disabled");
      } else if (templateName) {
        this.showStatus(`Template "${templateName}" selected`);
        this.checkAndApplyToExistingTorrent(templateName);
      }
    }
    // Apply template to form
    applyTemplate(templateName, torrentName, commentVariables = {}) {
      const template = this.templates[templateName];
      if (!template) return;
      const extracted = parseTemplate(
        template.mask,
        torrentName,
        template.greedyMatching !== false
      );
      let appliedCount = 0;
      Object.entries(template.fieldMappings).forEach(
        ([fieldName, valueTemplate]) => {
          const firstElement = findElementByFieldName(fieldName, this.config);
          if (firstElement && firstElement.type === "radio") {
            const formPrefix = this.config.TARGET_FORM_SELECTOR ? `${this.config.TARGET_FORM_SELECTOR} ` : "";
            const radioButtons = document.querySelectorAll(
              `${formPrefix}input[name="${fieldName}"][type="radio"]`
            );
            const newValue = interpolate(String(valueTemplate), extracted, commentVariables);
            radioButtons.forEach((radio) => {
              if (radio.hasAttribute("disabled")) {
                radio.removeAttribute("disabled");
              }
              const shouldBeChecked = radio.value === newValue;
              if (shouldBeChecked !== radio.checked) {
                radio.checked = shouldBeChecked;
                if (shouldBeChecked) {
                  radio.dispatchEvent(new Event("input", { bubbles: true }));
                  radio.dispatchEvent(new Event("change", { bubbles: true }));
                  appliedCount++;
                }
              }
            });
          } else if (firstElement) {
            if (firstElement.hasAttribute("disabled")) {
              firstElement.removeAttribute("disabled");
            }
            if (firstElement.type === "checkbox") {
              let newChecked;
              if (typeof valueTemplate === "boolean") {
                newChecked = valueTemplate;
              } else {
                const interpolated = interpolate(
                  String(valueTemplate),
                  extracted,
                  commentVariables
                );
                newChecked = /^(true|1|yes|on)$/i.test(interpolated);
              }
              if (newChecked !== firstElement.checked) {
                firstElement.checked = newChecked;
                firstElement.dispatchEvent(new Event("input", { bubbles: true }));
                firstElement.dispatchEvent(
                  new Event("change", { bubbles: true })
                );
                appliedCount++;
              }
            } else {
              const interpolated = interpolate(String(valueTemplate), extracted, commentVariables);
              if (firstElement.value !== interpolated) {
                firstElement.value = interpolated;
                firstElement.dispatchEvent(new Event("input", { bubbles: true }));
                firstElement.dispatchEvent(
                  new Event("change", { bubbles: true })
                );
                appliedCount++;
              }
            }
          }
        }
      );
      if (appliedCount > 0) {
        this.showStatus(
          `Template "${templateName}" applied to ${appliedCount} field(s)`
        );
      }
    }
    // Check for existing torrent file and apply template
    async checkAndApplyToExistingTorrent(templateName) {
      if (!templateName || templateName === "none") return;
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      for (const input of fileInputs) {
        if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
          try {
            const torrentData = await TorrentUtils.parseTorrentFile(
              input.files[0]
            );
            const commentVariables = TorrentUtils.parseCommentVariables(torrentData.comment);
            this.applyTemplate(templateName, torrentData.name, commentVariables);
            return;
          } catch (error) {
            console.warn("Could not parse existing torrent file:", error);
          }
        }
      }
    }
    // Watch file inputs for changes (no auto-application)
    watchFileInputs() {
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      fileInputs.forEach((input) => {
        input.addEventListener("change", (e) => {
          if (e.target.files[0] && e.target.files[0].name.toLowerCase().endsWith(".torrent")) {
            this.showStatus(
              "Torrent file selected. Click 'Apply Template' to fill form."
            );
            this.updateVariableCount();
          }
        });
      });
    }
    // Apply template to the currently selected torrent file
    async applyTemplateToCurrentTorrent() {
      if (!this.selectedTemplate || this.selectedTemplate === "none") {
        this.showStatus("No template selected", "error");
        return;
      }
      const fileInputs = this.config.TARGET_FORM_SELECTOR ? document.querySelectorAll(
        `${this.config.TARGET_FORM_SELECTOR} input[type="file"]`
      ) : document.querySelectorAll('input[type="file"]');
      for (const input of fileInputs) {
        if (input.files && input.files[0] && input.files[0].name.toLowerCase().endsWith(".torrent")) {
          try {
            const torrentData = await TorrentUtils.parseTorrentFile(
              input.files[0]
            );
            const commentVariables = TorrentUtils.parseCommentVariables(torrentData.comment);
            this.applyTemplate(this.selectedTemplate, torrentData.name, commentVariables);
            return;
          } catch (error) {
            console.error("Error processing torrent file:", error);
            this.showStatus("Error processing torrent file", "error");
          }
        }
      }
      this.showStatus("No torrent file selected", "error");
    }
    // Setup global keybinding for form submission
    setupSubmitKeybinding() {
      const keybinding = this.config.CUSTOM_SUBMIT_KEYBINDING || "Ctrl+Enter";
      const keys = parseKeybinding(keybinding);
      document.addEventListener("keydown", (e) => {
        if (matchesKeybinding(e, keys)) {
          e.preventDefault();
          const targetForm = document.querySelector(
            this.config.TARGET_FORM_SELECTOR
          );
          if (targetForm) {
            const submitButton = targetForm.querySelector(
              'input[type="submit"], button[type="submit"]'
            ) || targetForm.querySelector(
              'input[name*="submit"], button[name*="submit"]'
            ) || targetForm.querySelector(".submit-btn, #submit-btn");
            if (submitButton) {
              this.showStatus(`Form submitted via ${keybinding}`);
              submitButton.click();
            } else {
              this.showStatus(`Form submitted via ${keybinding}`);
              targetForm.submit();
            }
          }
        }
      });
    }
    // Setup global keybinding for applying template
    setupApplyKeybinding() {
      const keybinding = this.config.CUSTOM_APPLY_KEYBINDING || "Ctrl+Shift+A";
      const keys = parseKeybinding(keybinding);
      document.addEventListener("keydown", (e) => {
        if (matchesKeybinding(e, keys)) {
          e.preventDefault();
          this.applyTemplateToCurrentTorrent();
        }
      });
    }
    // Show combined template and settings manager modal
    showTemplateAndSettingsManager() {
      const modal = document.createElement("div");
      modal.className = "gut-modal";
      modal.innerHTML = MODAL_HTML(this);
      document.body.appendChild(modal);
      modal.querySelectorAll(".gut-tab-btn").forEach((btn) => {
        btn.addEventListener("click", (e) => {
          const tabName = e.target.dataset.tab;
          modal.querySelectorAll(".gut-tab-btn").forEach((b) => b.classList.remove("active"));
          e.target.classList.add("active");
          modal.querySelectorAll(".gut-tab-content").forEach((c) => c.classList.remove("active"));
          modal.querySelector(`#${tabName}-tab`).classList.add("active");
        });
      });
      const customSelectorsTextarea = modal.querySelector(
        "#setting-custom-selectors"
      );
      const previewGroup = modal.querySelector("#custom-selectors-preview-group");
      const matchedContainer = modal.querySelector("#custom-selectors-matched");
      const updateCustomSelectorsPreview = () => {
        const selectorsText = customSelectorsTextarea.value.trim();
        const selectors = selectorsText.split("\n").map((selector) => selector.trim()).filter((selector) => selector);
        const originalSelectors = this.config.CUSTOM_FIELD_SELECTORS;
        this.config.CUSTOM_FIELD_SELECTORS = selectors;
        if (selectors.length === 0) {
          previewGroup.style.display = "none";
          this.config.CUSTOM_FIELD_SELECTORS = originalSelectors;
          return;
        }
        previewGroup.style.display = "block";
        let matchedElements = [];
        const formSelector = modal.querySelector("#setting-form-selector").value.trim() || this.config.TARGET_FORM_SELECTOR;
        const targetForm = document.querySelector(formSelector);
        selectors.forEach((selector) => {
          try {
            const elements = targetForm ? targetForm.querySelectorAll(selector) : document.querySelectorAll(selector);
            Array.from(elements).forEach((element) => {
              const tagName = element.tagName.toLowerCase();
              const id = element.id;
              const name = element.name || element.getAttribute("name");
              const classes = element.className || "";
              const label = getFieldLabel(element, this.config);
              const elementId = element.id || element.name || `${tagName}-${Array.from(element.parentNode.children).indexOf(element)}`;
              if (!matchedElements.find((e) => e.elementId === elementId)) {
                matchedElements.push({
                  elementId,
                  element,
                  tagName,
                  id,
                  name,
                  classes,
                  label,
                  selector
                });
              }
            });
          } catch (e) {
            console.warn(`Invalid custom selector: ${selector}`, e);
          }
        });
        const matchedElementsLabel = modal.querySelector(
          "#matched-elements-label"
        );
        if (matchedElements.length === 0) {
          matchedElementsLabel.textContent = "Matched Elements:";
          matchedContainer.innerHTML = '<div class="gut-no-variables">No elements matched by custom selectors.</div>';
        } else {
          matchedElementsLabel.textContent = `Matched Elements (${matchedElements.length}):`;
          matchedContainer.innerHTML = matchedElements.map((item) => {
            const displayName = item.label || item.name || item.id || `${item.tagName}`;
            const displayInfo = [
              item.tagName.toUpperCase(),
              item.id ? `#${item.id}` : "",
              item.name ? `name="${item.name}"` : "",
              item.classes ? `.${item.classes.split(" ").filter((c) => c).join(".")}` : ""
            ].filter((info) => info).join(" ");
            return `
              <div class="gut-variable-item">
                <span class="gut-variable-name">${this.escapeHtml(displayName)}</span>
                <span class="gut-variable-value">${this.escapeHtml(displayInfo)}</span>
              </div>
            `;
          }).join("");
        }
        this.config.CUSTOM_FIELD_SELECTORS = originalSelectors;
      };
      updateCustomSelectorsPreview();
      customSelectorsTextarea.addEventListener(
        "input",
        updateCustomSelectorsPreview
      );
      modal.querySelector("#setting-form-selector").addEventListener("input", updateCustomSelectorsPreview);
      modal.querySelector("#ggn-infobox-link")?.addEventListener("click", (e) => {
        e.preventDefault();
        const currentValue = customSelectorsTextarea.value.trim();
        const ggnInfoboxSelector = ".infobox-input-holder input";
        if (!currentValue.includes(ggnInfoboxSelector)) {
          const newValue = currentValue ? `${currentValue}
${ggnInfoboxSelector}` : ggnInfoboxSelector;
          customSelectorsTextarea.value = newValue;
          updateCustomSelectorsPreview();
        }
      });
      modal.querySelector("#save-settings")?.addEventListener("click", () => {
        this.saveSettings(modal);
      });
      modal.querySelector("#reset-settings")?.addEventListener("click", () => {
        if (confirm(
          "Reset all settings to defaults? This will require a page reload."
        )) {
          this.resetSettings(modal);
        }
      });
      modal.querySelector("#delete-all-config")?.addEventListener("click", () => {
        if (confirm(
          "\u26A0\uFE0F WARNING: This will permanently delete ALL GGn Upload Templator data including templates, settings, and selected template.\n\nThis action CANNOT be undone!\n\nAre you sure you want to continue?"
        )) {
          this.deleteAllConfig();
        }
      });
      let isRecording = false;
      const setupRecordKeybindingHandler = (inputSelector, keybindingSpanIndex, recordBtnSelector) => {
        modal.querySelector(recordBtnSelector)?.addEventListener("click", () => {
          const input = modal.querySelector(inputSelector);
          const keybindingSpans = modal.querySelectorAll(".gut-keybinding-text");
          const keybindingSpan = keybindingSpans[keybindingSpanIndex];
          const recordBtn = modal.querySelector(recordBtnSelector);
          recordBtn.textContent = "Press keys...";
          recordBtn.disabled = true;
          isRecording = true;
          const handleKeydown = (e) => {
            e.preventDefault();
            const isModifierKey = ["Control", "Alt", "Shift", "Meta"].includes(
              e.key
            );
            if (e.key === "Escape") {
              recordBtn.textContent = "Record";
              recordBtn.disabled = false;
              isRecording = false;
              document.removeEventListener("keydown", handleKeydown);
              return;
            }
            if (!isModifierKey) {
              const keybinding = buildKeybindingFromEvent(e);
              input.value = keybinding;
              if (keybindingSpan) {
                keybindingSpan.textContent = keybinding;
              }
              recordBtn.textContent = "Record";
              recordBtn.disabled = false;
              isRecording = false;
              document.removeEventListener("keydown", handleKeydown);
            }
          };
          document.addEventListener("keydown", handleKeydown);
        });
      };
      setupRecordKeybindingHandler(
        "#custom-submit-keybinding-input",
        0,
        "#record-submit-keybinding-btn"
      );
      setupRecordKeybindingHandler(
        "#custom-apply-keybinding-input",
        1,
        "#record-apply-keybinding-btn"
      );
      modal.addEventListener("click", (e) => {
        if (e.target === modal) {
          document.body.removeChild(modal);
          return;
        }
        const action = e.target.dataset.action;
        const templateName = e.target.dataset.template;
        if (action && templateName) {
          switch (action) {
            case "edit":
              document.body.removeChild(modal);
              this.editTemplate(templateName);
              break;
            case "clone":
              this.cloneTemplate(templateName);
              this.refreshTemplateManager(modal);
              break;
            case "delete":
              if (confirm(`Delete template "${templateName}"?`)) {
                this.deleteTemplate(templateName);
                this.refreshTemplateManager(modal);
              }
              break;
          }
        }
      });
      modal.querySelector("#close-manager").addEventListener("click", () => {
        document.body.removeChild(modal);
      });
      const handleEscKey = (e) => {
        if (e.key === "Escape" && document.body.contains(modal) && !isRecording) {
          document.body.removeChild(modal);
          document.removeEventListener("keydown", handleEscKey);
        }
      };
      document.addEventListener("keydown", handleEscKey);
    }
    // Save settings from modal
    saveSettings(modal) {
      const formSelector = modal.querySelector("#setting-form-selector").value.trim();
      const submitKeybinding = modal.querySelector(
        "#setting-submit-keybinding"
      ).checked;
      const customSubmitKeybinding = modal.querySelector("#custom-submit-keybinding-input").value.trim();
      const applyKeybinding = modal.querySelector(
        "#setting-apply-keybinding"
      ).checked;
      const customApplyKeybinding = modal.querySelector("#custom-apply-keybinding-input").value.trim();
      const customSelectorsText = modal.querySelector("#setting-custom-selectors").value.trim();
      const customSelectors = customSelectorsText.split("\n").map((selector) => selector.trim()).filter((selector) => selector);
      const ignoredFieldsText = modal.querySelector("#setting-ignored-fields").value.trim();
      const ignoredFields = ignoredFieldsText.split("\n").map((field) => field.trim()).filter((field) => field);
      this.config = {
        TARGET_FORM_SELECTOR: formSelector || DEFAULT_CONFIG.TARGET_FORM_SELECTOR,
        SUBMIT_KEYBINDING: submitKeybinding,
        CUSTOM_SUBMIT_KEYBINDING: customSubmitKeybinding || DEFAULT_CONFIG.CUSTOM_SUBMIT_KEYBINDING,
        APPLY_KEYBINDING: applyKeybinding,
        CUSTOM_APPLY_KEYBINDING: customApplyKeybinding || DEFAULT_CONFIG.CUSTOM_APPLY_KEYBINDING,
        CUSTOM_FIELD_SELECTORS: customSelectors.length > 0 ? customSelectors : DEFAULT_CONFIG.CUSTOM_FIELD_SELECTORS,
        IGNORED_FIELDS_BY_DEFAULT: ignoredFields.length > 0 ? ignoredFields : DEFAULT_CONFIG.IGNORED_FIELDS_BY_DEFAULT
      };
      localStorage.setItem(
        "ggn-upload-templator-settings",
        JSON.stringify(this.config)
      );
      this.showStatus(
        "Settings saved successfully! Reload the page for some changes to take effect."
      );
    }
    // Reset settings to defaults
    resetSettings(modal) {
      localStorage.removeItem("ggn-upload-templator-settings");
      this.config = { ...DEFAULT_CONFIG };
      modal.querySelector("#setting-form-selector").value = this.config.TARGET_FORM_SELECTOR;
      modal.querySelector("#setting-submit-keybinding").checked = this.config.SUBMIT_KEYBINDING;
      modal.querySelector("#custom-submit-keybinding-input").value = this.config.CUSTOM_SUBMIT_KEYBINDING;
      modal.querySelector("#setting-apply-keybinding").checked = this.config.APPLY_KEYBINDING;
      modal.querySelector("#custom-apply-keybinding-input").value = this.config.CUSTOM_APPLY_KEYBINDING;
      modal.querySelector("#setting-custom-selectors").value = this.config.CUSTOM_FIELD_SELECTORS.join("\n");
      modal.querySelector("#setting-ignored-fields").value = this.config.IGNORED_FIELDS_BY_DEFAULT.join("\n");
      const submitKeybindingSpan = modal.querySelector(".gut-keybinding-text");
      submitKeybindingSpan.textContent = this.config.CUSTOM_SUBMIT_KEYBINDING;
      const applyKeybindingSpans = modal.querySelectorAll(".gut-keybinding-text");
      if (applyKeybindingSpans.length > 1) {
        applyKeybindingSpans[1].textContent = this.config.CUSTOM_APPLY_KEYBINDING;
      }
      this.showStatus(
        "Settings reset to defaults! Reload the page for changes to take effect."
      );
    }
    // Delete all local configuration
    deleteAllConfig() {
      localStorage.removeItem("ggn-upload-templator-templates");
      localStorage.removeItem("ggn-upload-templator-selected");
      localStorage.removeItem("ggn-upload-templator-hide-unselected");
      localStorage.removeItem("ggn-upload-templator-settings");
      this.templates = {};
      this.selectedTemplate = null;
      this.hideUnselectedFields = true;
      this.config = { ...DEFAULT_CONFIG };
      this.updateTemplateSelector();
      this.showStatus(
        "All local configuration deleted! Reload the page for changes to take full effect.",
        "success"
      );
    }
    // Delete template by name
    deleteTemplate(templateName) {
      delete this.templates[templateName];
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      if (this.selectedTemplate === templateName) {
        this.selectedTemplate = null;
        localStorage.removeItem("ggn-upload-templator-selected");
      }
      this.updateTemplateSelector();
      this.showStatus(`Template "${templateName}" deleted`);
    }
    // Clone template
    cloneTemplate(templateName) {
      const originalTemplate = this.templates[templateName];
      if (!originalTemplate) return;
      const cloneName = `${templateName} (Clone)`;
      this.templates[cloneName] = {
        mask: originalTemplate.mask,
        fieldMappings: { ...originalTemplate.fieldMappings },
        customUnselectedFields: originalTemplate.customUnselectedFields ? [...originalTemplate.customUnselectedFields] : void 0
      };
      localStorage.setItem(
        "ggn-upload-templator-templates",
        JSON.stringify(this.templates)
      );
      this.updateTemplateSelector();
      this.showStatus(`Template "${cloneName}" created`);
    }
    // Edit template
    editTemplate(templateName) {
      const template = this.templates[templateName];
      if (!template) return;
      this.showTemplateCreator(templateName, template);
    }
    // Refresh template manager modal content
    refreshTemplateManager(modal) {
      const templateList = modal.querySelector(".gut-template-list");
      if (!templateList) return;
      templateList.innerHTML = TEMPLATE_LIST_HTML(this);
    }
    // Show status message
    showStatus(message, type = "success") {
      const existing = document.querySelector(".gut-status");
      if (existing) existing.remove();
      const status = document.createElement("div");
      status.className = "gut-status";
      status.textContent = message;
      if (type === "error") {
        status.classList.add("error");
      }
      document.body.appendChild(status);
      setTimeout(() => {
        if (status.parentNode) {
          status.parentNode.removeChild(status);
        }
      }, 3e3);
    }
    // Escape HTML to prevent XSS
    escapeHtml(text) {
      const div = document.createElement("div");
      div.textContent = text;
      return div.innerHTML;
    }
  }
  logDebug("Script loaded (readyState:", document.readyState, ")");
  if (document.readyState === "loading") {
    document.addEventListener("DOMContentLoaded", () => {
      logDebug("Initializing after DOMContentLoaded");
      try {
        new GGnUploadTemplator();
      } catch (error) {
        console.error("Failed to initialize:", error);
      }
    });
  } else {
    logDebug("Initializing immediately (DOM already ready)");
    try {
      new GGnUploadTemplator();
    } catch (error) {
      console.error("Failed to initialize:", error);
    }
  }
})();