Smart Dark Mode

-

// ==UserScript==
// @name         Smart Dark Mode
// @description  -
// @version      2025.10.17
// @match        *://*/*
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM_registerMenuCommand
// @run-at       document-start
// @icon         data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' fill='%23232323' class='bi bi-moon-stars-fill' viewBox='0 0 16 16'%3e%3cpath d='M6 .278a.768.768 0 0 1 .08.858 7.208 7.208 0 0 0-.878 3.46c0 4.021 3.278 7.277 7.318 7.277.527 0 1.04-.055 1.533-.16a.787.787 0 0 1 .81.316.733.733 0 0 1-.031.893A8.349 8.349 0 0 1 8.344 16C3.734 16 0 12.286 0 7.71 0 4.266 2.114 1.312 5.124.06A.752.752 0 0 1 6 .278z'/%3e%3cpath d='M10.794 3.148a.217.217 0 0 1 .412 0l.387 1.162c.173.518.579.924 1.097 1.097l1.162.387a.217.217 0 0 1 0 .412l-1.162.387a1.734 1.734 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.734 1.734 0 0 0 9.31 6.593l-1.162-.387a.217.217 0 0 1 0-.412l1.162-.387a1.734 1.734 0 0 0 1.097-1.097l.387-1.162zM13.863.099a.145.145 0 0 1 .274 0l.258.774c.115.346.386.617.732.732l.774.258a.145.145 0 0 1 0 .274l-.774.258a1.156 1.156 0 0 0-.732.732l-.258.774a.145.145 0 0 1-.274 0l-.258-.774a1.156 1.156 0 0 0-.732-.732l-.774-.258a.145.145 0 0 1 0-.274l.774-.258c.346-.115.617-.386.732-.732L13.863.1z'/%3e%3c/svg%3e
// @namespace https://ndaesik.tistory.com/
// ==/UserScript==

const normHost = h => String(h||'').toLowerCase().replace(/^www\./,'');
const HOST = normHost(location.hostname);
const hostMatch = (h, e) => h===e || h.endsWith('.'+e);

let state = {
  settings: ['hotKeySetOn','Ctrl + D','setTimeOff','18:00','07:00'],
  alwaysOnList: '',
  alwaysOffList: `youtube.com,
m.youtube.com,
music.youtube.com,
studio.youtube.com,
docs.google.com,
keep.google.com`,
  uiReady: false
};

let drkMo;
let EARLY_OFF = false;

const earlyParseList = s => String(s||'')
  .split(/[\r\n,]+/)
  .map(v => v.trim().toLowerCase().replace(/^www\./,''))
  .filter(Boolean);

const earlyUrlMatch = list => {
  const paths = earlyParseList(list);
  return paths.some(entry=>{
    if (entry.includes('/')) {
      const [eHost,...rest]=entry.split('/');
      const ePath='/'+rest.join('/');
      return hostMatch(HOST,eHost) && location.pathname.startsWith(ePath);
    }
    return hostMatch(HOST,entry);
  });
};

(async () => {
  try {
    const offList = await GM.getValue('alwaysOffList','');
    EARLY_OFF = earlyUrlMatch(offList);
  } catch(_) { EARLY_OFF = false; }
  if (!EARLY_OFF && self === top) {
    const s = document.createElement('style');
    s.className = 'preventBlinkCSS';
    s.textContent = `*{background:#202124!important;border-color:#3c4043!important;color-scheme:dark!important;color:#e3e3e3!important;transition:none!important}`;
    document.documentElement.appendChild(s);
  }
})();

GM_registerMenuCommand('On/Off', () => window.postMessage({__SDM__: 'toggle'}, '*'));
GM_registerMenuCommand('Panel',  () => window.postMessage({__SDM__: 'panel' }, '*'));

window.addEventListener('message', e => {
  if (!e || !e.data || e.data.__SDM__==null) return;
  const cmd = e.data.__SDM__;
  (async () => {
    await ensureInit();
    if (cmd === 'toggle') safeToggle();
    if (cmd === 'panel')  togglePanel();
  })();
});

window.addEventListener('load', () => { ensureInit().then(postLoad); });

