Greasy Fork is available in English.

Auto Close Window

Automatically close browser windows/tabs when specific text patterns are detected. Useful for OAuth callbacks, AWS SSO, and temporary pages.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

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

(I already have a user script manager, let me install it!)

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.

ستحتاج إلى تثبيت إضافة مثل Stylus لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتتمكن من تثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

ستحتاج إلى تثبيت إضافة لإدارة أنماط المستخدم لتثبيت هذا النمط.

(لدي بالفعل مثبت أنماط للمستخدم، دعني أقم بتثبيته!)

// ==UserScript==
// @name         Auto Close Window
// @version      1.6
// @description  Automatically close browser windows/tabs when specific text patterns are detected. Useful for OAuth callbacks, AWS SSO, and temporary pages.
// @namespace    https://github.com/ofryl/userscripts
// @author       Ofry Linkovsky <[email protected]>
// @license      MIT
// @icon         data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiM0Q0FGNTAiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48Y2lyY2xlIGN4PSIxMiIgY3k9IjEyIiByPSIxMCIvPjxwb2x5bGluZSBwb2ludHM9IjEyIDYgMTIgMTIgMTYgMTQiLz48L3N2Zz4=
// @match        *://*/*
// @run-at       document-start
// @noframes
// @grant        window.close
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_registerMenuCommand
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.registerMenuCommand
// ==/UserScript==

