NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name Duolingo Mods
// @description Includes Lesson Review, Easy Accents, and other miscellaneous mods.
// @match *://www.duolingo.com/*
// @author HodofHod
// @namespace HodofHod
// @version 0.3.2
// ==/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
*/
function inject(f) {
var script = document.createElement('script');
script.type = 'text/javascript';
script.textContent = '(' + f.toString() + ')()';
script.setAttribute("name", "duo-mods");
document.head.appendChild(script);
}
inject(main);
function main(){
//modified from developer.mozilla.org/en-US/docs/Web/API/document.cookie
function getCookie(sKey) {
return unescape(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + escape(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
}
function setCookie(sKey, sValue) {
document.cookie = escape(sKey) + "=" + escape(sValue) +
"; expires=Fri, 31 Dec 9999 23:59:59 GMT; domain=duolingo.com; path=/";
}
var scripts = {
'Lesson Review' : {id: 'lesson_review', url: 'duolingo-lesson-review',
desc: 'Adds the ability to go back and review missed problems'},
'Easy Accents': {id: 'easy_accents', url: 'duo-easy-accents',
desc: 'Type accented characters easily in lessons, using the Alt key'}
},mods = {
'Discussion Search': {id: 'discussion_search', func: discussionSearch,
desc: 'Expandable search box on the discussions page'},
'Notification Links': {id: 'notification_links', func: notificationLinks,
desc: 'Clicking on a notification will take you to its event page'},
'Open in New Tab': {id: 'ctrl_new_tab', func: ctrlNewTab,
desc: 'Holding ctrl when clicking a link opens a new tab. Like it\'s supposed to'},
'Comment Links': {id: 'comment_links', func: commentLinks,
desc: 'Turns the timestamps underneath comments into direct links to themselves'}
};
function addMods(){
function addScript(URL, name) {
var script = document.createElement("script");
script.setAttribute("src", URL);
script.setAttribute("name", name);
document.head.appendChild(script);
}
var base = ['//rawgithub.com/HodofHod/Userscripts/master/', '.user.js'];
$.each(scripts, function(title, val){
var esc_name = val.id;
if (getCookie(esc_name) !== 'false' && !$('script[name='+esc_name+']')[0]){
addScript(base.join(val.url), esc_name);
}
});
$.each(mods, function(title, val){
var esc_name = val.id;
if (getCookie(esc_name) !== 'false'){
$('*').off('.' + esc_name);
val.func(val.id);
}
});
}
addMods();
$(document).on('mouseover', '#app.settings', function(){
if (!$('#mod-settings').length){
console.log('modded');
$('.radio-buttons:last').parents('li').after('<hr><li id="mod-settings" style="margin:-25px 0 20px 0;"/><hr>');
var template = $('<li style="float:left; margin-top:20px;">\
<label class="label"></label>\
<input class="border" type="checkbox" style="float:left; margin:4px 20px 0 30px;">\
</li>');
$.each($.extend({}, scripts, mods), function(title, val){
var esc_name = val.id;//[0],
$setting = template.clone().find('label').text(title).attr('title', val.desc)//[2])
.end().find('input').attr('id', esc_name).end();//this line is really unnecessary.
cookie = getCookie(esc_name),
enabled = (cookie === "true" || !cookie);
$('#mod-settings').append($setting);
$setting.find('input').prop('checked', enabled);
$setting.find('input').on('change', function(e){
var checked = $(this).prop('checked');
setCookie(esc_name, checked), addMods();
if (!checked){
$('script[name='+esc_name+']').remove();
$('*').off('.' + esc_name);
duo.js_version = '';//forces a page reload when navigating away.
}
});
});
}
});
function discussionSearch(nspace){
$(document).on('input.'+nspace, 'input[name=search]', function () {
var $textarea = $(this),
$searchtools = $('.comment-rankings>.search-topics, #ask-question');
if (!this.expanded && this.value.length >= 8){
$(this).prop('expanded', true);
$('.nav-tabs').css({'clear': 'both'});
$('.comment-rankings>h1').css({'position': 'absolute'});
$searchtools.animate({
'margin-top': '50px'
}, 250, function () {
//The textarea has a transition css property already
//So it'll animate on its own.
$textarea.css({'width': '415px', 'margin-bottom': '30px'});
});
} else if (this.expanded && this.value.length < 8) {
$(this).css({'width': '', 'margin-bottom': ''});
$searchtools.delay(250).animate({
'margin-top': '0px',
}, 300);
$(this).prop('expanded', false);
}
});
}
function notificationLinks(nspace){
$(document).on('mouseover.'+nspace, '#popover-notifications:not(.event-linked)', function(){
$(this).addClass('event-linked');
var link = $('<a class="event_link">').css({
position: 'absolute', 'z-index': '1',
width: '100%', height: '100%',
top: 0, left: 0
}),
ids = $.map(duo.user.get('notification_events'), function(el){
return el.id;
});
$('.list-notifications>li').css('position', 'relative').each(function(i){
$(this).find('a').css({'z-index': '9', 'position': 'relative'}).end()
.prepend(link.clone().attr('href', '/event/'+ids[i]));
});
$('.event_link').hover(function () {
$(this).css('background-color', 'rgba(0, 0, 0, 0.009)');
}, function () {
$(this).css('background-color', '');
});
});
}
function ctrlNewTab(nspace){
$('body').on('click.' + nspace, 'a:not([data-bypass])', function (e){
if (e.ctrlKey){
e.stopImmediatePropagation();
$(this).attr('target', '_blank').one('mouseout', function(){
$(this).removeAttr('target');
});
}
});
}
function commentLinks(nspace){
commentTimes();
$(document).on('mouseover', '.discussion-comments-list:not(.dlinked)', function (){
$(this).addClass('dlinked');
base_url = document.location.pathname.replace(/\$.+$/, '');
$('li[id*=comment-] .body').each(function(){
var $timestamp = $(this).next('.footer').contents().filter(function(){
return this.nodeType === 3;
}),
direct = '$comment_id=' + this.id.replace(/^body-(\d+)$/, '$1'),
$link = $('<a class="direct-comment-link">')
.attr('href', base_url+direct)
.text($timestamp.text());
$timestamp.replaceWith($link);
});
});
}
function commentTimes(){
var comViewRender = duo.CommentView.prototype.render,
listViewRender = duo.CommentListView.prototype.render,
CV, LV;
duo.CommentView.prototype.render = function(){
CV = this.model;
return comViewRender.apply(this, arguments);
};
duo.CommentListView.prototype.render = function(){
LV = this;
return listViewRender.apply(this, arguments);
};
$(document).on('mouseover', '.list-discussion-item-footer:not(.timeadded), .discussion-main-detail:not(.timeadded)', function(){
$(this).addClass('timeadded');
//id is only for list items and comment threads, not top comment posts.
var id = this.parentNode.id.replace('comment-',''),
post = (!$(this).is('footer') ? CV
: $(LV.collection.models).filter(function(){ return this.id==id; })[0]),
$timestamp = $(this).contents().filter(function(){return this.nodeType==3;}).eq(0);
$timestamp.replaceWith(
$('<a>').attr('title', gettime(post))
.text($timestamp.text())
);
});
$(document).on('mouseover', '.discussion-comments-list.dlinked:not(.timeadded)', function(){
$(this).addClass('timeadded');
var comments = CV.get('comments').models;
$.each(comments, function(){
$('[id$=comment-'+this.id + ']').find('.direct-comment-link')
.attr('title', gettime(this));
});
});
function gettime(a){return a.get('datetime_string').replace(/T([\d:]+)Z/,' at $1 GMT')}
}
}