LRN

Makes LRN training less painful

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        LRN
// @namespace   urn://https://www.georgegillams.co.uk/api/greasemonkey/lrn
// @include     *lrn.com*
// @exclude     none
// @version     0.0.11
// @description:en	Makes LRN training less painful
// @grant    		none
// @description Makes LRN training less painful
// @license MIT
// ==/UserScript==

/**
 * USAGE INSTRUCTIONS:
 *
 * BEFORE YOU USE THIS: Start the course and do the intro manually. This section determines which sections you have to go through and you don't want the script randomly choosing loads of them!
 *
 * Either: Install using the TamperMonkey browser extension
 * Or: Copy the popup URL to open the course in a normal tab, open dev tools (using menu as they've disabled right-click) and paste the script into the console.
 *
 * Sometimes it gets stuck, so if it does just reload (and if using the copy-paste method, re-execute in the console).
 *
 * Finally, if you've completed everything but it still needs you to spend more time (it requires you to spend at least 90 mins) there's a new "Slow mode" - toggle this on at the top of the script and re-open any completed section to go through it super slowly!
 */

const DEBUG = false;
const ATTEMPT_QUIZZES = true;
const SLOW_MODE = false;

let isWorking = false;

const getRandomItem = (arr) => {
  const randomIndex = Math.floor(Math.random() * arr.length);
  return arr[randomIndex] ?? arr[0];
};

const pause = (duration) =>
  new Promise((res) => setTimeout(() => res(), duration));

const debug = DEBUG ? (...args) => console.log('DEBUG:', ...args) : () => null;

const closePopup = () => {
  const closeButton = document.getElementById('POP_CLOSE');
  if (!closeButton) {
    return;
  }
  closeButton.click();
};

const isAnswerCheckbox = (element) =>
  element.getAttribute('type') === 'checkbox' &&
  element.id.startsWith('SAQ_CHECKBOX');
const isAnswerRadio = (element) =>
  element.getAttribute('type') === 'radio' &&
  element.id.startsWith('SAQ_RADIO');

const speedUpVideo = () => {
  const videos = [...document.getElementsByTagName('video')];
  debug('Speeding up videos', videos);
  videos.forEach((video) => {
    video.playbackRate = 4.0;
    if (video?.paused) {
      video.play();
    }
  });
};

const openHomepageSectionIfAvailable = async () => {
  const homepageSectionCards =
    document.getElementsByClassName('ip-tile-card-inner');
  debug('Homepage sections', homepageSectionCards);
  if (!homepageSectionCards.length) {
    return;
  }

  const unlockedHomepageSectionCards = [...homepageSectionCards].filter(
    (section) => {
      return !section.querySelector('.ip-icon-lock');
    },
  );
  debug('unlockedHomepageSectionCards', unlockedHomepageSectionCards);
  if (!unlockedHomepageSectionCards.length) {
    return;
  }

  const incompleteHomepageSectionCards = [
    ...unlockedHomepageSectionCards,
  ].filter((section) => {
    return !section.querySelector('.ip-tile-check');
  });
  debug('incompleteHomepageSectionCards', incompleteHomepageSectionCards);
  if (
    incompleteHomepageSectionCards.some((section) =>
      section.innerText.includes('Introduction'),
    )
  ) {
    debug('Introduction section found, skipping');
    return;
  }
  if (!incompleteHomepageSectionCards.length) {
    return;
  }
  debug('Filtered homepage sections', incompleteHomepageSectionCards);
  await pause(2000);
  getRandomItem(incompleteHomepageSectionCards).click();
};

const selectThumnailButtonIfAvailable = () => {
  // get all elements by class ip-ctr-select-thumbnail-btn
  // for each, click it
  const thumbnailButtons = [
    ...document.getElementsByClassName('ip-ctr-select-thumbnail-btn'),
  ];
  debug('thumbnailButtons', thumbnailButtons);
  if (!thumbnailButtons.length) {
    return;
  }
  isWorking = true;
  for (let button of thumbnailButtons) {
    debug('Selecting thumbnail button', button);
    button.click();
  }
  isWorking = false;
};

