您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Greasy Fork is available in English.
Shows the advancement / remaining time
// ==UserScript== // @name Lynda Countdown // @description Shows the advancement / remaining time // @match https://www.lynda.com/*.html* // @run-at document-end // @version 1.1.1 // @grant none // @namespace http://greasyfork.icu/users/213706 // ==/UserScript== NodeList.prototype.forEach = Array.prototype.forEach; if(typeof unsafeWindow == "undefined") { unsafeWindow = window; } var LyndaCountdown = { videoList: {}, totaltime: 0, watchtime: 0, /** * Called when the page loads * - calculate the total time * - the watch time * - add the event listeners * - display the countdown */ init: function() { var toc = document.getElementById('toc'); if(!toc) { return; } this.totaltime = 0; this.watchtime = 0; // Observer current video var observer = new MutationObserver(this.mutationCallback.bind(this)); // Loop videos in Table Of Contents toc.querySelectorAll('.toc-video-item').forEach(function(node){ let id = node.getAttribute('data-video-id'), duration = node.querySelector('.video-duration').innerHTML.trim(), icon = node.querySelector('.watch-trigger'); if(!duration) { return } observer.observe(node, { attributes: true }); // Get duration of video in seconds let [matching, h, m, s] = duration.match(/^(\d+h)? *(\d+m)? *(\d+s)?$/), time = 0; if(h) time += parseInt(h.slice(0,-1)) * 60 * 60; if(m) time += parseInt(m.slice(0,-1)) * 60; if(s) time += parseInt(s.slice(0,-1)); // Check if the video has been watched or not let watched = icon.classList.contains('eye'); // Add to the list this.videoList[id] = {time, watched}; this.totaltime += time; this.watchtime += watched ? time : 0; }.bind(this)); // Display the countdown this.render(); }, /** * Called whenever the attribute of a .toc-video-item changes * @param Array mutationsList */ mutationCallback: function(mutationsList) { for(let mutation of mutationsList) { if (mutation.type !== 'attributes' || mutation.attributeName !== 'class') { return; } if(mutation.target.classList.contains('current')) { this.currentVideo(mutation.target); } } }, /** * Called when the current active video changes * @param DOMElement node */ currentVideo: function(node) { var id = node.getAttribute('data-video-id'); // The user already watched this video if(this.videoList[id].watched) { return; } // Update the watch time this.videoList[id].watched = 1; this.watchtime += this.videoList[id].time; this.updateCountdown(); }, /** * Calculate the circumference of a circle knowing its radius * @param float r * @return float circ */ circ: function(r) { return 2 * Math.PI * r; }, /** * Called by init * - create the skeleton of the countdown * - update the progress bar * - display the countdown */ render: function() { // https://codepen.io/xgad/post/svg-radial-progress-meters var r = 22, circ = Math.round(this.circ(r), 3); // Create the HTML skeleton var div = unsafeWindow.document.createElement('div'); div.setAttribute('id', 'countdown'); div.innerHTML = ` <div class="txt"></div> <svg class="clip" width="50" height="50"> <circle class="bg" cx="50%" cy="50%" r="${r}"></circle> <circle class="arc" cx="50%" cy="50%" r="${r}"></circle> </svg> <style> #countdown { position: absolute; top: 12px; right: 120px; } #countdown .bg { fill: none; stroke-width: 5px; stroke: #ddd; } #countdown .arc { fill: none; stroke-width: 5; stroke: #ffb900; stroke-linecap: round; stroke-dasharray: ${circ}; stroke-dashoffset: 0; transform: rotate(-90deg); transform-origin: 50% 50%; animation: big 1.5s ease-in-out; } #countdown .txt { position: absolute; left: 0; top: 50%; transform: translateY(-50%); width: 100%; text-align: center; white-space: nowrap; } </style>`; // Update the progress bar this.countdown = { circ, txtElem: div.querySelector('.txt'), arcElem: div.querySelector('.arc'), totalTxt: this.timeToHuman(this.totaltime) }; this.updateCountdown(); // Add to the page document.querySelector('.title-banner').appendChild(div); }, /** * Updates the countdown * @param integer percent - Ex: 5 * @param string remainingTxt - Ex: 2h 30m */ updateCountdown: function() { var remains = this.totaltime - this.watchtime, percent = remains <= 0 ? 100 : Math.round(this.watchtime / this.totaltime * 100, 3), deg = Math.ceil(this.countdown.circ * (100 - percent) / 100); var txt_remains, txt_title; if(percent == 100) { txt_remains = 'Done'; txt_title = 'You have completed 100% of this course'; } else { txt_remains = `${this.timeToHuman(remains)}<br>left`; txt_title = `You have completed ${parseInt(percent)}% of this course (${this.timeToHuman(this.watchtime)} / ${this.countdown.totalTxt})`; } this.countdown.arcElem.style['stroke-dashoffset'] = deg; this.countdown.txtElem.setAttribute('title', txt_title); this.countdown.txtElem.innerHTML = txt_remains; }, /** * Returns a string to display the time * @param integer seconds * @return string time */ timeToHuman(seconds) { if(seconds <= 60) { return seconds + 's'; } if(seconds <= 3600) { return parseInt(seconds / 60) + 'm ' + (seconds % 60).toString().padStart(2, '0') + 's'; } return parseInt(seconds / 3600) + 'h ' + (parseInt(seconds / 60) % 60).toString().padStart(2, '0') + 'm'; } }; unsafeWindow.onload = setTimeout(function(){ LyndaCountdown.init(); },500);