Quizlet Reshuffle

Reshuffles in learn and flashcards with a button or keybind.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey, Greasemonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Violentmonkey.

Voor het installeren van scripts heb je een extensie nodig, zoals Tampermonkey of Userscripts.

Voor het installeren van scripts heb je een extensie nodig, zoals {tampermonkey_link:Tampermonkey}.

Voor het installeren van scripts heb je een gebruikersscriptbeheerder nodig.

(Ik heb al een user script manager, laat me het downloaden!)

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een extensie nodig, zoals {stylus_link:Stylus}.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

Voor het installeren van gebruikersstijlen heb je een gebruikersstijlbeheerder nodig.

(Ik heb al een beheerder - laat me doorgaan met de installatie!)

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

})();