Smart Dark Mode

-

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.

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

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

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

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

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

// ==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);
}