SolverDuo

It solves practice lessons automatically or manually, to increase xp use DuoSolverGrinder.

// ==UserScript==
// @name        SolverDuo
// @namespace   Violentmonkey Scripts
// @match       https://*.duolingo.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @require     https://cdn.tailwindcss.com
// @version     1.0.6
// @author      DuoSolverGrinder, has as base DuoPower modified totally.
// @description It solves practice lessons automatically or manually, to increase xp use DuoSolverGrinder.
// To grind xp automatically visit 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 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>
`;


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)=> element.classList.remove('bg-gray-900','text-white','font-bold'));
  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":
            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;
}