// ==UserScript==
// @name Literotica Downloader
// @description Single page HTML download for Literotica with improved readability
// @namespace literotica_downloader
// @include https://www.literotica.com/stories/memberpage.php*
// @include https://tags.literotica.com/*
// @include https://www.literotica.com/c/*
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// @require https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/jquery-ui.min.js
// @version 3.0
// @grant GM_info
// @grant GM_registerMenuCommand
// @grant GM.registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_openInTab
// @grant GM_getValue
// @grant GM.getValue
// @grant GM_setValue
// @grant GM.setValue
// @grant GM_notification
// @grant GM.notification
// @author Improved by a random redditor and @nylonmachete, originally by Patrick Kolodziejczyk
// ==/UserScript==
/* jshint esversion: 8 */
// Those valuse can be modifyed by the icon GreaseMonkey > User script commands > Toggle ...
console.log("Init Literotica Downloader");
var options= {
'isNightMode' : true,
'isUsernameInFilename' : true,
'isDescriptionInFilename' : false,
'isNoteInFilename' : false,
'isCategoryInFilename' : false,
'isLargeTable' : false,
'isBookmark' : false
};
// @grant
var SEPARATOR = '_';
var PREFIX_NOTE = 'RATING_';
var cssSelectorSerie ='.ser-ttl td:nth-child(1)';
var cssSelectorSingleStory ='.root-story td:nth-child(1)';
var cssSelectorChapter ='.sl td:nth-child(1)';
var bodyStyleNight = ' style="background-color:#333333; color: #EEEEEE; font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em; font-size:2.2em; padding: 50px 0 50px 0;" ';
var bodyStyle = ' style="font-family: Helvetica,Arial,sans-serif; width: 50%; margin: 0 auto; line-height: 1.5em; font-size:2.2em; padding: 50px 0 50px 0;" ';
var chapterStyle = '_style="line-height: 1.4em;" ';
var descriptionStyle = ' style="line-height: 1.2em;" ';
// Creating style for a download icon
var downloadTooltip = 'Download as html';
var bookmarkTooltip = 'Check as read';
var bookmarkCheckTooltip = 'Un-check as read';
var iconDonwload = '<i class="bi bi-arrow-down-circle" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
var iconBookmark = '<i class="bi bi-bookmark" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
var iconBookmarkCheck = '<i class="bi bi-bookmark-check-fill" style="cursor: pointer;cursor: hand;margin-right:5px; font-size:14px"></i>';
function buildFilename(title, author, description, note, category) {
var toReturn = title;
if (options.isUsernameInFilename) {
toReturn = toReturn + SEPARATOR + author;
}
if (options.isDescriptionInFilename) {
if (description != null && description != "") {
toReturn = toReturn + SEPARATOR + description.replace(/[^\w\s]/gi, '');
}
}
if (options.isNoteInFilename) {
if (note != null && note != "") {
toReturn = toReturn + SEPARATOR + PREFIX_NOTE + note;
}
}
if (options.isCategoryInFilename) {
if (category != null && category != "") {
toReturn = toReturn + SEPARATOR + category;
}
}
// Add file extension;
toReturn = toReturn + '.html';
return toReturn;
}
var headerToAdd = `<link href="https://ajax.googleapis.com/ajax/libs/jqueryui/1.12.1/themes/smoothness/jquery-ui.css" rel="stylesheet" type="text/css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.4.1/font/bootstrap-icons.css">'
'<style type="text/css">.lbIconOnStoryCardComponent {float:left}</style>
`;
$("head").append (
// loading CSS JqueryUI for options menu
headerToAdd
);
let GMsetValue, GMgetValue, GMregisterMenuCommand, GMunregisterMenuCommand, GMnotification, GMopenInTab;
async function onReadyDoc() {
'use strict';
/* Perfectly Compatible For Greasemonkey4.0+, TamperMonkey, ViolentMonkey * F9y4ng * 20210209 */
const GMinfo = GM_info;
const handlerInfo = GMinfo.scriptHandler;
const isGM = Boolean(handlerInfo.toLowerCase() === 'greasemonkey');
var panelOptionDownloader= `
<div id="literoticaDownloaderOption" title="Options Literotica Downloader" >
Please Configure the options :
<fieldset>
<legend>Interface: </legend>
<div>
<input type="checkbox" name="isLargeTable" id="isLargeTable" class="ldCheckbox">
<span>Large Story Table</span>
</div>
<div>
<input type="checkbox" name="isBookmark" id="isBookmark" class="ldCheckbox">
<span>Bookmark (locale storage)</span>
</div>
<div>Please refresh the page for thoses options</div>
</fieldset>
<fieldset>
<legend>Reading: </legend>
<div>
<input type="checkbox" name="isNightMode" id="isNightMode" class="ldCheckbox">
<span>Night Mode</span>
</div>
</fieldset>
<fieldset>
<legend>Filename: </legend>
<div>
<input type="checkbox" name="isUsernameInFilename" id="isUsernameInFilename" class="ldCheckbox">
<span>Username in filename</span>
</div>
<div>
<input type="checkbox" name="isDescriptionInFilename" id="isDescriptionInFilename" class="ldCheckbox">
<span>Description in filename (Not on series)</span>
</div>
<div>
<input type="checkbox" name="isNoteInFilename" id="isNoteInFilename" class="ldCheckbox">
<span>Rating in filename (Not on series)</span>
</div>
<div>
<input type="checkbox" name="isCategoryInFilename" id="isCategoryInFilename" class="ldCheckbox">
<span>Category in filename (Not on series)</span>
</div>
</fieldset>
</div>
`;
$('body').append(panelOptionDownloader);
// Handle of change of value by user.
$('.ldCheckbox').change(async function onChangeCheckBox(){
console.log(this.id);
GMsetValue(this.id, this.checked);
console.log('new value =>'+ await GMgetValue(this.id));
options[this.id] = this.checked;
console.log("to"+await GM.getValue('isLargeTable'))
});
$( "#literoticaDownloaderOption" ).dialog({ autoOpen: false, width: 450 });
if (isGM) {
GMsetValue = GM.setValue;
GMgetValue = GM.getValue;
GMregisterMenuCommand = GM.registerMenuCommand;
GMunregisterMenuCommand = () => {};
GMnotification = GM.notification;
console.log("It's GM !");
} else {
console.log("Other");
GMsetValue = GM_setValue;
GMgetValue = GM_getValue;
GMregisterMenuCommand = GM_registerMenuCommand;
GMunregisterMenuCommand = GM_unregisterMenuCommand;
}
// Globale registry of the options menu.
GMregisterMenuCommand("Literotica Downloader Options", () => {
$( "#literoticaDownloaderOption" ).dialog( "open" );
});
async function initCheckBox(element, index, array) {
options[element] = await GMgetValue(element,options[element]);
$("#"+element)[0].checked= options[element];
console.log(element+"-> "+options[element]);
return true;
}
// Init of all options
for(const element of Object.getOwnPropertyNames(options)){
await initCheckBox(element)
}
console.log("end Init");
console.log(options);
// Function used to return content as a file for the user.
function saveTextAsFile(textToWrite, fileNameToSaveAs) {
var textFileAsBlob = new Blob([textToWrite], {
type: 'text/javascript'
});
var downloadLink = document.createElement('a');
downloadLink.download = fileNameToSaveAs;
downloadLink.innerHTML = 'Download File';
// Firefox requires the link to be added to the DOM
// before it can be clicked.
downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
//downloadLink.onclick = destroyClickedElement;
downloadLink.style.display = 'none';
document.body.appendChild(downloadLink);
downloadLink.click();
}
// Function parsing all pages to get the storie based
function getContentOfStoie(baseURL) {
console.log("Fetching " + baseURL);
var remote;
$.ajax({
url: baseURL,
type: 'GET',
async: false,
crossDomain: true,
success: function(data) {
if ($(data).find('a.l_bJ.l_bL').size() > 0) {
remote = $(data).find('.panel.article.aa_eQ .aa_ht').html() + getContentOfStoie($(data).find('a.l_bJ.l_bL')[0].href);
} else {
remote = $(data).find('.panel.article.aa_eQ .aa_ht').html();
}
}
});
return remote;
}
function getABookForSerieDiv(myDiv) {
var title = getTitleSerieFormDiv(myDiv);
// Get the X Part Series
var descriptionSeries = getDescriptionSerieFormDiv(myDiv);
alert("Starting building file for " + title + " of " + getAuthor() + ".\nPlease wait...");
var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
book += '<title>' + title + '</title>';
book += '<meta content="' + getAuthor() + '" name="author">';
if(options.isNightMode){
book += '</head>\n<body ' + bodyStyleNight + ' >';
}else {
book += '</head>\n<body ' + bodyStyle + ' >';
}
function addChapter(element, index, array) {
if ($(this).find('a').size() > 0) {
var chapeterTitle = $($(this).find('td a')[0]).text();
var description = $($(this).find('td')[1]).text();
book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapeterTitle + '</h1>';
book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
var link = $($(this).find('a')[0]);
book += getContentOfStoie(link.attr('href'));
}
}
myDiv.nextUntil('.ser-ttl,.root-story').each(addChapter);
saveTextAsFile(book, buildFilename(title, getAuthor(), descriptionSeries, ));
}
function getAuthor(){
return $('.contactheader').text();
}
function getTitleSerieFormDiv(myDiv){
return $.trim(myDiv.text().split(':')[0]);
}
function getDescriptionSerieFormDiv(myDiv){
return $.trim(myDiv.text().split(':')[1]);
}
function getTitleChapterFormDiv(myDiv){
return $.trim($(myDiv).text().split('(')[0]);
}
function getNoteChapterFormDiv(myDiv){
return $.trim($($(myDiv)).text().split('(')[1]).replace(")", "");
}
function isSerie(myDiv){
return $(myDiv.parent('tr')).hasClass('ser-ttl');
}
function getABookForStory(link, title, note, chapterTitle, description, category, author) {
var book = '<html>\n<head>\n<meta content="text/html; charset=UTF-8" http-equiv="Content-Type">\n';
console.log('title' + title);
book += '<title>' + title + '</title>';
book += '<meta content="' + author + '" name="author">';
if(options.isNightMode){
book += '</head>\n<body ' + bodyStyleNight + ' >';
}else {
book += '</head>\n<body ' + bodyStyle + ' >';
}
if(chapterTitle != null){
book += '<h1 class=\'chapter\'' + chapterStyle + '>' + chapterTitle + '</h1>';
}
if(description != null){
book += '<h2 class=\'chapter\'' + descriptionStyle + '>' + description + '</h2>';
}
book += getContentOfStoie(link);
saveTextAsFile(book, buildFilename(title, author, description, note, category));
}
function getABookForStoryDiv(myDiv) {
var title = getTitleChapterFormDiv(myDiv);
var note = getNoteChapterFormDiv(myDiv);
var chapterTitle = null;
var description = null;
var category =null;
if ($(myDiv).find('a').size() >= 0) {
chapterTitle = $($(myDiv.parent()).find('td a')[0]).text();
description = $($(myDiv.parent()).find('td')[1]).text();
category = $($(myDiv.parent()).find('td')[2]).text();
}
var link = $($(myDiv).find('a')[0]).attr('href');
console.log("chapterTitle:"+chapterTitle)
getABookForStory(link, title, note, chapterTitle, description, category, getAuthor());
}
/*****
*
* Section download on author
*
****/
$(cssSelectorSerie).prepend('<span class="ldSerie"> ' + iconDonwload +'</span>');
$('.ldSerie').click(function() {
getABookForSerieDiv($(this).parent().parent());
});
var idIcon = Math.floor(Math.random() * 1000);
$(cssSelectorSingleStory+','+cssSelectorChapter).prepend('<span id="' + idIcon + '" class="ldChapter"> ' + iconDonwload + '</span>');
$('.ldChapter').click(function() {
getABookForStoryDiv($(this).parent());
});
function getTitleChapterFormStoryCardComponent(cardComponent){
return $(cardComponent).find('.ai_ii').text();
}
function getNoteChapterFormStoryCardComponent(cardComponent){
return $(cardComponent).find('.K_H').text();
}
function getAuthorChapterFormStoryCardComponent(cardComponent){
return $(cardComponent).find('[typeof="Person"] .ai_im').text();
}
function getABookForStoryOnStoryCardComponent(cardComponent) {
console.log(cardComponent);
var title = getTitleChapterFormStoryCardComponent(cardComponent);
var note = getNoteChapterFormStoryCardComponent(cardComponent);
var chapterTitle = title;
var description = $(cardComponent).find('.ai_ij').text();
var category = $(cardComponent).find('.ai_im').text();
var link = $($(cardComponent).find('.ai_ii')[0]).attr('href');
var author = getAuthorChapterFormStoryCardComponent(cardComponent);
console.log("title:"+title)
console.log("note:"+note)
console.log("chapterTitle:"+chapterTitle)
console.log("description:"+description)
console.log("category:"+category)
console.log("link:"+link)
getABookForStory(link, title, note, chapterTitle, description, category, author);
}
/*****
*
* Section download on category
*
****/
var idIcon = Math.floor(Math.random() * 1000);
$(".b-slb-item").prepend('<span id="' + idIcon + '" class="ldChapter lbIconOnStoryCardComponent"> ' + iconDonwload + '</span>');
$('.ldChapter').click(function() {
getABookForStoryItemCategory($(this).parent());
});
function getTitleChapterFormStoryOnItemCategory(cardComponent){
return $(cardComponent).find('h3 a').text();
}
function getNoteChapterFormStoryOnItemCategory(cardComponent){
return $(cardComponent).find('.b-slib-rating').text();
}
function getAuthorChapterFormStoryOnItemCategory(cardComponent){
return $(cardComponent).find('.b-user-info-name').text();
}
function getABookForStoryItemCategory(cardComponent) {
console.log(cardComponent);
var title = getTitleChapterFormStoryOnItemCategory(cardComponent);
var note = getNoteChapterFormStoryOnItemCategory(cardComponent);
var chapterTitle = title;
var description = $(cardComponent).find('.b-slib-description')[0].childNodes[0].nodeValue;
description = description.substring(0,description.length-4)
var category = $('h1').text().replace(' Stories Hub', "");
var link = $($(cardComponent).find('h3 a')[0]).attr('href');
var author = getAuthorChapterFormStoryOnItemCategory(cardComponent);
console.log("title:"+title)
console.log("note:"+note)
console.log("chapterTitle:"+chapterTitle)
console.log("description:"+description)
console.log("category:"+category)
console.log("link:"+link)
getABookForStory(link, title, note, chapterTitle, description, category, author);
}
/*****
*
* Section download on tags
*
****/
$(document).on('DOMNodeInserted', function(e) {
if ( $(e.target).hasClass('ai_gJ') ) {
// Créé une instance de l'observateur lié à la fonction de callback
$(e.target).find('.ai_iG').prepend('<span class="ldDownloadTag lbIconOnStoryCardComponent"> ' + iconDonwload +'</span>');
$('.ldDownloadTag.lbIconOnStoryCardComponent').unbind( "click" ).click(function() {
getABookForStoryOnStoryCardComponent($(this).parent().parent());
});
}
});
/****
*
* Section Bookmark
*
****/
function isStoryCardComponent(myDiv){
return $(myDiv).hasClass('ai_gJ');
}
function isItemCategory(myDiv){
return $(myDiv).hasClass("b-slb-item");
}
function getBookmarkKey(myDiv){
var key = "LD_BOOKMARK_";
if(isStoryCardComponent(myDiv)){
return key+getAuthorChapterFormStoryCardComponent(myDiv)+"_chapter_"+getTitleChapterFormStoryCardComponent(myDiv);
}
if(isItemCategory(myDiv)){
return key+getAuthorChapterFormStoryOnItemCategory(myDiv)+"_chapter_"+getTitleChapterFormStoryOnItemCategory(myDiv);
}
var key = key+getAuthor();
if(isSerie(myDiv)){
key =key+"_serie_"+getTitleSerieFormDiv(myDiv);
}else {
key =key+"_chapter_"+getTitleChapterFormDiv(myDiv);
}
return key;
}
async function toogleBookmark(toToogle){
var key = getBookmarkKey($(toToogle));
var oldValue = await GMgetValue(key);
if(oldValue != "1") {
await GMsetValue(key,"1");
}else {
await GMsetValue(key,null);
}
await updateBookmark($(toToogle));
}
async function updateBookmark(toToogle){
console.log("in updateBookmark");
$(toToogle).find('.ldbookMark').remove();
var key = getBookmarkKey(toToogle);
console.log(key);
var value = await GMgetValue(key);
if(options.isBookmark){
if(isStoryCardComponent(toToogle)){
if(value!=null){
$(toToogle).find('.ai_iG').prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmarkCheck +'</span>');
}else {
$(toToogle).find('.ai_iG').prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmark +'</span>');
}
$(toToogle).find('.ldbookMark').click(function() {
toogleBookmark($(this).parent().parent());
});
}else if(isItemCategory(toToogle)){
if(value!=null){
$(toToogle).prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmarkCheck +'</span>');
}else {
$(toToogle).prepend('<span class="ldbookMark lbIconOnStoryCardComponent"> ' + iconBookmark +'</span>');
}
$(toToogle).find('.ldbookMark').click(function() {
toogleBookmark($(this).parent());
});
}else {
if(value!=null){
$(toToogle).prepend('<span class="ldbookMark"> ' + iconBookmarkCheck +'</span>');
}else {
$(toToogle).prepend('<span class="ldbookMark"> ' + iconBookmark +'</span>');
}
$(toToogle).find('.ldbookMark').click(function() {
toogleBookmark($(this).parent());
});
}
}
}
$(cssSelectorSingleStory+','+cssSelectorChapter+','+cssSelectorSerie).prepend('<span class="ldbookMark"></span>');
function initBookmark(index,element , array) {
updateBookmark($(element).parent());
}
/*****
*
* Section Bookmark on category
*
****/
$(".b-slb-item").prepend('<span class="ldbookMark"></span>');
function initBookmark(index,element , array) {
updateBookmark($(element).parent());
}
/*****
*
* Section Bookmark on tags
*
****/
async function insertBookBackOnDOMNodeInserted(e) {
if ( $(e.target).hasClass('ai_gJ') ) {
// Créé une instance de l'observateur lié à la fonction de callback
$(e.target).find('.ai_iG').prepend('<span class="ldbookMark">toto</span>');
await updateBookmark($(e.target));
}
}
$(document).on('DOMNodeInserted',(e)=> insertBookBackOnDOMNodeInserted(e));
/****
*
* Section Large Table
*
****/
function updateLargeTable(){
// Enlarge table
console.log(options);
if(options.isLargeTable== true){
console.log('Enlarged table')
$('div[style*=width], table[width="733"], img[width="733"], table[width="760"]').each(function(index, value) {if($(value).width()==733 || $(value).width()==760){$(value).width("100%")}})
}
}
updateLargeTable();
$('.ldbookMark').each(initBookmark);
}
$(document).ready(onReadyDoc());