async function ensureInit() {
  if (state.uiReady) return;
  try {
    state.settings      = await GM.getValue('settings', state.settings);
    state.alwaysOnList  = await GM.getValue('alwaysOnList', state.alwaysOnList);
    state.alwaysOffList = await GM.getValue('alwaysOffList', state.alwaysOffList);
  } catch(_) {}
  buildUI();
  wireUI();
  state.uiReady = true;
}

function postLoad() {
  try { initialApplyLogic(); watchSpa(); } catch(_) {}
}

const parseList = s => String(s||'')
  .split(/[\r\n,]+/)
  .map(v => v.trim().toLowerCase().replace(/^www\./,''))
  .filter(Boolean);

const urlMatch = list => {
  const paths = parseList(list);
  return paths.some(entry=>{
    if (entry.includes('/')) {
      const [eHost,...rest]=entry.split('/');
      const ePath='/'+rest.join('/');
      return hostMatch(HOST,eHost) && location.pathname.startsWith(ePath);
    }
    return hostMatch(HOST,entry);
  });
};

function ensureDrkStyle() {
  if (!drkMo) {
    drkMo = document.createElement('style');
    drkMo.className = 'drkMo';
    drkMo.textContent = `
html{color-scheme:dark!important;background:#fff;color:#000}
html *{color-scheme:light!important;text-shadow:0 0 .1px}
html body{background:none!important}

html,
html :is(img,image,embed,video,canvas,option,object,:fullscreen:not(iframe),iframe:not(:fullscreen)),
html body>* [style*="url("]:not([style*="cursor:"]):not([type="text"]) {
  filter: invert(1) hue-rotate(180deg)!important;
}

html body>* [style*="url("]:not([style*="cursor:"]) :not(#_),
html:not(#_) :is(canvas,option,object) :is(img,image,embed,video),
html:not(#_) :is(video:fullscreen,img[src*="/svg/"],img[src*=".svg."],img[src*="fonts.gstatic.com/s/i/"]) {
  filter: unset!important;
}

#SDM_body { filter: invert(1) hue-rotate(180deg)!important; }
#SDM_body *{ color-scheme:dark!important; }
`;
  }
}

function hardOffNow() { return EARLY_OFF || urlMatch(state.alwaysOffList); }

function applyFilter() {
  if (hardOffNow()) return;
  ensureDrkStyle();
  if (!document.querySelector('style.drkMo')) document.head.appendChild(drkMo);
}

function removeFilterAll() {
  document.querySelectorAll('style.drkMo').forEach(e=>e.remove());
  document.querySelector('.preventBlinkCSS')?.remove();
}

const isOn = () => !!document.querySelector('style.drkMo');

function checkTimeSet() {
  const timeChk = document.querySelector('#SDM_timeSet')?.checked ?? (state.settings[2]==='setTimeOn');
  if (!timeChk) return true;
  const from = (document.querySelector('#SDM_timeSet_input_from')?.value || state.settings[3] || '18:00');
  const to   = (document.querySelector('#SDM_timeSet_input_to')?.value   || state.settings[4] || '07:00');
  const [fh,fm] = from.split(':').map(n=>+n); const [th,tm] = to.split(':').map(n=>+n);
  const now = new Date(); const ch=now.getHours(), cm=now.getMinutes();
  const afterStart = ch>fh || (ch===fh && cm>=fm);
  const beforeEnd  = ch<th || (ch===th && cm<tm);
  return (fh<=th) ? (afterStart && beforeEnd) : (afterStart || beforeEnd);
}

function initialApplyLogic() {
  if (!hardOffNow()) document.querySelector('.preventBlinkCSS')?.remove();
  const offNow = hardOffNow();
  const onNow  = urlMatch(state.alwaysOnList);
  const inTime = checkTimeSet();
  const shouldApply = !offNow && inTime && ( onNow || autoDetectBright() );
  if (offNow) { removeFilterAll(); setToggle(false); }
  else if (shouldApply) { applyFilter(); setToggle(true); }
  else { setToggle(isOn()); }
}

