SolverDuo

It solves practice lessons automatically or manually, to increase xp use DuoSolverGrinder tool (https://duosolver.is-great.net).

当前为 2024-12-26 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name        SolverDuo
// @name:es     SolverDuo
// @name:es-419 SolverDuo
// @name:fr     SolverDuo
// @name:vi     SolverDuo
// @name:fr-CA  SolverDuo
// @namespace   Violentmonkey Scripts
// @match       https://*.duolingo.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @require     https://cdn.tailwindcss.com
// @version     1.1.0
// @author      DuoSolverGrinder, has as base DuoPower modified totally.
// @description It solves practice lessons automatically or manually, to increase xp use DuoSolverGrinder tool (https://duosolver.is-great.net).
// @description:es Soluciona lecciones de práctica automáticamente o manualmente, para incrementar xp usa la herramienta DuoSolverGrinder (https://duosolver.is-great.net).
// @description:es-419 Soluciona lecciones de práctica automáticamente o manualmente, para incrementar xp usa la herramienta DuoSolverGrinder (https://duosolver.is-great.net).
// @description:fr Résolvez les leçons pratiques automatiquement ou manuellement, pour augmenter l'XP, utilisez l'outil DuoSolverGrinder (https://duosolver.is-great.net).
// @description:fr-CA Résolvez les leçons pratiques automatiquement ou manuellement, pour augmenter l'XP, utilisez l'outil DuoSolverGrinder (https://duosolver.is-great.net).
// @description:vi Giải bài thực hành tự động hoặc thủ công, để tăng xp hãy sử dụng công cụ DuoSolverGrinder (https://duosolver.is-great.net).
// ==/UserScript==

let solveTimerId;
let isAutoMode  = GM_getValue('isAutoMode', false);
let isPanelShow = GM_getValue('isPanelShow', true);
let solveSpeedList = {'speedSlow': 2000, 'speedMedium': 1000, 'speedFast': 500, 'speedFastest': 0 }
let solveSpeed  = GM_getValue('solveSpeed', 'speedMedium');

const version = '1.1.0';
const mainLessonFormClass = "[id='root'] > div > div > div > div > div:first-child._3v4ux";

let panelHtml = `
<div id="panelShowBttns" class="flex flex-col gap-y-2 z-[111] fixed xl:bottom-4 right-5 bottom-48 hidden">
  <button id="panelShowBttn" class="font-bold text-gray-800 shadow-lg rounded-full px-2 bg-indigo-200 border border-2" >+</button>
  <a target="_blank"class="px-1" href="https://duosolver.is-great.net/">
       <img class="rounded-md w-7 h-7" src="https://duosolvergrinder.github.io/DuoSolverGrinder/dsg.png" >
  </a>
</div>
<div id="panelDuoSolver" class="inline-flex flex-col gap-y-4 justify-end rounded-xl fixed xl:bottom-4 right-5 bottom-28 z-[111] border border-1 p-4 bg-indigo-400 dark:bg-gray-900">
  <button id="panelHideBttn" title="hide" class="w-6 self-center rounded-lg  bg-gray-600 text-gray-200">-</button>
  <button id="startBttn" class="px-4 py-2 rounded-md border border-2 bg-blue-600 text-gray-200  dark:bg-gray-300 dark:text-gray-800">Start</button>
  <section class="inline-flex rounded-md shadow-sm" role="group">
      <button id="speedSlow"  type="button"  class="speedSelector px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-s-lg   dark:bg-gray-800 dark:border-gray-700 dark:text-white  dark:focus:ring-blue-500 dark:focus:text-white">
          Slow
      </button>
      <button id="speedMedium" type="button"   class="speedSelector px-4 py-2 text-sm font-medium text-gray-900 bg-white border-t border-b border-gray-200   dark:bg-gray-800 dark:border-gray-700 dark:text-white  dark:focus:ring-blue-500 dark:focus:text-white">
          Medium
      </button>
      <button id="speedFast" type="button"  class="speedSelector px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200   dark:bg-gray-800 dark:border-gray-700 dark:text-white  dark:focus:ring-blue-500 dark:focus:text-white">
          Fast
      </button>
      <button id="speedFastest" type="button"  class="speedSelector px-4 py-2 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-e-lg   dark:bg-gray-800 dark:border-gray-700 dark:text-white  dark:focus:ring-blue-500 dark:focus:text-white">
          Fastest
      </button>
  </section>
  <p class="dark:text-gray-200">To grind higher xp, use next link:</p>
  <div class="flex justify-center gap-x-2">
      <a target="_blank" href="https://github.com/DuoSolverGrinder/DuoSolverGrinder/">
         <svg  class="dark:hidden text-red-500 w-8 h-8" fill="none" viewBox="0 0 120 120"
            stroke="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#24292f"/>
        </svg>
        <svg class="hidden dark:block w-8 h-8" fill="none" viewBox="0 0 120 120" stroke="currentColor" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#fff"/>
        </svg>
      </a>
      <a target="_blank" href="https://duosolver.is-great.net/">
          <img class="rounded-md w-7 h-7" src="https://duosolvergrinder.github.io/DuoSolverGrinder/dsg.png" >
      </a>
  </div>
  <div class="flex justify-center">
        <span class="dark:text-gray-200">v${version}</span>
  </div>
</div>
`;


let solvesBttnHtml = `
    <button id="solveBttn" style="--web-ui_button-background-color: rgb(var(--color-gold)); --web-ui_button-border-color: rgb(var(--color-gold))"  class="_1rcV8 _1VYyp _1ursp _7jW2t _3DbUj _38g3s lg:block hidden">Solve</button>
    <button id="solveAllBttn" class="_1rcV8 _1VYyp _1ursp _7jW2t _3DbUj _38g3s _2oGJR">Solve All</button>
`;

function insertPanelAndBttns()
{
  let panel = document.getElementById('panelDuoSolver');
  if(!panel) {
     document.body.insertAdjacentHTML('beforeend', panelHtml);
     let bttn = document.getElementById('startBttn');
     bttn.addEventListener('click', startStopMain );
     const showBttn = document.getElementById('panelShowBttn');
     const hideBttn = document.getElementById('panelHideBttn');
     showBttn.addEventListener('click', toggleShowHidePanel );
     hideBttn.addEventListener('click', toggleShowHidePanel );
     document.querySelectorAll('.speedSelector').forEach((element)=> element.addEventListener('click', speedSolveChange));
     updatePanelDisplay();
     updateSpeedBttnsActive();
  }
  if (window.location.pathname === '/lesson' || window.location.pathname === '/practice') {
    addButtons();
    panel ? panel.children[1].style.display = 'none' : null;
    return;
  }
  panel ? panel.children[1].style.display = '': null;
}

window.onload = (event) => {
  GM_addStyle('img { max-width: none}');
}

setInterval(insertPanelAndBttns, 1000);


function speedSolveChange()
{
  solveSpeed = this.id;
  GM_setValue('solveSpeed', solveSpeed);
  updateSpeedBttnsActive();
}

function updateSpeedBttnsActive()
{
  const speedBttns = document.querySelectorAll('.speedSelector');
  speedBttns.forEach((element)=> {
         if(element.id == solveSpeed) {
            element.classList.remove('bg-white', 'text-gray-900', 'font-semibold', 'dark:bg-gray-800');
            element.classList.add('bg-gray-900', 'text-white', 'font-bold', 'dark:bg-black');
           return;
         }
          element.classList.add('bg-white', 'text-gray-900', 'font-semibold', 'dark:bg-gray-800');
          element.classList.remove('bg-gray-900', 'text-white', 'font-bold', 'dark:bg-black');
  });

}

function updatePanelDisplay(display)
{
     const panelShowBttns = document.getElementById('panelShowBttns');
     const panel = document.getElementById('panelDuoSolver');
     if(isPanelShow) {
       panel.classList.remove('collapse');
       panelShowBttns.classList.add('hidden');
       return;
     }
      GM_setValue('isPanelShow', false);
      panel.classList.add('collapse');
      panelShowBttns.classList.remove('hidden');
      return;

}

function toggleShowHidePanel()
{
     isPanelShow = !GM_getValue('isPanelShow');
     GM_setValue('isPanelShow', isPanelShow )
     updatePanelDisplay();
}

function setAutoMode(state)
{
  isAutoMode = state;
  GM_setValue('isAutoMode', state);
}

function startStopMain()
{
  setAutoMode(!isAutoMode);
  updateBttnsCaptions();
  if(isAutoMode) {
    window.location.assign('/practice');
  }

}


function addButtons()
{
    const checkBttn = document.querySelectorAll('[data-test="player-next"]')[0];
    if(!checkBttn) {
      return;
    }
    let solveAllBttn = document.getElementById("solveAllBttn");
    if (solveAllBttn !== null) {
        return;
    }
    checkBttn.parentElement.classList.add('flex', 'gap-x-8');
    checkBttn.parentElement.insertAdjacentHTML('beforeend',solvesBttnHtml);

    const solveBttn = document.getElementById("solveBttn");
    solveAllBttn = document.getElementById("solveAllBttn");
    solveBttn.addEventListener('click', solveOne);
    solveAllBttn.addEventListener('click', solvingAll);

    updateBttnsCaptions();
    resetTimerAutoMode();
}



function updateBttnsCaptions()
{
  const solveAllBttn = document.getElementById("solveAllBttn");
  const startBttn = document.getElementById("startBttn");
  if (isAutoMode) {
        solveAllBttn ? solveAllBttn.innerText = "PAUSE ALL" : null;
        startBttn    ? startBttn.innerText = "Stop" : null;
    } else {
        solveAllBttn ? solveAllBttn.innerText = "SOLVE ALL" : null;
        startBttn    ? startBttn.innerText = "Start" : null;
    }
}


function solvingAll()
{
    setAutoMode(!isAutoMode);
    updateBttnsCaptions();
    resetTimerAutoMode();
}

function solveOne()
{
    const practiceAgain = document.querySelector('[data-test="player-practice-again"]');
    if (practiceAgain !== null && isAutoMode) {
        practiceAgain.click();
        return;
    }
    
     if(document.querySelector('[data-test="session-complete-slide"]') && isAutoMode && !practiceAgain && window.innerWidth <= 768 && window.location.pathname === '/practice') {
       window.location.assign('/practice');
      return;
    }

   let subType = "";
   try {
        window.sol = findReact(document.querySelectorAll(mainLessonFormClass)[0]).props.currentChallenge;
        subType = window.sol.challengeGeneratorIdentifier.specificType;
    } catch {
        let next = document.querySelector('[data-test="player-next"]');
        if (next) {
            next.click();
        }
        resetTimerAutoMode();
        return;
    }
    if (!window.sol) {
        resetTimerAutoMode();
        return;
    }

    let nextButton = document.querySelector('[data-test="player-next"]');
    if (!nextButton) {
        resetTimerAutoMode();
        return;
    }


     switch(window.sol.type) {
       case "listenMatch":
       case "listenIsolation":
       case "listenTap":
       case "speak":       
            const buttonSkip = document.querySelector('button[data-test="player-skip"]');
            if (buttonSkip) {
                buttonSkip.click();
            }
       break;
       case "translate":
         switch(subType)
         {
           case "reverse_translate":
              const elm = document.querySelector('textarea[data-test="challenge-translate-input"]');
              if(elm) {
                const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value").set;
                nativeInputValueSetter.call(elm, window.sol.correctSolutions ? window.sol.correctSolutions[0] : window.sol.prompt);
                let inputEvent = new Event('input', {
                    bubbles: true
                });
                elm.dispatchEvent(inputEvent);
              }
              break;
           case "tap":
           case "reverse_tap":
              translateTapReverseTapSolve();
              break;
           default:
              null;
         }
         break;
       case "assist":
       case "gapFill":
           document.querySelectorAll('[data-test="challenge-choice"]')[window.sol.correctIndex]?.click();
           break;
       case "name":
          let textInput = document.querySelector('[data-test="challenge-text-input"]');
          if(textInput) {
            if(window.sol.correctSolutions) {
               let nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value").set;
               nativeInputValueSetter.call(textInput, window.sol.correctSolutions[0]);
               let inputEvent = new Event('input', {
                  bubbles: true
               });
               textInput.dispatchEvent(inputEvent);
            }
          }
         break;
       case "partialReverseTranslate":
          let elm = document.querySelector('[data-test*="challenge-partialReverseTranslate"]')?.querySelector("span[contenteditable]");
          if(elm) {
            let nativeInputNodeTextSetter = Object.getOwnPropertyDescriptor(Node.prototype, "textContent").set;
            nativeInputNodeTextSetter.call(elm, window.sol?.displayTokens?.filter(t => t.isBlank)?.map(t => t.text)?.join()?.replaceAll(',', '') );
            let inputEvent = new Event('input', {
                bubbles: true
            });
            elm.dispatchEvent(inputEvent);
          }
         break;
       default:
         null;
     }

    nextButton.click();
    resetTimerAutoMode();
}

function translateTapReverseTapSolve() {
    const all_tokens = document.querySelectorAll('[data-test$="challenge-tap-token"]');
    const correct_tokens = window.sol.correctTokens;
    const clicked_tokens = [];
    correct_tokens.forEach(correct_token => {
        const matching_elements = Array.from(all_tokens).filter(element => element.textContent.trim() === correct_token.trim());
        if (matching_elements.length > 0) {
            const match_index = clicked_tokens.filter(token => token.textContent.trim() === correct_token.trim()).length;
            if (match_index < matching_elements.length) {
                matching_elements[match_index].click();
                clicked_tokens.push(matching_elements[match_index]);
            } else {
                clicked_tokens.push(matching_elements[0]);
            }
        }
    });
}

 function resetTimerAutoMode()
 {
    if(isAutoMode) {
        clearTimeout(solveTimerId);
        solveTimerId = setTimeout(solveOne, solveSpeedList[solveSpeed]);
    }
 }

function findReact(dom) {
    let reactProps = Object.keys(dom.parentElement).find((key) => key.startsWith("__reactProps$"));
    let child = dom?.parentElement?.[reactProps]?.children;
    return child?.props?.children?._owner?.stateNode ?? child?._owner?.stateNode;
}