Calculator

Calculator corner button with dark/light theme, skins, custom font/size, full/paged history.

// ==UserScript==
// @name         Calculator
// @version      1.2
// @author       Mane
// @license      CC0-1.0
// @description  Calculator corner button with dark/light theme, skins, custom font/size, full/paged history.
// @match        *://*/*
// @run-at       document-end
// @namespace https://greasyfork.org/users/1491313
// ==/UserScript==

;(function(){
  'use strict';

  // --- KEYS ---
  const KEY_EXPR   = 'cc_expr';
  const KEY_HIST   = 'cc_hist';
  const KEY_THEME  = 'cc_theme';
  const KEY_MEM    = 'cc_mem';
  const KEY_ACC    = 'cc_accent';
  const KEY_FONT   = 'cc_font';
  const KEY_FS     = 'cc_font_size';

  // --- STATE ---
  let expr    = localStorage.getItem(KEY_EXPR)     || '';
  let theme   = localStorage.getItem(KEY_THEME)    || 'dark';
  let history = [];
  let mem     = parseFloat(localStorage.getItem(KEY_MEM) || '0');
  let accent  = localStorage.getItem(KEY_ACC)      || '#007acc';
  let font    = localStorage.getItem(KEY_FONT)     || 'sans-serif';
  let fs      = localStorage.getItem(KEY_FS)       || '16';

  // --- STYLES ---
  const css = `
    :root {
      --bg:#1e1e1e;--fg:#f1f1f1;--btn-bg:#333;--btn-fg:#f1f1e1;--accent:#007acc;
    }
    [data-theme="light"]{
      --bg:#f1f1f1;--fg:#1e1e1e;--btn-bg:#ddd;--btn-fg:#1e1e1e;--accent:#005a9e;
    }
    #cc-toggle {
      position:fixed;bottom:20px;right:20px;width:36px;height:36px;
      background:var(--accent);color:#fff;border:none;border-radius:4px;
      cursor:pointer;z-index:999999;font-size:18px;
    }
    #cc-panel {
      position:fixed;bottom:60px;right:20px;width:320px;
      background:var(--bg);color:var(--fg);padding:10px;
      border-radius:6px;box-shadow:0 4px 12px rgba(0,0,0,.3);
      font-family:sans-serif;display:none;z-index:999999;
    }
    .small-btn {
      position:absolute;width:24px;height:24px;
      border:none;border-radius:4px;cursor:pointer;
      background:var(--btn-bg);color:var(--btn-fg);
      font-size:14px;line-height:1;
    }
    #cc-theme {top:8px;left:8px;}
    #cc-skin  {top:8px;right:44px;}
    #cc-history-toggle {top:8px;right:8px;}
    #cc-display {
      flex:1;height:36px;
      padding:0 8px;font-size:18px;text-align:right;
      background:var(--btn-bg);color:var(--fg);border:none;
    }
    #cc-copy {
      margin-left:6px;
      background:var(--btn-bg);color:var(--btn-fg);
      border:none;border-radius:4px;cursor:pointer;
      font-size:18px;height:36px;width:36px;
    }
    .cc-row{display:flex;margin:4px 0}
    .cc-btn{
      flex:1;margin:2px;height:32px;
      background:var(--btn-bg);color:var(--btn-fg);
      border:none;border-radius:4px;cursor:pointer;
      font-size:16px;
    }
    .cc-op{background:var(--accent);color:#fff}
    #cc-history {
      display:none;margin-top:8px;
      max-height:140px;overflow:auto;
      background:var(--btn-bg);padding:6px;border-radius:4px;
    }
    #cc-history header{
      margin-bottom:4px;
    }
    #cc-history ul{
      list-style:none;padding:0;margin:0;font-size:14px;
    }
    #cc-history li{
      padding:2px 0;border-bottom:1px solid rgba(255,255,255,.1);
      cursor:pointer;
    }
    #cc-history button {
      width:48%;margin:4px 1%;padding:4px;
      background:var(--btn-bg);color:var(--btn-fg);
      border:none;border-radius:4px;cursor:pointer;
    }
    #cc-settings {
      display:none;margin-top:8px;
      background:var(--btn-bg);color:var(--btn-fg);
      padding:6px;border-radius:4px;font-size:14px;
    }
    #cc-settings label {display:block;margin-bottom:6px;}
    #cc-settings input, #cc-settings select {
      margin-left:6px;
    }
  `;
  const style = document.createElement('style');
  style.textContent = css;
  document.head.appendChild(style);

  // --- APPLY THEME & SKIN ---
  document.documentElement.setAttribute('data-theme', theme);
  document.documentElement.style.setProperty('--accent', accent);

  // --- BUILD UI ---
  const toggle = document.createElement('button');
  toggle.id = 'cc-toggle';
  toggle.textContent = '🖩';
  document.body.appendChild(toggle);

  const panel = document.createElement('div');
  panel.id = 'cc-panel';
  panel.innerHTML = `
    <button id="cc-theme" class="small-btn">${theme==='dark'?'☀️':'🌙'}</button>
    <button id="cc-skin"  class="small-btn">⚙️</button>
    <button id="cc-history-toggle" class="small-btn">📜</button>
    <div style="display:flex;align-items:center;margin:32px 0 8px;">
      <input id="cc-display" type="text"/>
      <button id="cc-copy" title="Copy">📋</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn cc-op">π</button>
      <button class="cc-btn cc-op">e</button>
      <button class="cc-btn cc-op">ln</button>
      <button class="cc-btn cc-op">exp</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn cc-op">^</button>
      <button class="cc-btn cc-op">%</button>
      <button class="cc-btn cc-op">!</button>
      <div style="flex:1;margin:2px"></div>
    </div>
    <div class="cc-row">
      <button class="cc-btn">M+</button>
      <button class="cc-btn">M-</button>
      <button class="cc-btn">MR</button>
      <button class="cc-btn">MC</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn">7</button><button class="cc-btn">8</button>
      <button class="cc-btn">9</button><button class="cc-btn cc-op">/</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn">4</button><button class="cc-btn">5</button>
      <button class="cc-btn">6</button><button class="cc-btn cc-op">*</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn">1</button><button class="cc-btn">2</button>
      <button class="cc-btn">3</button><button class="cc-btn cc-op">-</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn">0</button><button class="cc-btn">.</button>
      <button class="cc-btn cc-op">=</button><button class="cc-btn cc-op">+</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn cc-op">(</button>
      <button class="cc-btn cc-op">)</button>
      <button class="cc-btn">←</button>
      <button class="cc-btn">C</button>
    </div>
    <div class="cc-row">
      <button class="cc-btn">√</button>
      <button class="cc-btn" style="visibility:hidden"></button>
      <button class="cc-btn" style="visibility:hidden"></button>
      <button class="cc-btn" style="visibility:hidden"></button>
    </div>
    <div id="cc-history">
      <header><strong>History</strong></header>
      <ul></ul>
      <button id="cc-clear">Clear</button>
      <button id="cc-copy-all">Copy All</button>
    </div>
    <div id="cc-settings">
      <label>Accent:<input type="color" id="cc-accent-picker" value="${accent}"></label>
      <label>Font:<select id="cc-font-picker">
        <option value="sans-serif">Sans</option>
        <option value="monospace">Mono</option>
      </select></label>
      <label>Size:<input type="number" id="cc-font-size" min="12" max="24" value="${fs}" style="width:50px">px</label>
    </div>
  `;
  document.body.appendChild(panel);

  // --- APPLY FONT & SIZE ---
  panel.style.fontFamily = font;
  panel.style.fontSize   = fs + 'px';

  // --- ELEMENTS ---
  const disp        = panel.querySelector('#cc-display');
  disp.value        = expr;
  const settingsDiv = panel.querySelector('#cc-settings');

  // --- EVENTS ---
  toggle.addEventListener('click', () => {
    panel.style.display = panel.style.display==='block'?'none':'block';
  });

  panel.addEventListener('click', e => {
    const b = e.target, v = b.textContent;

    // theme toggle
    if (b.id==='cc-theme') {
      theme = theme==='dark'?'light':'dark';
      document.documentElement.setAttribute('data-theme', theme);
      localStorage.setItem(KEY_THEME, theme);
      b.textContent = theme==='dark'?'☀️':'🌙';
      return;
    }

    // skin/settings toggle
    if (b.id==='cc-skin') {
      settingsDiv.style.display = settingsDiv.style.display==='block'?'none':'block';
      return;
    }

    // history toggle
    if (b.id==='cc-history-toggle') {
      const h = panel.querySelector('#cc-history');
      const open = h.style.display!=='block';
      h.style.display = open?'block':'none';
      if(open){ loadHistory(); renderHistory(); }
      return;
    }

    // clear history
    if (b.id==='cc-clear') {
      history = []; saveHistory(); renderHistory();
      return;
    }

    // copy all history
    if (b.id==='cc-copy-all') {
      const txt = history.map(it=>`${it.input} = ${it.result}`).join('\n');
      navigator.clipboard.writeText(txt).catch(console.error);
      return;
    }

    // copy display
    if (b.id==='cc-copy') {
      navigator.clipboard.writeText(expr).catch(console.error);
      return;
    }

    // memory
    if (['M+','M-','MR','MC'].includes(v)) {
      const cur = parseFloat(expr)||0;
      if(v==='M+') mem+=cur;
      if(v==='M-') mem-=cur;
      if(v==='MR'){ expr=String(mem); updateDisplay(); }
      if(v==='MC') mem=0;
      localStorage.setItem(KEY_MEM, mem);
      return;
    }

    // delete last char
    if (v==='←') {
      expr = expr.slice(0, -1);
      updateDisplay();
      return;
    }

    // clear all
    if (v==='C') {
      expr = '';
      updateDisplay();
      return;
    }

    // calculator buttons
    if (b.classList.contains('cc-btn')) {
      if (v==='=') { evaluate(); }
      else { expr += v; updateDisplay(); }
    }
  });

  // paste into display
  disp.addEventListener('paste', e => {
    e.preventDefault();
    const txt = e.clipboardData.getData('text');
    expr = txt.replace(/[^\d+\-*/().√πeexpeln!%^sincotanlog]/g,'');
    updateDisplay();
  });

  // settings inputs
  panel.querySelector('#cc-accent-picker')
    .addEventListener('input', e => {
      accent = e.target.value;
      document.documentElement.style.setProperty('--accent', accent);
      localStorage.setItem(KEY_ACC, accent);
    });
  panel.querySelector('#cc-font-picker')
    .addEventListener('input', e => {
      font = e.target.value;
      panel.style.fontFamily = font;
      localStorage.setItem(KEY_FONT, font);
    });
  panel.querySelector('#cc-font-size')
    .addEventListener('input', e => {
      fs = e.target.value;
      panel.style.fontSize = fs + 'px';
      localStorage.setItem(KEY_FS, fs);
    });

  // --- CALC LOGIC ---
  function evaluate(){
    try {
      const factorial = n=>n<2?1:n*factorial(n-1);
      let safe = expr
        .replace(/√/g,'Math.sqrt')
        .replace(/π/g,'Math.PI')
        .replace(/\be\b/g,'Math.E')
        .replace(/\bln\b/g,'Math.log')
        .replace(/\bexp\b/g,'Math.exp')
        .replace(/\^/g,'**')
        .replace(/(\d+)!/g,'factorial($1)')
        .replace(/\b(sin|cos|tan|log)\b/g,'Math.$1');
      const res = Function('Math','factorial','return '+safe)(Math,factorial);
      history.unshift({input:expr, result:res});
      history = history.slice(0,50);
      saveHistory();
      expr = String(res);
    } catch {
      expr = 'Error';
    }
    updateDisplay();
  }

  function updateDisplay(){
    disp.value = expr;
    localStorage.setItem(KEY_EXPR, expr);
  }

  // --- HISTORY & STORAGE ---
  function loadHistory(){
    history = JSON.parse(localStorage.getItem(KEY_HIST)||'[]');
  }
  function saveHistory(){
    localStorage.setItem(KEY_HIST, JSON.stringify(history));
  }
  function renderHistory(){
    const ul = panel.querySelector('#cc-history ul');
    ul.innerHTML = history.map(it=>
      `<li>${it.input} = ${it.result}</li>`
    ).join('');
    ul.querySelectorAll('li').forEach((li,i)=>
      li.addEventListener('click',()=>{
        expr = history[i].input;
        updateDisplay();
      })
    );
  }

})();