function autoDetectBright() {
  try {
    const frame = self !== top;
    const bodyZero = document.body ? document.body.offsetHeight===0 : false;
    const elems = document.querySelectorAll('body > :not(script)');
    const rgb = el => {
      const m = getComputedStyle(el).getPropertyValue('background-color').match(/\d+/g)||[0,0,0,1];
      return m.map(x=>+x);
    };
    const bright = el => { const [r,g,b,a]=rgb(el); return a===0 || (r*.299+g*.587+b*.114)>186; };
    if ((!frame && !bodyZero || frame) && bright(document.documentElement) && bright(document.body)) return true;
    if (!frame && bodyZero) {
      for (let i=0;i<elems.length;i++){
        if (elems[i].scrollHeight>window.innerHeight && bright(elems[i])) return true;
      }
    }
  } catch(_) {}
  return false;
}

function watchSpa() {
  let lastHref = location.href;
  new MutationObserver(() => {
    if (lastHref !== location.href) {
      lastHref = location.href;
      EARLY_OFF = urlMatch(state.alwaysOffList);
      if (hardOffNow()) { removeFilterAll(); setToggle(false); }
      else if (!isOn() && (urlMatch(state.alwaysOnList) && checkTimeSet())) { applyFilter(); setToggle(true); }
    }
  }).observe(document.body || document.documentElement, {subtree:true, childList:true});
}