const pressPlayVideo = () => {
  const playButton = document.getElementById('VIDEO_PLAY_BUTTON');
  debug('Play button', playButton);
  if (!playButton) {
    return;
  }
  playButton.click();
  debug('Pressing play button');
};

const selectRandomBinaryOption = () => {
  if (!ATTEMPT_QUIZZES) {
    return;
  }

  const radioInputs = [...document.getElementsByTagName('INPUT')].filter(
    (input) => input.id.startsWith('RADIO-'),
  );
  debug('radioInputs', radioInputs);
  if (!radioInputs.length) {
    return;
  }
  getRandomItem(radioInputs).click();
};

const answerQuestionIfAvailable = () => {
  const scorePieChart = document.getElementById('PIE_TEXT_QUICK_QUIZ_PROGRESS');
  if (scorePieChart && !ATTEMPT_QUIZZES) {
    // We can't automate this part as we need the answers to be correct!
    debug('Not attempting quiz');
    return;
  }

  const submitButton =
    document.getElementById('IP_SAQ_SUBMIT_BUTTON') ||
    document.getElementById('SUBMIT_BUTTON') ||
    document.getElementById('RETRY_BUTTON');
  debug('submitButton', submitButton);
  if (!submitButton) {
    return;
  }
  debug('Attempting question(s)');
  const submitDisabled = submitButton.getAttribute('aria-disabled') === 'true';
  debug('submitDisabled', submitDisabled);
  if (!submitDisabled) {
    submitButton.click();
  }

  // The submit button cannot yet be pressed, so we need to select more answers
  const uncheckedAnswers = [...document.getElementsByTagName('INPUT')].filter(
    (input) =>
      (isAnswerCheckbox(input) || isAnswerRadio(input)) &&
      input.getAttribute('aria-checked') !== 'true',
  );
  debug('uncheckedAnswers', uncheckedAnswers);
  if (!uncheckedAnswers.length) {
    return;
  }
  getRandomItem(uncheckedAnswers).click();
};

const expandSectionsIfAvailable = async () => {
  const expandableItems = [
    ...document.getElementsByClassName('ip-ctr-inner-btn'),
    ...document.getElementsByClassName('ip-accordion-popupBtn'),
    ...document.getElementsByClassName('ctrhotspot-hotspot'),
  ];
  debug('Expandable items', expandableItems);
  if (!expandableItems.length) {
    return;
  }
  isWorking = true;
  for (let item of expandableItems) {
    debug('Expandable item', item);
    item.click();
    await pause(800);
  }
  isWorking = false;
};

const pressNextIfAvailable = () => {
  const nextButton = document.getElementById('NAV_NEXT');
  debug('nextButton', nextButton);
  if (!nextButton) {
    return;
  }
  const isDisabled = nextButton.getAttribute('aria-disabled') === 'true';
  debug('nextButton isDisabled', isDisabled);
  if (isDisabled) {
    return;
  }
  // TODO: Hide video elements? Maybe they're interfering with the next button?
  nextButton.click();
};

const pressSwitchToTextIfAvailable = () => {
  const switchButton = document.getElementById('BTN_TEXT_MODE');
  debug('switchButton', switchButton);
  if (!switchButton) {
    return;
  }
  const isDisabled = switchButton.getAttribute('aria-disabled') === 'true';
  debug('switchButton isDisabled', isDisabled);
  if (isDisabled) {
    return;
  }
  switchButton.click();
};

function doActions() {
  openHomepageSectionIfAvailable();
  pressSwitchToTextIfAvailable();
  selectThumnailButtonIfAvailable();
  pressNextIfAvailable();
  expandSectionsIfAvailable();
  answerQuestionIfAvailable();
  selectRandomBinaryOption();
  pressPlayVideo();
  speedUpVideo();
  closePopup();
}

function worker() {
  try {
    // If another action is in progress, then don't trigger new actions yet...
    if (!isWorking) {
      doActions();
    }
  } catch (e) {
    // eslint-disable-next-line no-console
    console.log(e);
  }
}

setInterval(worker, SLOW_MODE ? 15000 : 500);