GGn Upload Templator

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

Pada tanggal 29 September 2025. Lihat %(latest_version_link).

// ==UserScript==
// @name        GGn Upload Templator
// @version     0.5
// @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
// @icon        https://gazellegames.net/favicon.ico
// @match       https://*.gazellegames.net/upload.php*
// @grant       GM_addStyle
// @namespace http://tampermonkey.net/
// ==/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,
          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, files: [] };
      }
    }
    // 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 interpolate(template, data) {
    if (!template || !data) return template;
    return template.replace(/\$\{([^}]+)\}/g, (match, key) => data[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 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>

    <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>` : `<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 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>
`;
  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()
        );
      }
    } 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 = "";
    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 || "";
          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 extracted = parseTemplate(mask, sample, greedyMatching);
      const extractedVarsContainer = modal.querySelector("#extracted-variables");
      if (Object.keys(extracted).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(extracted).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 && extracted[variableName.replace(/^\$\{|\}$/g, "")]) {
              const variableValue = extracted[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";
                preview.style.display = "block";
              } else {
                preview.textContent = `\u2192 No match found for "${variableValue}" using ${matchType}`;
                preview.className = "gut-preview";
                preview.style.display = "block";
              }
            } else if (variableName) {
              preview.textContent = `\u2192 Variable ${variableName} not found in extracted data`;
              preview.className = "gut-preview";
              preview.style.display = "block";
            } else {
              preview.textContent = "";
              preview.className = "gut-preview";
              preview.style.display = "none";
            }
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
            preview.style.display = "none";
          }
        } else {
          const inputValue = input.value || "";
          const interpolated = interpolate(inputValue, extracted);
          if (inputValue.includes("${") && Object.keys(extracted).length > 0) {
            preview.textContent = `\u2192 ${interpolated}`;
            preview.className = "gut-preview active";
            preview.style.display = "block";
          } else {
            preview.textContent = "";
            preview.className = "gut-preview";
            preview.style.display = "none";
          }
        }
      });
    };
    [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.style.display = "none";
          variableControls.style.display = "flex";
        } else {
          staticSelect.style.display = "block";
          variableControls.style.display = "none";
        }
        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 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: -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}\n\n.gut-preview.active {\n  color: #4dd0e1;\n  font-weight: bold;\n  font-style: normal;\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';
  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);
    }
    // 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();
      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();
      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) {
      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);
            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
                );
                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);
              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]
            );
            this.applyTemplate(templateName, torrentData.name);
            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."
            );
          }
        });
      });
    }
    // 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]
            );
            this.applyTemplate(this.selectedTemplate, torrentData.name);
            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);
    }
  }
})();