function buildUI() {
  if (document.getElementById('SDM_body')) return;
  const ui = `
<div id="SDM_body" class="SDM_root" style="display:none">
  <div class="SDM_wrap">
    <div class="SDM_bar">
      <div class="SDM_title">Smart Dark Mode</div>
      <div class="SDM_barBtns">
        <button class="SDM_btn" id="SDM_add_page" title="Add current domain">+</button>
        <label class="SDM_switch" title="Toggle filter">
          <input id="SDM_toggle" type="checkbox" class="SDM_tabInput">
          <span class="SDM_slider"></span>
        </label>
        <button class="SDM_btn" id="SDM_close">✕</button>
      </div>
    </div>
    <div class="SDM_tabs">
      <input id="tab_on" class="SDM_tabInput" type="radio" name="sdm_tab" checked><label class="SDM_tabLabel" for="tab_on">Always On</label>
      <input id="tab_off" class="SDM_tabInput" type="radio" name="sdm_tab"><label class="SDM_tabLabel" for="tab_off">Always Off</label>
      <input id="tab_settings" class="SDM_tabInput" type="radio" name="sdm_tab"><label class="SDM_tabLabel" for="tab_settings">Settings</label>
    </div>
    <div class="SDM_main">
      <div class="SDM_tabc" data-tab="on"><textarea id="SDM_on_textarea" class="SDM_textarea" spellcheck="false" placeholder="example.com, mysite.com">${state.alwaysOnList}</textarea></div>
      <div class="SDM_tabc" data-tab="off" style="display:none"><textarea id="SDM_off_textarea" class="SDM_textarea" spellcheck="false" placeholder="example.com, mysite.com">${state.alwaysOffList}</textarea></div>
      <div class="SDM_tabc" data-tab="settings" style="display:none">
        <div class="SDM_row">
          <label class="SDM_tgl"><input id="SDM_hotkey" type="checkbox" class="SDM_tabInput" ${state.settings[0]==='hotKeySetOn'?'checked':''}><span>Hotkey</span></label>
          <input id="SDM_hotkey_input" class="SDM_text" value="${state.settings[1]}" placeholder="Ctrl + D">
        </div>
        <div class="SDM_row">
          <label class="SDM_tgl"><input id="SDM_timeSet" type="checkbox" class="SDM_tabInput" ${state.settings[2]==='setTimeOn'?'checked':''}><span>Time Window</span></label>
          <input id="SDM_timeSet_input_from" class="SDM_time" maxlength="5" value="${state.settings[3]}"><span class="SDM_sep">~</span><input id="SDM_timeSet_input_to" class="SDM_time" maxlength="5" value="${state.settings[4]}">
        </div>
      </div>
    </div>
  </div>
</div>
<style>
#SDM_body.SDM_root{position:fixed;top:24px;right:24px;z-index:2147483647;font-family:system-ui,Segoe UI,Roboto,Apple SD Gothic Neo,Arial}
#SDM_body .SDM_wrap{width:340px;border-radius:14px;overflow:hidden;box-shadow:0 10px 24px rgba(0,0,0,.45);background:#0f1115;border:1px solid #262a33}
#SDM_body .SDM_bar{height:44px;display:flex;align-items:center;justify-content:space-between;padding:0 10px;background:linear-gradient(180deg,#12151b,#0f1115);border-bottom:1px solid #1b1f27}
#SDM_body .SDM_title{font-weight:600;font-size:14px;line-height:1.2;color:#e3e3ea;letter-spacing:.2px}
#SDM_body .SDM_barBtns{display:flex;gap:8px;align-items:center}
#SDM_body .SDM_btn{width:28px;height:28px;border-radius:8px;border:1px solid #2a2f3a;background:#151922;color:#cfd2d8;cursor:pointer}
#SDM_body .SDM_btn:hover{border-color:#3a4050}
#SDM_body .SDM_switch{position:relative;display:inline-block;width:46px;height:26px}
#SDM_body .SDM_switch input{opacity:0;width:0;height:0}
#SDM_body .SDM_slider{position:absolute;cursor:pointer;top:0;left:0;right:0;bottom:0;background:#2a2f3a;border-radius:16px;transition:.2s}
#SDM_body .SDM_slider:before{position:absolute;content:"";height:20px;width:20px;left:3px;top:3px;background:#cfd2d8;border-radius:50%;transition:.2s}
#SDM_body .SDM_switch input:checked + .SDM_slider{background:#ffb100}
#SDM_body .SDM_switch input:checked + .SDM_slider:before{transform:translateX(20px);background:#1a1a1a}
#SDM_body .SDM_tabs{display:grid;grid-template-columns:1fr 1fr 1fr;background:#0f1115}
#SDM_body .SDM_tabInput{all:unset}
#SDM_body .SDM_tabInput[type="radio"]{position:absolute;opacity:0;pointer-events:none}
#SDM_body .SDM_tabLabel{padding:10px 0;text-align:center;font-weight:600;font-size:12px;line-height:1;color:#9aa1ad;border-bottom:2px solid transparent;cursor:pointer;user-select:none}
#SDM_body #tab_on:checked   + .SDM_tabLabel{color:#ffb100;border-color:#ffb100}
#SDM_body #tab_off:checked  + .SDM_tabLabel{color:#ffb100;border-color:#ffb100}
#SDM_body #tab_settings:checked + .SDM_tabLabel{color:#ffb100;border-color:#ffb100}
#SDM_body .SDM_main{padding:10px;background:#0f1115}
#SDM_body .SDM_tabc{height:300px}
#SDM_body .SDM_textarea{width:100%;height:100%;resize:none;box-sizing:border-box;border:1px solid #2a2f3a;background:#0b0d11;color:#dfe3ea;border-radius:10px;padding:10px;font:13px/1.4 ui-monospace,Consolas,Monaco}
#SDM_body .SDM_text{height:34px;border:1px solid #2a2f3a;background:#0b0d11;color:#e3e6ec;border-radius:8px;padding:0 10px;min-width:160px;font:13px/1.2 system-ui}
#SDM_body .SDM_time{height:34px;border:1px solid #2a2f3a;background:#0b0d11;color:#e3e6ec;border-radius:8px;padding:0 10px;width:70px;font:13px/1.2 system-ui;text-align:center}
#SDM_body .SDM_sep{color:#8f96a3;margin:0 6px}
#SDM_body .SDM_row{display:flex;align-items:center;gap:10px;margin:10px 0}
#SDM_body .SDM_tgl{display:flex;align-items:center;gap:8px;color:#c9ced9;font:500 13px/1.2 system-ui}
#SDM_body .SDM_root *{color:#e3e6ec}
#SDM_body #SDM_hotkey_input:focus{outline:2px solid #ffb100; box-shadow:0 0 0 3px rgba(255,177,0,.15)}
</style>
`;
  document.body.insertAdjacentHTML('beforeend', ui);
}