/*
 * MIT License
 *
 * Copyright (c) 2025 Ofry Linkovsky
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

// Requires Firefox about:config: dom.allow_scripts_to_close_windows = true

(function() {
  'use strict';

  // ============================================================================
  // ENUMS
  // ============================================================================
  const Enum = {
    Theme: { AUTO: 'auto', LIGHT: 'light', DARK: 'dark' },
    SaveState: { SAVED: 'saved', SAVING: 'saving', UNSAVED: 'unsaved' },
    Tab: { PATTERNS: 'patterns', CONFIG: 'config', ADVANCED: 'advanced', CREDITS: 'credits' },
    ElementId: {
      DIALOG: 'auto-close-config-dialog',
      PATTERNS_TEXTAREA: 'auto-close-patterns',
      LINE_NUMBERS: 'auto-close-line-numbers',
      RESET_BTN: 'auto-close-reset',
      CLOSE_BTN: 'auto-close-x',
      THEME_BTN: 'auto-close-theme',
      DEBUG_CHECKBOX: 'auto-close-debug',
      ABORT_CHECKBOX: 'auto-close-show-abort',
      COUNTDOWN_INPUT: 'auto-close-countdown',
      DELAY_INPUT: 'auto-close-delay',
      COUNTDOWN_DISPLAY: 'auto-close-countdown-display',
      OVERLAY: 'auto-close-overlay',
      ABORT_BTN: 'auto-close-abort-btn'
    }
  };

  // ============================================================================
  // COMMON - Shared utilities and state
  // ============================================================================
  const Common = {
    CONFIG: {
      STORAGE_KEY: 'autoCloseStrings',
      DEBUG_KEY: 'autoCloseDebug',
      COUNTDOWN_KEY: 'autoCloseCountdown',
      DELAY_KEY: 'autoCloseDelay',
      ABORT_KEY: 'autoCloseShowAbort',
      THEME_KEY: 'autoCloseTheme',
      SAVE_INDICATOR_DELAY_MS: 400,
      AUTOSAVE_DEBOUNCE_MS: 800,
      DEFAULTS: {
        COUNTDOWN_SECONDS: 5,
        DETECTION_DELAY_MS: 1000,
        SHOW_ABORT_BUTTON: false,
        THEME: Enum.Theme.AUTO
      }
    },

    state: {
      debugEnabled: false,
      showAbortButton: false,
      theme: Enum.Theme.AUTO,
      detectionPatterns: [],
      settings: {
        countdownSeconds: 5,
        detectionDelayMs: 1000
      }
    },

    DEFAULT_PATTERNS: [
      "You can close this window and start using the AWS CLI.",
      "Authentication details received, processing details. You may close this window at any time.",
      "You can close this window",
      "We couldn't complete your request right now. Please try again later."
    ],

    Storage: {
      getValue: null,
      setValue: null,
      registerMenuCommand: null,

      init() {
        this.getValue = Common.createStorageMethod('getValue', (key, defaultValue) => defaultValue);
        this.setValue = Common.createStorageMethod('setValue', () => {});
        this.registerMenuCommand = Common.createStorageMethod('registerMenuCommand', () => {});
      }
    },

    createStorageMethod(methodName, fallback) {
      const gmUnderscore = window[`GM_${methodName}`];
      if (typeof gmUnderscore !== 'undefined') {
        return gmUnderscore;
      }

      const gmDot = typeof GM !== 'undefined' && GM[methodName];
      if (gmDot) {
        return (...args) => GM[methodName](...args);
      }

      return fallback;
    },

    Debug: {
      log(...args) {
        if (Common.state.debugEnabled) {
          console.log('[AutoClose]', ...args);
        }
      }
    },

    Theme: {
      COLORS: {
        [Enum.Theme.DARK]: {
          bg: '#1a1a1a',
          bgAlt: '#252525',
          bgEditor: '#0d0d0d',
          bgLineNumbers: '#151515',
          text: '#ffffff',
          textMuted: '#aaaaaa',
          textDim: '#888888',
          textLineNumbers: '#555555',
          border: '#444444',
          borderDim: '#333333',
          accent: '#4CAF50',
          btnPrimary: '#4CAF50',
          btnSecondary: '#444444',
          btnTertiary: '#666666',
          code: '#333333'
        },
        [Enum.Theme.LIGHT]: {
          bg: '#ffffff',
          bgAlt: '#f5f5f5',
          bgEditor: '#fafafa',
          bgLineNumbers: '#f0f0f0',
          text: '#1a1a1a',
          textMuted: '#666666',
          textDim: '#888888',
          textLineNumbers: '#999999',
          border: '#dddddd',
          borderDim: '#eeeeee',
          accent: '#2e7d32',
          btnPrimary: '#2e7d32',
          btnSecondary: '#666666',
          btnTertiary: '#888888',
          code: '#e8e8e8'
        }
      },

      ICONS: {
        [Enum.Theme.AUTO]: '◐',
        [Enum.Theme.LIGHT]: '☀',
        [Enum.Theme.DARK]: '☾'
      },

      systemPrefersDark() {
        return window.matchMedia('(prefers-color-scheme: dark)').matches;
      },

      isDark() {
        const theme = Common.state.theme;
        if (theme === Enum.Theme.AUTO) {
          return this.systemPrefersDark();
        }
        return theme === Enum.Theme.DARK;
      },

      get() {
        return this.COLORS[this.isDark() ? Enum.Theme.DARK : Enum.Theme.LIGHT];
      },

      getIcon() {
        return this.ICONS[Common.state.theme] || this.ICONS[Enum.Theme.AUTO];
      },

      async toggle() {
        const modes = [Enum.Theme.AUTO, Enum.Theme.LIGHT, Enum.Theme.DARK];
        const currentIndex = modes.indexOf(Common.state.theme);
        const nextIndex = (currentIndex + 1) % modes.length;
        Common.state.theme = modes[nextIndex];
        await Promise.resolve(Common.Storage.setValue(Common.CONFIG.THEME_KEY, Common.state.theme));
        return Common.state.theme;
      }
    },

    init() {
      this.Storage.init();
      this.state.detectionPatterns = [...this.DEFAULT_PATTERNS];
      this.state.settings.countdownSeconds = this.CONFIG.DEFAULTS.COUNTDOWN_SECONDS;
      this.state.settings.detectionDelayMs = this.CONFIG.DEFAULTS.DETECTION_DELAY_MS;
    }
  };

  // ============================================================================
  // UI - Configuration interface
  // ============================================================================
  const UI = {
    Styles: {
      get() {
        const t = Common.Theme.get();
        return {
          overlay: `
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: ${t.bg}; z-index: 999999; display: flex; flex-direction: column;
            font-family: sans-serif; color: ${t.text};
          `,
          header: `
            display: flex; justify-content: space-between; align-items: center;
            padding: 15px 25px; background: ${t.bgAlt}; border-bottom: 1px solid ${t.border};
          `,
          tabs: `
            display: flex; gap: 0; background: ${t.bgAlt}; border-bottom: 1px solid ${t.border};
          `,
          tab: `
            padding: 12px 25px; cursor: pointer; border: none; background: transparent;
            color: ${t.textDim}; font-size: 14px; border-bottom: 2px solid transparent;
          `,
          tabActive: `
            padding: 12px 25px; cursor: pointer; border: none; background: ${t.bg};
            color: ${t.text}; font-size: 14px; border-bottom: 2px solid ${t.accent};
          `,
          content: `
            flex: 1; padding: 25px; overflow-y: auto; display: flex; flex-direction: column;
            width: 100%; box-sizing: border-box;
          `,
          button: `
            padding: 10px 20px; color: #fff; border: none; border-radius: 4px;
            cursor: pointer; font-size: 14px;
          `,
          closeButton: `
            background: transparent; border: none; color: ${t.textDim}; font-size: 28px;
            cursor: pointer; padding: 0; line-height: 1;
          `,
          themeToggle: `
            background: transparent; border: 1px solid ${t.border}; color: ${t.textDim};
            font-size: 18px; cursor: pointer; padding: 6px 10px; border-radius: 4px;
            line-height: 1; margin-right: 15px;
          `,
          headerButtons: `display: flex; align-items: center;`,
          pageHeader: `
            display: flex; justify-content: space-between; align-items: center;
            margin: 0 0 15px;
          `,
          textMuted: `color: ${t.textMuted};`,
          link: `color: ${t.accent}; text-decoration: none;`,
          code: `background: ${t.code}; padding: 2px 6px; border-radius: 3px;`
        };
      }
    },

    Components: {
      Button: {
        render(id, label, color) {
          const s = UI.Styles.get();
          return `<button id="${id}" style="${s.button}background:${color};">${label}</button>`;
        }
      },

      SaveIndicator: {
        getStyles() {
          const t = Common.Theme.get();
          return {
            container: `
              display: inline-flex; align-items: center; gap: 6px;
              font-size: 12px; padding: 4px 10px; border-radius: 4px;
              transition: all 0.2s ease;
            `,
            saved: `background: ${t.accent}22; color: ${t.accent};`,
            saving: `background: ${t.accent}22; color: ${t.accent};`,
            unsaved: `background: #ff990022; color: #ff9900;`
          };
        },

        render(id) {
          const s = this.getStyles();
          return `<span id="${id}" style="${s.container}${s.saved}">✓ Saved</span>`;
        },

        update(id, state) {
          const el = document.getElementById(id);
          if (!el) return;

          const s = this.getStyles();
          const states = {
            [Enum.SaveState.SAVED]: { style: s.saved, text: '✓ Saved' },
            [Enum.SaveState.SAVING]: { style: s.saving, text: '⟳ Saving...' },
            [Enum.SaveState.UNSAVED]: { style: s.unsaved, text: '● Unsaved' }
          };

          const { style, text } = states[state];
          el.style.cssText = s.container + style;
          el.textContent = text;
        },

        async save(id, saveOperation) {
          this.update(id, Enum.SaveState.SAVING);
          await Promise.all([
            saveOperation(),
            new Promise(resolve => setTimeout(resolve, Common.CONFIG.SAVE_INDICATOR_DELAY_MS))
          ]);
          this.update(id, Enum.SaveState.SAVED);
        }
      },

      SettingCheckbox: {
        getStyles() {
          const t = Common.Theme.get();
          return {
            label: `
              display: flex; align-items: center; gap: 12px; cursor: pointer;
              padding: 15px; background: ${t.bgAlt}; border-radius: 6px;
              border: 1px solid ${t.border}; margin-bottom: 12px;
            `,
            checkbox: `width: 18px; height: 18px; cursor: pointer; accent-color: ${t.accent};`,
            description: `margin: 5px 0 0; font-size: 13px; color: ${t.textMuted};`
          };
        },

        render(id, label, description, checked) {
          const s = this.getStyles();
          return `
            <label style="${s.label}">
              <input type="checkbox" id="${id}" style="${s.checkbox}" ${checked ? 'checked' : ''}>
              <span>
                <strong>${label}</strong>
                <p style="${s.description}">${description}</p>
              </span>
            </label>
          `;
        }
      },

      SettingNumber: {
        getStyles() {
          const t = Common.Theme.get();
          return {
            container: `
              display: flex; justify-content: space-between; align-items: center;
              padding: 15px; background: ${t.bgAlt}; border-radius: 6px;
              border: 1px solid ${t.border}; margin-bottom: 12px;
            `,
            input: `
              width: 80px; padding: 8px 12px; border: 1px solid ${t.border};
              border-radius: 4px; background: ${t.bg}; color: ${t.text};
              font-size: 14px; text-align: center;
            `,
            description: `margin: 5px 0 0; font-size: 13px; color: ${t.textMuted};`
          };
        },

        render(id, label, description, value, unit, min, max) {
          const s = this.getStyles();
          return `
            <div style="${s.container}">
              <span>
                <strong>${label}</strong>
                <p style="${s.description}">${description}</p>
              </span>
              <div style="display:flex;align-items:center;gap:8px;">
                <input type="number" id="${id}" style="${s.input}" value="${value}" min="${min}" max="${max}">
                <span>${unit}</span>
              </div>
            </div>
          `;
        }
      },

      Editor: {
        getStyles() {
          const t = Common.Theme.get();
          return {
            container: `
              display: flex; border: 1px solid ${t.border}; border-radius: 4px;
              background: ${t.bgEditor}; flex: 1; overflow: hidden;
            `,
            lineNumbers: `
              padding: 15px 10px; background: ${t.bgLineNumbers}; color: ${t.textLineNumbers};
              font-family: monospace; font-size: 14px; line-height: 1.5;
              text-align: right; user-select: none; border-right: 1px solid ${t.borderDim};
              overflow: hidden; min-width: 45px;
            `,
            textarea: `
              flex: 1; background: ${t.bgEditor}; color: ${t.text}; border: none; padding: 15px;
              font-family: monospace; font-size: 14px; line-height: 1.5;
              resize: none; outline: none; white-space: pre; overflow: auto;
            `
          };
        },

        render(patterns) {
          const s = this.getStyles();
          return `
            <div style="${s.container}">
              <div id="${Enum.ElementId.LINE_NUMBERS}" style="${s.lineNumbers}"></div>
              <textarea id="${Enum.ElementId.PATTERNS_TEXTAREA}" style="${s.textarea}" spellcheck="false">${patterns.join('\n')}</textarea>
            </div>
          `;
        },

        bindEvents() {
          const textarea = document.getElementById(Enum.ElementId.PATTERNS_TEXTAREA);
          const lineNumbers = document.getElementById(Enum.ElementId.LINE_NUMBERS);

          textarea.oninput = () => this.updateLineNumbers();
          textarea.onscroll = () => { lineNumbers.scrollTop = textarea.scrollTop; };
        },

        updateLineNumbers() {
          const textarea = document.getElementById(Enum.ElementId.PATTERNS_TEXTAREA);
          const lineNumbers = document.getElementById(Enum.ElementId.LINE_NUMBERS);
          const lineCount = textarea.value.split('\n').length;

          lineNumbers.innerHTML = Array.from({ length: lineCount }, (_, i) => i + 1).join('<br>');
        },

        getValue() {
          return document.getElementById(Enum.ElementId.PATTERNS_TEXTAREA).value;
        },

        setValue(text) {
          document.getElementById(Enum.ElementId.PATTERNS_TEXTAREA).value = text;
          this.updateLineNumbers();
        },

        parsePatterns(text) {
          return text.split('\n').map(line => line.trim()).filter(line => line.length > 0);
        }
      }
    },

    Tabs: {
      TABS: [Enum.Tab.PATTERNS, Enum.Tab.CONFIG, Enum.Tab.ADVANCED, Enum.Tab.CREDITS],
      activeTab: Enum.Tab.PATTERNS,

      render() {
        const s = UI.Styles.get();
        return `
          <div style="${s.tabs}">
            ${this.TABS.map(name =>
              `<button id="tab-${name}" class="tab-btn" style="${name === this.activeTab ? s.tabActive : s.tab}">
                ${name.charAt(0).toUpperCase() + name.slice(1)}
              </button>`
            ).join('')}
          </div>
        `;
      },

      bindEvents() {
        this.TABS.forEach(name => {
          document.getElementById(`tab-${name}`).onclick = () => this.show(name);
        });
      },

      show(tabName) {
        this.activeTab = tabName;
        const s = UI.Styles.get();

        document.querySelectorAll('.tab-btn').forEach(btn => {
          btn.style.cssText = s.tab;
        });
        document.getElementById(`tab-${tabName}`).style.cssText = s.tabActive;

        this.TABS.forEach(name => {
          document.getElementById(`tab-content-${name}`).style.display = name === tabName ? 'flex' : 'none';
          document.getElementById(`tab-footer-${name}`).style.display = name === tabName ? 'flex' : 'none';
        });
      }
    },

    Pages: {
      Patterns: {
        saveTimeout: null,
        originalValue: '',

        render(patterns) {
          const s = UI.Styles.get();
          const t = Common.Theme.get();
          const resetStyle = `
            background: transparent; border: none; color: ${t.textDim};
            cursor: pointer; font-size: 13px; text-decoration: underline;
          `;
          return `
            <div id="tab-content-patterns" style="${s.content}">
              <div style="${s.pageHeader}">
                <span style="${s.textMuted}">One pattern per line. Page closes when any pattern is detected.</span>
                <div style="display:flex;align-items:center;gap:15px;">
                  <button id="${Enum.ElementId.RESET_BTN}" style="${resetStyle}">Reset to Default</button>
                  ${UI.Components.SaveIndicator.render('patterns-save-status')}
                </div>
              </div>
              ${UI.Components.Editor.render(patterns)}
            </div>
            <div id="tab-footer-patterns" style="display:none;"></div>
          `;
        },

        bindEvents() {
          this.originalValue = UI.Components.Editor.getValue();

          document.getElementById(Enum.ElementId.RESET_BTN).onclick = () => this.handleReset();

          const textarea = document.getElementById(Enum.ElementId.PATTERNS_TEXTAREA);
          textarea.oninput = () => {
            UI.Components.Editor.updateLineNumbers();
            this.scheduleAutosave();
          };

          UI.Components.Editor.updateLineNumbers();
        },

        scheduleAutosave() {
          const currentValue = UI.Components.Editor.getValue();
          const hasChanges = currentValue !== this.originalValue;

          if (!hasChanges) {
            UI.Components.SaveIndicator.update('patterns-save-status', Enum.SaveState.SAVED);
            return;
          }

          UI.Components.SaveIndicator.update('patterns-save-status', Enum.SaveState.UNSAVED);

          clearTimeout(this.saveTimeout);
          this.saveTimeout = setTimeout(() => this.handleSave(), Common.CONFIG.AUTOSAVE_DEBOUNCE_MS);
        },

        async handleSave() {
          const newPatterns = UI.Components.Editor.parsePatterns(UI.Components.Editor.getValue());

          await UI.Components.SaveIndicator.save('patterns-save-status', async () => {
            await Promise.resolve(Common.Storage.setValue(Common.CONFIG.STORAGE_KEY, newPatterns));
            Common.state.detectionPatterns = newPatterns;
          });

          this.originalValue = UI.Components.Editor.getValue();
        },

        handleReset() {
          UI.Components.Editor.setValue(Common.DEFAULT_PATTERNS.join('\n'));
          this.scheduleAutosave();
        }
      },

      Config: {
        originalValues: {},

        render(isDebugEnabled, showAbortButton) {
          const s = UI.Styles.get();
          return `
            <div id="tab-content-config" style="${s.content}display:none;">
              <div style="width:100%;">
                <div style="${s.pageHeader}margin-bottom:20px;">
                  <h3 style="margin:0;">Settings</h3>
                  ${UI.Components.SaveIndicator.render('config-save-status')}
                </div>
                ${UI.Components.SettingCheckbox.render(
                  Enum.ElementId.DEBUG_CHECKBOX,
                  'Enable Debug Mode',
                  'Log pattern detection and matching info to the console',
                  isDebugEnabled
                )}
                ${UI.Components.SettingCheckbox.render(
                  Enum.ElementId.ABORT_CHECKBOX,
                  'Show Abort Button',
                  'Display an abort button during countdown to cancel auto-close',
                  showAbortButton
                )}
              </div>
            </div>
            <div id="tab-footer-config" style="display:none;"></div>
          `;
        },

        bindEvents(isDebugEnabled, showAbortButton) {
          this.originalValues = { isDebugEnabled, showAbortButton };
          document.getElementById(Enum.ElementId.DEBUG_CHECKBOX).onchange = () => this.handleSave();
          document.getElementById(Enum.ElementId.ABORT_CHECKBOX).onchange = () => this.handleSave();
        },

        async handleSave() {
          const debugChecked = document.getElementById(Enum.ElementId.DEBUG_CHECKBOX).checked;
          const abortChecked = document.getElementById(Enum.ElementId.ABORT_CHECKBOX).checked;

          const hasChanges = debugChecked !== this.originalValues.isDebugEnabled ||
                             abortChecked !== this.originalValues.showAbortButton;

          if (!hasChanges) {
            return;
          }

          await UI.Components.SaveIndicator.save('config-save-status', async () => {
            await Promise.resolve(Common.Storage.setValue(Common.CONFIG.DEBUG_KEY, debugChecked));
            await Promise.resolve(Common.Storage.setValue(Common.CONFIG.ABORT_KEY, abortChecked));
            Common.state.debugEnabled = debugChecked;
            Common.state.showAbortButton = abortChecked;
          });

          this.originalValues = { isDebugEnabled: debugChecked, showAbortButton: abortChecked };
        }
      },

      Advanced: {
        originalValues: {},

        render(countdownSeconds, detectionDelayMs) {
          const s = UI.Styles.get();
          return `
            <div id="tab-content-advanced" style="${s.content}display:none;">
              <div style="width:100%;">
                <div style="${s.pageHeader}margin-bottom:20px;">
                  <h3 style="margin:0;">Advanced Settings</h3>
                  ${UI.Components.SaveIndicator.render('advanced-save-status')}
                </div>
                ${UI.Components.SettingNumber.render(
                  Enum.ElementId.COUNTDOWN_INPUT,
                  'Countdown Duration',
                  'Time to wait before closing the window',
                  countdownSeconds,
                  'seconds',
                  1,
                  60
                )}
                ${UI.Components.SettingNumber.render(
                  Enum.ElementId.DELAY_INPUT,
                  'Detection Delay',
                  'Time to wait after page load before scanning for patterns',
                  detectionDelayMs,
                  'ms',
                  0,
                  10000
                )}
              </div>
            </div>
            <div id="tab-footer-advanced" style="display:none;"></div>
          `;
        },

        bindEvents(countdownSeconds, detectionDelayMs) {
          this.originalValues = { countdownSeconds, detectionDelayMs };

          document.getElementById(Enum.ElementId.COUNTDOWN_INPUT).onchange = () => this.handleSave();
          document.getElementById(Enum.ElementId.DELAY_INPUT).onchange = () => this.handleSave();
        },

        async handleSave() {
          const countdown = parseInt(document.getElementById(Enum.ElementId.COUNTDOWN_INPUT).value, 10);
          const delay = parseInt(document.getElementById(Enum.ElementId.DELAY_INPUT).value, 10);

          const hasChanges = countdown !== this.originalValues.countdownSeconds ||
                             delay !== this.originalValues.detectionDelayMs;

          if (!hasChanges) {
            return;
          }

          await UI.Components.SaveIndicator.save('advanced-save-status', async () => {
            await Promise.resolve(Common.Storage.setValue(Common.CONFIG.COUNTDOWN_KEY, countdown));
            await Promise.resolve(Common.Storage.setValue(Common.CONFIG.DELAY_KEY, delay));
            Common.state.settings.countdownSeconds = countdown;
            Common.state.settings.detectionDelayMs = delay;
          });

          this.originalValues = { countdownSeconds: countdown, detectionDelayMs: delay };
        }
      },

      Credits: {
        render() {
          const s = UI.Styles.get();
          return `
            <div id="tab-content-credits" style="${s.content}display:none;">
              <div style="width:100%;">
                <h3 style="margin-top:0;">Auto Close Window v1.6</h3>
                <p style="${s.textMuted}line-height:1.6;">
                  Automatically closes browser windows/tabs when specific text patterns are detected on the page.
                  Useful for closing OAuth callbacks, AWS SSO windows, and other temporary pages.
                </p>
                <h4>Author</h4>
                <p style="${s.textMuted}">
                  Ofry Linkovsky<br>
                  <a href="mailto:[email protected]" style="${s.link}">[email protected]</a>
                </p>
                <h4>Requirements</h4>
                <p style="${s.textMuted}line-height:1.6;">
                  Firefox: Set <code style="${s.code}">dom.allow_scripts_to_close_windows</code>
                  to <code style="${s.code}">true</code> in about:config
                </p>
                <h4>License</h4>
                <p style="${s.textMuted}">MIT License</p>
                <h4>Changelog</h4>
                <ul style="${s.textMuted}line-height:1.8;padding-left:20px;">
                  <li><strong>v1.6</strong> - Refactored: centralized element IDs in Enum, extracted pageHeader style, added AUTOSAVE_DEBOUNCE_MS constant</li>
                  <li><strong>v1.5</strong> - Added Enum object for Theme, SaveState, and Tab constants</li>
                  <li><strong>v1.4</strong> - Added theme toggle button (auto/light/dark) in header</li>
                  <li><strong>v1.3</strong> - Added optional abort button (hidden by default in Config tab)</li>
                  <li><strong>v1.2</strong> - Reorganized code into Common/UI/Logic namespaces</li>
                  <li><strong>v1.1</strong> - Advanced tab with countdown/delay settings, reusable setting components</li>
                  <li><strong>v1.0</strong> - Autosave with status indicators, removed footer buttons</li>
                  <li><strong>v0.9</strong> - Added Config tab with debug mode</li>
                  <li><strong>v0.8</strong> - Refactored to component architecture, added dark mode support</li>
                  <li><strong>v0.7</strong> - Added fullscreen config with tabs</li>
                  <li><strong>v0.6</strong> - Added line numbers to pattern editor</li>
                  <li><strong>v0.5</strong> - Split config UI and auto-close logic</li>
                  <li><strong>v0.4</strong> - Added configuration UI via userscript menu</li>
                  <li><strong>v0.3</strong> - Added countdown overlay before closing</li>
                  <li><strong>v0.2</strong> - Added pattern-based detection</li>
                  <li><strong>v0.1</strong> - Initial release</li>
                </ul>
              </div>
            </div>
            <div id="tab-footer-credits" style="display:none;"></div>
          `;
        },

        bindEvents() {}
      }
    },

    Controller: {
      async open() {
        await this.waitForDomReady();
        this.removeExisting();

        Common.state.theme = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.THEME_KEY, Common.CONFIG.DEFAULTS.THEME)
        );
        const currentPatterns = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.STORAGE_KEY, Common.DEFAULT_PATTERNS)
        );
        const isDebugEnabled = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.DEBUG_KEY, false)
        );
        const showAbortButton = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.ABORT_KEY, Common.CONFIG.DEFAULTS.SHOW_ABORT_BUTTON)
        );
        const countdownSeconds = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.COUNTDOWN_KEY, Common.CONFIG.DEFAULTS.COUNTDOWN_SECONDS)
        );
        const detectionDelayMs = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.DELAY_KEY, Common.CONFIG.DEFAULTS.DETECTION_DELAY_MS)
        );

        const configValues = { isDebugEnabled, showAbortButton, countdownSeconds, detectionDelayMs };
        const dialog = this.createDialog(currentPatterns, configValues);
        document.body.appendChild(dialog);
        this.bindEvents(dialog, configValues);
        UI.Tabs.show(Enum.Tab.PATTERNS);
      },

      waitForDomReady() {
        if (document.readyState !== 'loading') {
          return Promise.resolve();
        }
        return new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve));
      },

      removeExisting() {
        const existing = document.getElementById(Enum.ElementId.DIALOG);
        if (existing) {
          existing.remove();
        }
      },

      createDialog(patterns, configValues) {
        const s = UI.Styles.get();
        const { isDebugEnabled, showAbortButton, countdownSeconds, detectionDelayMs } = configValues;

        const dialog = document.createElement('div');
        dialog.id = Enum.ElementId.DIALOG;
        dialog.style.cssText = s.overlay;

        dialog.innerHTML = `
          <div style="${s.header}">
            <h2 style="margin:0;font-size:20px;">Auto Close Window</h2>
            <div style="${s.headerButtons}">
              <button id="${Enum.ElementId.THEME_BTN}" style="${s.themeToggle}" title="Toggle theme">${Common.Theme.getIcon()}</button>
              <button id="${Enum.ElementId.CLOSE_BTN}" style="${s.closeButton}">&times;</button>
            </div>
          </div>
          ${UI.Tabs.render()}
          ${UI.Pages.Patterns.render(patterns)}
          ${UI.Pages.Config.render(isDebugEnabled, showAbortButton)}
          ${UI.Pages.Advanced.render(countdownSeconds, detectionDelayMs)}
          ${UI.Pages.Credits.render()}
        `;

        return dialog;
      },

      bindEvents(dialog, configValues) {
        const closeDialog = () => dialog.remove();
        const { isDebugEnabled, showAbortButton, countdownSeconds, detectionDelayMs } = configValues;

        document.getElementById(Enum.ElementId.CLOSE_BTN).onclick = closeDialog;
        document.getElementById(Enum.ElementId.THEME_BTN).onclick = async () => {
          await Common.Theme.toggle();
          this.refreshDialog(configValues);
        };

        UI.Tabs.bindEvents();
        UI.Pages.Patterns.bindEvents();
        UI.Pages.Config.bindEvents(isDebugEnabled, showAbortButton);
        UI.Pages.Advanced.bindEvents(countdownSeconds, detectionDelayMs);
        UI.Pages.Credits.bindEvents();

        this.bindKeyboardEvents(closeDialog);
      },

      refreshDialog(configValues) {
        const currentPatterns = UI.Components.Editor.parsePatterns(UI.Components.Editor.getValue());
        const activeTab = UI.Tabs.activeTab;
        this.removeExisting();
        const dialog = this.createDialog(currentPatterns, configValues);
        document.body.appendChild(dialog);
        this.bindEvents(dialog, configValues);
        UI.Tabs.show(activeTab);
      },

      bindKeyboardEvents(closeDialog) {
        document.addEventListener('keydown', (e) => {
          if (e.key === 'Escape' && document.getElementById(Enum.ElementId.DIALOG)) {
            closeDialog();
          }
        });
      }
    }
  };

  // ============================================================================
  // LOGIC - Auto-close functionality
  // ============================================================================
  const Logic = {
    Storage: {
      async load() {
        const stored = await Promise.resolve(Common.Storage.getValue(Common.CONFIG.STORAGE_KEY, null));
        if (this.isValid(stored)) {
          Common.state.detectionPatterns = stored;
        }

        Common.state.debugEnabled = await Promise.resolve(Common.Storage.getValue(Common.CONFIG.DEBUG_KEY, false));
        Common.state.showAbortButton = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.ABORT_KEY, Common.CONFIG.DEFAULTS.SHOW_ABORT_BUTTON)
        );
        Common.state.settings.countdownSeconds = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.COUNTDOWN_KEY, Common.CONFIG.DEFAULTS.COUNTDOWN_SECONDS)
        );
        Common.state.settings.detectionDelayMs = await Promise.resolve(
          Common.Storage.getValue(Common.CONFIG.DELAY_KEY, Common.CONFIG.DEFAULTS.DETECTION_DELAY_MS)
        );

        Common.Debug.log('Initialized', {
          patternCount: Common.state.detectionPatterns.length,
          debugEnabled: Common.state.debugEnabled,
          showAbortButton: Common.state.showAbortButton,
          countdownSeconds: Common.state.settings.countdownSeconds,
          detectionDelayMs: Common.state.settings.detectionDelayMs
        });
      },

      isValid(value) {
        return value && Array.isArray(value) && value.length > 0;
      }
    },

    Detector: {
      getPageText() {
        const root = document.documentElement;
        return root ? root.innerText.toLowerCase() : '';
      },

      findMatch() {
        const pageText = this.getPageText();
        Common.Debug.log('Scanning page for patterns...', {
          url: location.href,
          patternCount: Common.state.detectionPatterns.length
        });

        for (const pattern of Common.state.detectionPatterns) {
          const matches = pageText.includes(pattern.toLowerCase());
          Common.Debug.log(`Pattern "${pattern.substring(0, 50)}..."`, matches ? 'MATCHED' : 'no match');
          if (matches) {
            return pattern;
          }
        }
        return null;
      },

      hasMatch() {
        return !!this.findMatch();
      }
    },

    Overlay: {
      intervalId: null,
      closeTimeoutId: null,

      getStyles() {
        const t = Common.Theme.get();
        return {
          overlay: `
            position: fixed; top: 0; left: 0; width: 100%; height: 100%;
            background: ${t.bg}ee; display: flex; flex-direction: column;
            justify-content: center; align-items: center; z-index: 999999;
          `,
          message: `
            color: ${t.text}; font-size: 24px; text-align: center; font-family: sans-serif;
          `,
          countdown: `
            color: ${t.accent}; font-weight: bold; font-size: 32px;
          `,
          abortButton: `
            margin-top: 30px; padding: 12px 30px; font-size: 16px;
            background: #dc3545; color: white; border: none; border-radius: 6px;
            cursor: pointer; font-family: sans-serif;
          `
        };
      },

      show() {
        const overlay = this.createElement();
        document.body.appendChild(overlay);
        this.bindEvents();
        this.startCountdown();
      },

      createElement() {
        const s = this.getStyles();

        const overlay = document.createElement('div');
        overlay.id = Enum.ElementId.OVERLAY;
        overlay.style.cssText = s.overlay;

        const message = document.createElement('div');
        message.style.cssText = s.message;
        message.innerHTML = `
          This window will close in
          <span id="${Enum.ElementId.COUNTDOWN_DISPLAY}" style="${s.countdown}">${Common.state.settings.countdownSeconds}</span>
          seconds...
        `;

        overlay.appendChild(message);

        if (Common.state.showAbortButton) {
          const abortButton = document.createElement('button');
          abortButton.id = Enum.ElementId.ABORT_BTN;
          abortButton.style.cssText = s.abortButton;
          abortButton.textContent = 'Abort';
          overlay.appendChild(abortButton);
        }

        return overlay;
      },

      bindEvents() {
        if (Common.state.showAbortButton) {
          const abortButton = document.getElementById(Enum.ElementId.ABORT_BTN);
          if (abortButton) {
            abortButton.onclick = () => this.abort();
          }
        }
      },

      startCountdown() {
        let remaining = Common.state.settings.countdownSeconds;
        const element = document.getElementById(Enum.ElementId.COUNTDOWN_DISPLAY);

        this.intervalId = setInterval(() => {
          remaining--;
          element.textContent = remaining;

          if (remaining <= 0) {
            this.stopCountdown();
          }
        }, 1000);
      },

      stopCountdown() {
        if (this.intervalId) {
          clearInterval(this.intervalId);
          this.intervalId = null;
        }
      },

      abort() {
        Common.Debug.log('Auto-close aborted by user');
        this.stopCountdown();
        Logic.WindowCloser.cancelClose();
        const overlay = document.getElementById(Enum.ElementId.OVERLAY);
        if (overlay) {
          overlay.remove();
        }
      }
    },

    WindowCloser: {
      timeoutId: null,

      scheduleClose() {
        const delayMs = Common.state.settings.countdownSeconds * 1000;
        this.timeoutId = setTimeout(() => this.close(), delayMs);
      },

      cancelClose() {
        if (this.timeoutId) {
          clearTimeout(this.timeoutId);
          this.timeoutId = null;
        }
      },

      close() {
        window.close('', '_parent', '');
      }
    },

    Scheduler: {
      onDomReady(callback) {
        if (document.readyState === 'loading') {
          document.addEventListener('DOMContentLoaded', callback);
        } else {
          callback();
        }
      },

      delay(callback, ms) {
        setTimeout(callback, ms);
      }
    },

    Controller: {
      async loadSettings() {
        await Logic.Storage.load();
      },

      scheduleDetection() {
        Logic.Scheduler.onDomReady(() => {
          Logic.Scheduler.delay(
            () => this.detectAndClose(),
            Common.state.settings.detectionDelayMs
          );
        });
      },

      detectAndClose() {
        if (Logic.Detector.hasMatch()) {
          this.initiateClose();
        }
      },

      initiateClose() {
        Logic.Overlay.show();
        Logic.WindowCloser.scheduleClose();
      }
    }
  };

  // ============================================================================
  // INITIALIZATION
  // ============================================================================
  async function initialize() {
    Common.init();
    await Logic.Controller.loadSettings();
    Common.Storage.registerMenuCommand('Configure Auto-Close Patterns', () => UI.Controller.open());
    Logic.Controller.scheduleDetection();
  }

  initialize();
})();