NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Duolingo - Lesson Review
// @description This script allows you to go back and review all the different challenges in a lesson.
// @match *://www.duolingo.com/*
// @author HodofHod
// @namespace HodofHod
// @version 0.2.4
// ==/UserScript==
/*
Copyright (c) 2013-2014 HodofHod (https://github.com/HodofHod)
Licensed under the MIT License (MIT)
Full text of the license is available at https://raw2.github.com/HodofHod/Userscripts/master/LICENSE
*/
//TODO: Inject a stylesheet and use classes instead of .css()?
function inject(f) { //Inject the script into the document
var script;
script = document.createElement('script');
script.type = 'text/javascript';
script.setAttribute('name', 'lesson_review');
script.textContent = '(' + f.toString() + ')(jQuery)';
document.head.appendChild(script);
}
inject(init);
function init(){
console.log('Duolingo Lesson Review');
var OrigSessionView = $.extend({}, duo.SessionView.prototype),
origTimesUp = duo.TimedSessionView.prototype.timesUp,
origPushState = window.history.pushState;
if (/^\/practice|skill|word_practice\/.*/.test(window.location.pathname)){ //for the first time the script is loaded, check if we're already on a page.
main();
}
window.history.pushState = function checkPage(a, b, c){ //Hijack the window's pushState so we know whenever duolingo navigates to a new page.
/^\/practice|skill|word_practice\/.*/.test(c) ? main() : cleanup();
return origPushState.call(this, a, b, c);
};
function cleanup(){
$(document).off('.hh'); //unbind any handlers from previous lessons.
$.extend(duo.SessionView.prototype, OrigSessionView);
duo.TimedSessionView.prototype.timesUp = origTimesUp;
}
function main(){
cleanup();
var lessons = {},
current_id = 1,
selected_id = current_id,
finished = false,
failed = false,
header, last_lesson_id,
redesign = duo.user.get("ab_options").site_redesign_experiment;
function save_lesson(save_id){
var $problem = $('.player-main'),
$controls = $('.player-container>footer'),
$discussion = $('#discussion-modal-container');
lessons[save_id] = $.extend(lessons[save_id] || {}, {
0: $problem.add($controls),
1: $discussion,
2: $('#app').prop('class') });
if (save_id !== 'end'){
lessons[save_id][0] = $problem.after($problem.clone()).add($controls.clone(true)).detach();
var l = lessons[save_id][0].add(lessons[save_id][1]),
resume_button = $('<button id="resume_button" class="btn success btn-lg right" tabindex="20">Resume</button>');
//Disable or hide all controls except for discuss and report
l.find('#submit_button, #skip_button, #home-button, #fix_mistakes_button').hide();
l.find('#next_button, #continue_button, #retry-button')
.after($( $('#resume_button')[0] || resume_button) ).hide();
}else{
console.log('else '+ save_id);
}
}
function replace_lesson(replace_id){
console.log('cur: ' + current_id + ', old id: ' + selected_id + ', new id ' + replace_id);
if (selected_id === replace_id){ return false; } //don't replace yourself with yourself.
if (selected_id === current_id){//if switching away from cur_lesson or end
lessons[current_id] = $.extend(lessons[current_id] || {},
{0: $('.player-main, #end-carousel').hide()
//detach footer to prevent #discussion-toggle conflicts
.add($('.player-container>footer').detach()),
1: $('#discussion-modal-container').detach(),
2: $('#app').prop('class')});
}else{
$('.player-container>footer, .player-main, #discussion-modal-container').detach();
}
var lesson = lessons[replace_id];
$('.player-container').append(lesson[0].show());
$('body').append(lesson[1]);//discussions
$('#app').prop('class', lesson[2]);
$('.hint-table, .twipsy, #controls .tooltip').hide();
if (finished && !failed){//Add or remove header as necessary, since success endview removes it.
(replace_id !== current_id) ? $('.player-container').prepend(header)
: $('.player-header').detach();
}
if (!$("#discussion-modal").length && current_id !== replace_id &&
!(failed && replace_id === last_lesson_id)) {
$('#discussion-toggle').off().one('click', function(e){
e.stopImmediatePropagation();
loadDiscussion(replace_id);//TODO: pass lessons[replace_id] instead?
$('#discussion-toggle, .close-modal-background').off().on('click', function(e){
e.stopImmediatePropagation();
$('#discussion-modal').modal('toggle');
});
});
}
selected_id = replace_id;
select_cell(selected_id);
}
function loadDiscussion(lesson_id){
var sentence = new duo.Sentence({ id : lessons[lesson_id].key }),
container = $( $('#discussion-modal-container')[0] || '<div id="discussion-modal-container"></div>' );
learning_language = duo.user.get('learning_language'),
ui_language = duo.user.get('ui_language');
sentence.fetch({
data: {
ui_language : ui_language || undefined,
learning_language : learning_language
},success: function (){
lessons[lesson_id][1] = container.appendTo(document.body);
var comments = new duo.CommentModalView({
el : container,
correct : lessons[lesson_id].correct,
model : sentence,
ui_language : ui_language,
learning_language : learning_language
});
comments.render();
}
});
}
function select_cell(cell_num){
//TODO: For redesign: add the box-shadow to .inner only.
// Inherit b-color on .nothing, and reset on .done (red uses b-image, so it won't reset that);
$('li[id^=element-]').find('.inner').andSelf().css({'box-shadow': '', 'border-right-width': '1px'});
$('li#element-'+cell_num).find('.inner')
.andSelf().css({'box-shadow': '1px 1px 3px 1px black inset',
'border-right-width': '0px'});
$('li[id^=element-]:first-child').css({'-webkit-border-radius': '9px 0 0 9px',
'border-radius': '9px 0 0 9px'});
$('li[id^=element-]:last-child').css({'-webkit-border-radius': '0 9px 9px 0',
'border-radius': '0 9px 9px 0'});
activate_arrows();
}
//Hijack the rendered() function (it runs after each problem is rendered)
duo.SessionView.prototype.rendered = function newRendered(){
if ($('#prev-arrow, #next-arrow').length !== 2){
add_arrows();
}
select_cell(selected_id);
return OrigSessionView.rendered.apply(this, arguments);
};
//Hijack the function that switches lessons.
duo.SessionView.prototype.next = function newNext(){
$('#discussion-modal-container').detach();
$('#pause_toggle').remove();
current_id = (this.model.get('position') + 1); //adjust from 0-indexed
save_lesson(current_id);
current_id += 1;
selected_id = current_id;
return OrigSessionView.next.apply(this, arguments);
};
duo.SessionView.prototype.graded = function(){
//check to see if this is being called from (and with) TimedSessionView
if (this.timer_view !== undefined){
//add pause Button
var timer = this.timer_view;
$('#pause_toggle').off().remove();//don't assume graded won't be called twice.
$('#next_button').before('<button id="pause_toggle" class="btn btn-blue btn-lg" style="margin-right:10px;">Pause</button>');
$('#pause_toggle').on('click', function(){
if (timer.paused === null){
timer.pause();
$('#next_button').attr('disabled','disabled');
$(this).text('Resume');
}else{
timer.resume();
$('#next_button').removeAttr('disabled');
$(this).text('Pause');
}
});
}
var solution = this.model.getSubmittedSolution();
OrigSessionView.graded.apply(this, arguments);
if (solution.get('incorrect') && !solution.get('try-again')){
$('li#element-' + current_id + '>.inner').andSelf()
.css('background-image', '-webkit-linear-gradient(top, #EE6969, #FF0000)')
.css('background-image', '-moz-linear-gradient(center top , #EE6969, #FF0000)');
}
lessons[current_id] = $.extend(lessons[current_id] || {}, {
key: this.model.currentElement().get("solution_key"),
correct: this.model.getSubmittedSolution().get("correct")
});
};
function finish(){
var button = $('<button id="review_button" class="btn large btn-lg btn-standard right" style="margin:0 10px 0 0;">Review</button>'),
loc = failed ? '#controls>.col-right' : '.session-end-footer';
$(loc).prepend(button);
button.css(!failed ? {position:'absolute', left:'40px'}
: {float: 'left'});
finished = true;
last_lesson_id = current_id;
current_id = 'end';
selected_id = 'end';
select_cell();//none
}
duo.TimedSessionView.prototype.timesUp = function(){
save_lesson(current_id);
finished = true;
origTimesUp.apply(this, arguments);
};
duo.SessionView.prototype.showFailView = function(){
failed = true;
save_lesson(current_id);
var r = OrigSessionView.showFailView.apply(this, arguments);
finish();
$('.close-fail').hide();
add_arrows();
return r;
};
duo.SessionView.prototype.showEndView = function(){
//Timed out lessons don't need this. Timeouts will set finished to true earlier.
//newNext() increments on the last one too, adjust for that, (except for timesUp)
current_id -= !finished;
header = $('.player-header').detach();
OrigSessionView.showEndView.apply(this, arguments);
//end view won't have loaded. Wait for mousemove on end app, where the footer exists.
$(document).on('mousemove', '#app.slide-session-end', function(){
if ($('.session-end-footer').length){
finish();
$(document).off('mousemove', '#app.slide-session-end');
}
});
};
$(document).on('click.hh', 'li[id^=element-]', function(){
var clicked_id = parseInt(this.id.replace('element-', ''), 10);
if (lessons[clicked_id] !== undefined){
replace_lesson(clicked_id);
}
});
$(document).on('click.hh', '#resume_button', function(){
replace_lesson(current_id);
if (finished && ($('#end-carousel .left').length > 1 || !$('#end-carousel .active').length)){
$('#end-carousel .item').removeClass('active next left');
$('#end-carousel .item:eq(' + $('.carousel-dots .active').data('slide') + ')').addClass('active');
$('#end-carousel .item.active').is(':last-child') && $(".carousel").carousel("pause");
}
});
$(document).on('click.hh', '#review_button', function(){
$(".carousel").carousel("pause");
replace_lesson(last_lesson_id);
});
$(document).on('click.hh', '#prev-arrow, #next-arrow', function(){
var lesson_id = (this.id === 'prev-arrow') ? selected_id-1 : selected_id+1;
if (selected_id === 'end'){
$('#review_button').click();
}else if (!lessons[lesson_id] && current_id === 'end'){
replace_lesson('end');
}else{
replace_lesson(lesson_id);
}
});
function add_arrows(){
$('#prev-arrow, #next-arrow').remove();
var arrow = $('<span></span>').css({
background: 'url("//d7mj4aqfscim2.cloudfront.net/images/sprite_mv_082bd900117422dec137f596afcc1708.png") no-repeat',
width: '24px', height: '18px', position: 'absolute', 'pointer-events':'none', 'background-position': '-323px -130px'
});
var strength = $('.strength-bar').length;
$.each(['prev', 'next'], function(i, val){
var rotate = 'rotate('+(val==='prev'?'-':'')+'90deg)';
arrow.clone().attr('id', val +'-arrow').css({
top: strength ? '22px' : '31px',
left: val==='prev' ? (strength ? '80px' : '16px') : '' ,
right: val==='next' && strength ? '154px' : '',
transform: rotate, '-webkit-transform': rotate
})[(val==='prev' ? 'prepend' : 'append') + 'To']('#progress-bar');
});
activate_arrows();
}
function activate_arrows(){
var active = {'background-position': '-323px -178px', cursor: 'pointer', 'pointer-events': ''},
inactive = {'background-position': '-323px -130px', cursor: 'initial', 'pointer-events': 'none'};
$('#prev-arrow').css( (selected_id > 1 || selected_id === 'end') ? active : inactive);
$('#next-arrow').css( (selected_id !== current_id) ? active : inactive);
}
}
}