function wireUI() {
  document.getElementById('SDM_toggle').addEventListener('click', safeToggle);
  document.getElementById('SDM_close').addEventListener('click', togglePanel);

  [['tab_on','on'],['tab_off','off'],['tab_settings','settings']].forEach(([id,name])=>{
    document.getElementById(id).addEventListener('change',()=>{
      document.querySelectorAll('#SDM_body .SDM_tabc').forEach(v=>v.style.display='none');
      document.querySelector(`#SDM_body .SDM_tabc[data-tab="${name}"]`).style.display='block';
    });
  });

  document.getElementById('SDM_add_page').addEventListener('click', () => {
    const domain = HOST;
    if (document.getElementById('tab_on').checked) {
      const t = document.querySelector('#SDM_on_textarea');
      t.value = (t.value ? t.value.trim()+', ' : '') + domain;
    } else if (document.getElementById('tab_off').checked) {
      const t = document.querySelector('#SDM_off_textarea');
      t.value = (t.value ? t.value.trim()+', ' : '') + domain;
    }
    saveSettings();
  });

  document.querySelector('#SDM_body').addEventListener('input', saveSettings);
  document.querySelector('#SDM_body').addEventListener('change', saveSettings);

  const hkInput = document.getElementById('SDM_hotkey_input');
  hkInput.addEventListener('focus', ()=> hkInput.select());
  hkInput.addEventListener('keydown', e => {
    if (e.key === 'Tab') return;
    if (e.key === 'Escape') { hkInput.blur(); e.preventDefault(); return; }
    if (e.key === 'Backspace' || e.key === 'Delete') {
      hkInput.value = ''; saveSettings(); e.preventDefault(); return;
    }
    let combo = '';
    if (e.ctrlKey && e.key!=='Control') combo+='Ctrl + ';
    if (e.altKey && e.key!=='Alt') combo+='Alt + ';
    if (e.shiftKey && e.key!=='Shift') combo+='Shift + ';
    let key = e.key;
    if (/^.$/u.test(key)) key = key.toUpperCase();
    const ignore = ['Control','Alt','Shift','Meta','OS','Dead','Unidentified'];
    if (!ignore.includes(key)) {
      hkInput.value = combo + key;
      saveSettings();
    }
    e.preventDefault();
  });

  document.addEventListener('keydown', e => {
    const a = document.activeElement;
    if (a && (/^(input|textarea)$/i.test(a.tagName) || a.isContentEditable)) return;
    const seq = (document.querySelector('#SDM_hotkey_input')?.value || state.settings[1])
      .split(' + ').map(s=>s.trim()).filter(Boolean);
    const needCtrl = seq.includes('Ctrl'), needAlt = seq.includes('Alt'), needShift = seq.includes('Shift');
    const mainKey = seq.find(k=>!['Ctrl','Alt','Shift'].includes(k)) || '';
    const match =
      (!needCtrl || e.ctrlKey) &&
      (!needAlt || e.altKey) &&
      (!needShift || e.shiftKey) &&
      (!!mainKey && mainKey.toUpperCase() === (e.key || '').toUpperCase());
    const hotOn = (document.querySelector('#SDM_hotkey')?.checked ?? (state.settings[0]==='hotKeySetOn'));
    if (hotOn && match) { e.preventDefault(); safeToggle(); }
  });
}

function togglePanel() {
  const el = document.getElementById('SDM_body');
  if (!el) return;
  el.style.display = (el.style.display==='none' || !el.style.display)?'block':'none';
}

function setToggle(v) {
  const t = document.querySelector('#SDM_toggle');
  if (t) t.checked = !!v;
}

function safeToggle() {
  if (hardOffNow()) { removeFilterAll(); setToggle(false); return; }
  if (isOn()) { removeFilterAll(); setToggle(false); }
  else { applyFilter(); setToggle(true); }
}

function saveSettings() {
  state.alwaysOnList  = document.querySelector('#SDM_on_textarea').value.replace(/^, ?/,'');
  state.alwaysOffList = document.querySelector('#SDM_off_textarea').value.replace(/^, ?/,'');
  state.settings = [
    document.querySelector('#SDM_hotkey').checked ? 'hotKeySetOn' : 'hotKeySetOff',
    document.querySelector('#SDM_hotkey_input').value,
    document.querySelector('#SDM_timeSet').checked ? 'setTimeOn' : 'setTimeOff',
    document.querySelector('#SDM_timeSet_input_from').value,
    document.querySelector('#SDM_timeSet_input_to').value
  ];
  GM.setValue('alwaysOnList',  state.alwaysOnList);
  GM.setValue('alwaysOffList', state.alwaysOffList);
  GM.setValue('settings',      state.settings);
}