A Better Safari Books Online Experience : http://denizkumsal.com/programming/a-better-safari-books-online-experience/
var course_id = RegExp(/\d+/g).exec(location.href)[0]; | |
var styles = { | |
pagewrapid: { | |
width: "1270px", | |
margin: "0 auto" | |
}, | |
p: { | |
"font-size": "16px !important", | |
"line-height": "1.3rem !important" | |
} | |
}; | |
function rearrange(){ | |
return new Promise((resolve, reject) => { | |
pagewrapid = document.querySelector("#pagewrapid"); | |
pagewrapid.setAttribute("style", stylize("pagewrapid")); | |
p = document.querySelectorAll(".htmlcontent p"); | |
p.forEach(p=>p.setAttribute("style", stylize("p"))); | |
document.querySelector("#readerleft").style.height = document.querySelector("#readerright").style.height; | |
resolve(); | |
}); | |
} | |
function saveBookmark() { | |
new Promise((resolve, reject) => { | |
localStorage.setItem( | |
course_id, | |
document.querySelector(".current").dataset.xmlid | |
); | |
resolve(); | |
}); | |
} | |
function loadBookmark() { | |
var video_id = localStorage.getItem(course_id); | |
if (video_id) { | |
Array.from(document.querySelectorAll("#lefttoc a")) | |
.filter(a => a.dataset.xmlid === video_id)[0] | |
.click(); | |
} | |
} | |
function stylize(id) { | |
return Object.keys(styles[id]) | |
.reduce((cssText, key) => cssText.concat(`${key}:${styles[id][key]};`), []) | |
.join(""); | |
} | |
$(document).ajaxSuccess((event, xhr, settings) => { | |
if (/_ajax_htmlview/.test(settings.url)) { | |
rearrange() | |
.then(saveBookmark); | |
} | |
}); | |
rearrange().then(loadBookmark); |
var kdp; | |
var scrubInSeconds = 10; | |
var altModifier = 2; | |
var shiftModifier = 3; | |
var paused = false; | |
var tocWrapperScrollTop = 0; | |
var tocWrapperScrollEventAttached = false; | |
var course_id = RegExp(/\d+/g).exec(location.href)[0]; | |
var styles = { | |
percentage: { | |
position: "absolute", | |
right: "25px", | |
top: "25px", | |
"font-size": "larger", | |
"font-weight": "bold" | |
}, | |
tocWrapper: { | |
overflow: "auto", | |
height: "550px" | |
}, | |
pagewrapid: { | |
width: "1570px", | |
display: "flex", | |
justifyContent: "space-around", | |
border: "1px solid #7990A2", | |
"border-radius": "5px" | |
}, | |
metadata_flashactive: { | |
alignSelf: "flex-start" | |
}, | |
shadowBox1: { | |
"border-radius": 0, | |
padding: 0, | |
margin: 0, | |
border: 0 | |
}, | |
catalog_container: { | |
height: "606px", | |
margin: 0, | |
padding: "25px", | |
border: 0, | |
"border-radius": 0, | |
position: "relative" | |
}, | |
bcv_next: { | |
top: "-12%" | |
}, | |
bcv_previous: { | |
top: "-12%", | |
right: "20px", | |
left: "inherit" | |
} | |
}; | |
var percentage = document.createElement("div"); | |
percentage.setAttribute("style", stylize("percentage")); | |
document.querySelector(".catalog_container").appendChild(percentage); | |
function rearrange() { | |
return new Promise((resolve, reject) => { | |
Array.from(document.querySelector("#pagewrapid").childNodes) | |
.filter( | |
child => | |
child.nodeName !== "INPUT" && | |
child.id !== "metadata_flashactive" && | |
child.className !== "catalog_container" | |
) | |
.forEach(selector => selector.remove()); | |
pagewrapid = document.querySelector("#pagewrapid"); | |
toc = document.querySelector(".videotoc"); | |
tocWrapper = document.createElement("div"); | |
tocWrapper.id = "tocWrapper"; | |
toc.parentNode.appendChild(tocWrapper); | |
if (tocWrapperScrollEventAttached === false) attachTocWrapperScrollEvent(); | |
tocWrapper.appendChild(toc); | |
tocWrapper.setAttribute("style", stylize("tocWrapper")); | |
tocWrapper.scrollTop = tocWrapperScrollTop; | |
pagewrapid.setAttribute("style", stylize("pagewrapid")); | |
document | |
.querySelector("#metadata_flashactive") | |
.setAttribute("style", stylize("metadata_flashactive")); | |
document | |
.querySelector(".shadowBox1") | |
.setAttribute("style", stylize("shadowBox1")); | |
document | |
.querySelector(".catalog_container") | |
.setAttribute("style", stylize("catalog_container")); | |
document | |
.getElementById("bcv_next") | |
.setAttribute("style", stylize("bcv_next")); | |
document | |
.getElementById("bcv_previous") | |
.setAttribute("style", stylize("bcv_previous")); | |
resolve(); | |
}); | |
} | |
function attachTocWrapperScrollEvent() { | |
tocWrapperScrollEventAttached = true; | |
tocWrapper.onscroll = event => (tocWrapperScrollTop = event.target.scrollTop); | |
} | |
function keyEvents(event) { | |
switch (event.key) { | |
case ".": | |
document.getElementById("bcv_next").click(); | |
break; | |
case ",": | |
document.getElementById("bcv_previous").click(); | |
break; | |
case "ArrowUp": | |
event.altKey | |
? sr | |
.increment() | |
.then(speed => | |
kdp.sendNotification("playbackRateChangeSpeed", speed) | |
) | |
: vr | |
.increment() | |
.then(volume => kdp.sendNotification("changeVolume", volume)); | |
break; | |
case "ArrowDown": | |
event.altKey | |
? sr | |
.decrement() | |
.then(speed => | |
kdp.sendNotification("playbackRateChangeSpeed", speed) | |
) | |
: vr | |
.decrement() | |
.then(volume => kdp.sendNotification("changeVolume", volume)); | |
break; | |
case "ArrowRight": | |
adjustTime("forward", event); | |
break; | |
case "ArrowLeft": | |
adjustTime("backward", event); | |
break; | |
case " ": | |
kdp.sendNotification(paused ? "doPlay" : "doPause"); | |
paused = !paused; | |
break; | |
case "0": | |
case "1": | |
case "2": | |
case "3": | |
case "4": | |
case "5": | |
case "6": | |
case "7": | |
case "8": | |
case "9": | |
adjustTime("headshot", event); | |
break; | |
default: | |
break; | |
} | |
} | |
class Range { | |
constructor(value, minimum, maximum, interval) { | |
this.value = value; | |
this.minimum = minimum; | |
this.maximum = maximum; | |
this.interval = interval; | |
} | |
increment() { | |
return new Promise((resolve, reject) => { | |
var newValue = this.value + this.interval; | |
this.value = | |
newValue >= this.maximum ? this.maximum : parseFloat(newValue); | |
resolve(this.value); | |
}); | |
} | |
decrement() { | |
return new Promise((resolve, reject) => { | |
var newValue = this.value - this.interval; | |
this.value = | |
newValue <= this.minimum ? this.minimum : parseFloat(newValue); | |
resolve(this.value); | |
}); | |
} | |
} | |
class VolumeRange extends Range { | |
constructor() { | |
super(1, 0.1, 1, 0.1); | |
} | |
} | |
class SpeedRange extends Range { | |
constructor() { | |
super(1.25, 0.25, 2, 0.25); | |
} | |
} | |
function stylize(id) { | |
return Object.keys(styles[id]) | |
.reduce((cssText, key) => cssText.concat(`${key}:${styles[id][key]};`), []) | |
.join(""); | |
} | |
function adjustTime(transition, event) { | |
var currentTime = kdp.evaluate("{video.player.currentTime}"); | |
var timeAdjustment = | |
scrubInSeconds * | |
(event.altKey ? altModifier : 1) * | |
(event.shiftKey ? shiftModifier : 1); | |
if ( | |
transition === "forward" && | |
currentTime + timeAdjustment < kdp.evaluate("{duration}") | |
) { | |
kdp.sendNotification("doSeek", currentTime + timeAdjustment); | |
} | |
if (transition === "backward" && currentTime - timeAdjustment > 0) { | |
kdp.sendNotification("doSeek", currentTime - timeAdjustment); | |
} | |
if (transition === "headshot") { | |
kdp.sendNotification( | |
"doSeek", | |
parseInt(event.key) / 10 * kdp.evaluate("{duration}") | |
); | |
} | |
} | |
function saveBookmark() { | |
new Promise((resolve, reject) => { | |
localStorage.setItem( | |
course_id, | |
document.querySelector(".clipselected").dataset.clipRef | |
); | |
resolve(); | |
}); | |
} | |
function loadBookmark() { | |
var video_id = localStorage.getItem(course_id); | |
if (video_id) { | |
Array.from(document.querySelectorAll(".videotoc a")) | |
.filter(a => a.dataset.clipRef === video_id)[0] | |
.click(); | |
document.getElementById("tocWrapper").scrollTop = document.querySelector( | |
".clipselected" | |
).offsetParent.offsetTop; | |
} | |
} | |
function calculateWatchedTime() { | |
var totalTime = 0, | |
watchedTime = 0; | |
var selectedClipIndex = Array.from(document.querySelectorAll(".videotoc a")) | |
.filter(a => a.dataset.clipRef) | |
.map(a => a.className) | |
.indexOf("clipselected"); | |
var spans = Array.from(document.querySelectorAll(".videotoc p span")).filter( | |
span => { | |
return span.parentElement.classList[1] === "time" && span.innerText; | |
} | |
); | |
totalTime = collectTime(spans); | |
spans.splice(selectedClipIndex); | |
watchedTime = collectTime(spans); | |
return `${Math.round(watchedTime / totalTime * 100)}%`; | |
} | |
function collectTime(spans) { | |
var hour = 0, | |
min = 0, | |
second = 0; | |
spans.forEach(span => { | |
var chunks = span.innerText.split(":"); | |
hour += parseInt(chunks[0]); | |
min += parseInt(chunks[1]); | |
second += parseInt(chunks[2]); | |
}); | |
return hour * 60 * 60 + min * 60 + second; | |
} | |
function setPercentage() { | |
percentage.innerText = calculateWatchedTime(); | |
} | |
kWidget.addReadyCallback(function(playerId) { | |
kdp = document.getElementById(playerId); | |
kdp.kBind("playerPlayEnd", function(data, id) { | |
document.getElementById("bcv_next").click(); | |
}); | |
}); | |
let vr = new VolumeRange(); | |
let sr = new SpeedRange(); | |
document.addEventListener("keydown", keyEvents); | |
$(document).ajaxSuccess((event, xhr, settings) => { | |
if (/ajaxtoc/.test(settings.url)) { | |
rearrange() | |
.then(saveBookmark) | |
.then(setPercentage); | |
} | |
}); | |
$(document).ajaxSend( | |
() => (tocWrapperScrollTop = document.getElementById("tocWrapper").scrollTop) | |
); | |
rearrange().then(loadBookmark); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This comment has been minimized.
The current version allows you to: