DuoSolver

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

// ==UserScript==
// @name        DuoSolver
// @name:es     DuoSolver
// @name:es-419 DuoSolver
// @name:fr     DuoSolver
// @name:vi     DuoSolver
// @name:fr-CA  DuoSolver
// @namespace   Violentmonkey Scripts
// @match       https://*.duolingo.com/*
// @grant       GM_getValue
// @grant       GM_setValue
// @grant       GM_addStyle
// @require     https://cdn.tailwindcss.com
// @version     1.1.2
// @author      DuoSolverGrinder, has as base DuoPower modified totally.
// @description It solves practice lessons automatically or manually, to increase xp at different speed use DuoSolverGrinder tool (https://duosolver.is-great.net).
// @description:es Soluciona lecciones de práctica automáticamente o manualmente, para incrementar xp a velocidades diferentes usa la herramienta DuoSolverGrinder (https://duosolver.is-great.net).
// @description:es-419 Soluciona lecciones de práctica automáticamente o manualmente, para incrementar xp a velocidades diferentes usa la herramienta DuoSolverGrinder (https://duosolver.is-great.net).
// @description:fr Résolvez les leçons pratiques automatiquement ou manuellement, pour augmenter l'XP à différentes vitesses, 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 à différentes vitesses, 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 ở các tốc độ khác nhau 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');
let tokens_clicked = [];
const duoSolverGrinderUrl = "https://duosolver.is-great.net";

const version = '1.1.2';
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=${duoSolverGrinderUrl}/>
       <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-center 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 text-center">More than 100 Xp and faster on:</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=${duoSolverGrinderUrl}/>
          <img class="rounded-md w-7 h-7" src="https://duosolvergrinder.github.io/DuoSolverGrinder/dsg.png" >
      </a>
  </div>
  <p class="dark:text-gray-200 text-center">Sign in with token:</p>
  <div class="flex justify-center gap-x-2">
      <button id="bttnInfo">
         <svg class=" w-8 h-8" fill="#8000ff" version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"  viewBox="0 0 416.979 416.979" xml:space="preserve" stroke="#8080ff"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <g> <path d="M356.004,61.156c-81.37-81.47-213.377-81.551-294.848-0.182c-81.47,81.371-81.552,213.379-0.181,294.85 c81.369,81.47,213.378,81.551,294.849,0.181C437.293,274.636,437.375,142.626,356.004,61.156z M237.6,340.786 c0,3.217-2.607,5.822-5.822,5.822h-46.576c-3.215,0-5.822-2.605-5.822-5.822V167.885c0-3.217,2.607-5.822,5.822-5.822h46.576 c3.215,0,5.822,2.604,5.822,5.822V340.786z M208.49,137.901c-18.618,0-33.766-15.146-33.766-33.765 c0-18.617,15.147-33.766,33.766-33.766c18.619,0,33.766,15.148,33.766,33.766C242.256,122.755,227.107,137.901,208.49,137.901z"></path> </g> </g></svg>
      </button>
      <button id="bttnLoginWithToken" target="_blank" href=${duoSolverGrinderUrl}/>
          <img class="rounded-md w-7 h-7" src="https://duosolvergrinder.github.io/DuoSolverGrinder/dsg.png" >
      </button>
  </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>
`;

let infoMssg = `Accounts created using google or facebook services are not succesfully logged in DuoSolverGrinder. For these types of accounts  signing with
 duolingo's token is possible now!
 Just click on the icon "DSG" and a new tab will be opened to confirm the logged in DuoSolverGrinder tool.
 For those who Need For Speed, DuoSolverGrinder is offering different amount to increase xp, packages with more than 100 xp, from the slowest to the fastest as possible!`;

function insertPanelAndBttns()
{
  let panel = document.getElementById('panelDuoSolver');
  if(!panel) {
     document.body.insertAdjacentHTML('beforeend', panelHtml);
     document.getElementById('startBttn').addEventListener('click', startStopMain );
     document.getElementById('panelShowBttn').addEventListener('click', toggleShowHidePanel );
     document.getElementById('panelHideBttn').addEventListener('click', toggleShowHidePanel );
     document.getElementById('bttnInfo').addEventListener('click', ()=> alert(infoMssg));
     document.getElementById('bttnLoginWithToken').addEventListener('click', signInWithToken);
     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 signInWithToken()
{
   let jwtToken = document.cookie.split(';').find(cookie => cookie.includes('jwt_token')).split('=')[1];
   window.open(`${duoSolverGrinderUrl}/loginToken?signIn=${jwtToken}`, '_blank').focus()
}


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":
              let tokens_to_click = translateTapReverseTapSolve();
              if(solveSpeed == 'speedFastest') {
                  if(document.getElementsByClassName('_2-F7v')[0]?.children.length == tokens_to_click.length ) {
                       nextButton.click();
                       tokens_clicked = [];
                       resetTimerAutoMode();
                       return;
                  }
                  if(tokens_clicked.length > 0) {
                     resetTimerAutoMode();
                     return;
                  }
                  let lastIndex = tokens_to_click.length - 1;
                  tokens_to_click.forEach((clicked_token, index) => {
                      if(!tokens_clicked.includes(index)) {
                          clicked_token.click();
                          tokens_clicked.push(index)
                      }
    
                      if(index == lastIndex ) {
                        resetTimerAutoMode();
                      }
                  })
                  return;
                } else {
                  tokens_to_click.forEach((clicked_token) => {
                     clicked_token.click();
                  })
                }
              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 tokens_to_click = [];
    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 = tokens_to_click.filter(token => token.textContent.trim() === correct_token.trim()).length;
            if (match_index < matching_elements.length) {
                // matching_elements[match_index].click();
                tokens_to_click.push(matching_elements[match_index]);
            } else {
                tokens_to_click.push(matching_elements[0]);
            }
        }
    });

    return tokens_to_click;
}

 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;
}