Quizlet Reshuffle

Reshuffles in learn and flashcards with a button or keybind.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name         Quizlet Reshuffle
// @namespace    quizlet-reshuffle
// @version      1.0
// @description  Reshuffles in learn and flashcards with a button or keybind.
// @author       Richard Rogalski
// @match        https://quizlet.com/*/learn*
// @match        https://quizlet.com/*/flashcards*
// @grant        none
// @run-at       document-idle
// @license      0BSD
// ==/UserScript==

(function () {
  'use strict';

  // Config: how long to wait before each task (change if having problems)
  const WAIT_MS = 0;

  // Config: change key combo here (always functions)
  const KEYBIND = { ctrl: true, alt: false, shift: false, meta: false, key: 'R' }; // meta == ⌘ on macOS

  // Config: change key combo here (functions when not in a text box)
  const KEYBIND2 = 'R';

  function isTyping() {
    const el = document.activeElement;
    if (!el) return false;
    const tag = el.tagName;
    if (tag === 'INPUT' || tag === 'TEXTAREA') return true;
    if (el.isContentEditable) return true;
    return false;
  }

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  function clickAway() {
    document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
    document.body.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
    document.body.dispatchEvent(new MouseEvent('click', { bubbles: true }));
  }

  function getOptionsButton() {
    return document.querySelector('button[aria-label="Options"]');
  }

  function getShuffleButton() {
    return document.querySelector('button[aria-label="Shuffle"]');
  }

  function isShuffleOn(btn) {
    return btn?.getAttribute('aria-pressed') === 'true';
  }

  async function forceReshuffle() {
    let shuffleBtn = getShuffleButton();
    const optionsBtn = getOptionsButton();
    let cards = true;
    if (!shuffleBtn) cards = false;
    //if (!optionsBtn) return;

    // Open Options
    if (!cards) optionsBtn.click();

    // Wait for Shuffle to exist
    for (let i = 0; i < 10; i++) {
      await sleep(WAIT_MS);
      shuffleBtn = getShuffleButton();
      if (shuffleBtn) break;
    }
    if (!shuffleBtn) return;

    // Turn shuffle OFF if ON
    if (isShuffleOn(shuffleBtn)) {
      shuffleBtn.click();
      clickAway();
      await sleep(WAIT_MS);

      // Reopen options
      if (!cards) optionsBtn.click();
      await sleep(WAIT_MS);
      shuffleBtn = getShuffleButton();
    }

    // Turn shuffle ON
    shuffleBtn?.click();

    // click-away to close menu and commit state
    await sleep(WAIT_MS);
    clickAway();

    //console.log('Deck reshuffled');
  }

  function createFloatingButton() {
    const btn = document.createElement('button');
    btn.textContent = 'Reshuffle';
    Object.assign(btn.style, {
      position: 'fixed',
      bottom: '20px',
      right: '20px',
      zIndex: 9999,
      padding: '10px 14px',
      background: '#2e3856',
      color: '#d9dde8',
      border: 'none',
      borderRadius: '12px',
      fontSize: '.875rem',
      cursor: 'pointer',
      boxShadow: '0 6px 16px rgba(0,0,0,0.25)'
    });

    btn.addEventListener('click', forceReshuffle);
    document.body.appendChild(btn);
  }

  function matchesKeybind(e) {
    if (isTyping()) {
        if (!!KEYBIND.ctrl !== !!e.ctrlKey) return false;
        if (!!KEYBIND.alt !== !!e.altKey) return false;
        if (!!KEYBIND.shift !== !!e.shiftKey) return false;
        if (!!KEYBIND.meta !== !!e.metaKey) return false;
        return e.key && e.key.toLowerCase() === KEYBIND.key.toLowerCase();
    } return e.key && e.key.toLowerCase() === KEYBIND2.toLowerCase();
  }

  window.addEventListener('keydown', function (e) {
    if (matchesKeybind(e)) {
      e.preventDefault();
      e.stopPropagation();
      forceReshuffle();
    }
  }, true);

  createFloatingButton();

})();