您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Have you ever closed a YouTube video by accident, or have you gone to another one and when you come back the video starts from 0? With this extension it won't happen anymore
// ==UserScript== // @license MIT // @name Youtube Save/Resume Progress // @namespace http://tampermonkey.net/ // @version 1.5.7 // @description Have you ever closed a YouTube video by accident, or have you gone to another one and when you come back the video starts from 0? With this extension it won't happen anymore // @author Costin Alexandru Sandu // @match https://www.youtube.com/watch* // @icon https://raw.githubusercontent.com/SaurusLex/YoutubeSaveResumeProgress/refs/heads/master/youtube_save_resume_progress_icon.jpg // @grant none // ==/UserScript== (function () { "strict"; var configData = { sanitizer: null, savedProgressAlreadySet: false, savingInterval: 2000, currentVideoId: null, lastSaveTime: 0, dependenciesURLs: { floatingUiCore: "https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]", floatingUiDom: "https://cdn.jsdelivr.net/npm/@floating-ui/[email protected]", fontAwesomeIcons: "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css", } }; var state = { floatingUi: { cleanUpFn: null, settingsContainer: null, } } var FontAwesomeIcons = { trash: ["fa-solid", "fa-trash-can"], xmark: ["fa-solid", "fa-xmark"], }; function createIcon(iconName, color) { const icon = document.createElement("i"); const cssClasses = FontAwesomeIcons[iconName]; icon.classList.add(...cssClasses); icon.style.color = color; icon.style.fontSize = "16px"; return icon; } // ref: https://stackoverflow.com/questions/3733227/javascript-seconds-to-minutes-and-seconds function fancyTimeFormat(duration) { // Hours, minutes and seconds const hrs = ~~(duration / 3600); const mins = ~~((duration % 3600) / 60); const secs = ~~duration % 60; // Output like "1:01" or "4:03:59" or "123:03:59" let ret = ""; if (hrs > 0) { ret += "" + hrs + ":" + (mins < 10 ? "0" : ""); } ret += "" + mins + ":" + (secs < 10 ? "0" : ""); ret += "" + secs; return ret; } /*function executeFnInPageContext(fn) { const fnStringified = fn.toString() return window.eval('(' + fnStringified + ')' + '()') }*/ function getVideoCurrentTime() { const player = document.querySelector("#movie_player"); const currentTime = player.getCurrentTime(); return currentTime; } function getVideoName() { const player = document.querySelector("#movie_player"); const videoName = player.getVideoData().title; return videoName; } function getVideoId() { if (configData.currentVideoId) { return configData.currentVideoId; } const player = document.querySelector("#movie_player"); const id = player.getVideoData().video_id; return id; } function playerExists() { const player = document.querySelector("#movie_player"); const exists = Boolean(player); return exists; } function setVideoProgress(progress) { const player = document.querySelector("#movie_player"); player.seekTo(progress); } function updateLastSaved(videoProgress) { const lastSaveEl = document.querySelector(".last-save-info-text"); const lastSaveText = `Last save: ${fancyTimeFormat(videoProgress)}`; // This is for browsers that support Trusted Types const lastSaveInnerHtml = configData.sanitizer ? configData.sanitizer.createHTML(lastSaveText) : lastSaveText; if (lastSaveEl) { lastSaveEl.innerHTML = lastSaveInnerHtml; } } function saveVideoProgress() { const videoProgress = getVideoCurrentTime(); updateLastSaved(videoProgress); const videoId = getVideoId(); configData.currentVideoId = videoId; configData.lastSaveTime = Date.now(); const idToStore = "Youtube_SaveResume_Progress-" + videoId; const progressData = { videoProgress, saveDate: Date.now(), videoName: getVideoName(), }; window.localStorage.setItem(idToStore, JSON.stringify(progressData)); } function getSavedVideoList() { const savedVideoList = Object.entries(window.localStorage).filter( ([key, value]) => key.includes("Youtube_SaveResume_Progress-") ); return savedVideoList; } function getSavedVideoProgress() { const videoId = getVideoId(); const idToStore = "Youtube_SaveResume_Progress-" + videoId; const savedVideoData = window.localStorage.getItem(idToStore); const { videoProgress } = JSON.parse(savedVideoData) || {}; return videoProgress; } function videoHasChapters() { const chaptersSection = document.querySelector( '.ytp-chapter-container[style=""]' ); const chaptersSectionDisplay = getComputedStyle(chaptersSection).display; return chaptersSectionDisplay !== "none"; } function setSavedProgress() { const savedProgress = getSavedVideoProgress(); setVideoProgress(savedProgress); configData.savedProgressAlreadySet = true; } // code ref: https://stackoverflow.com/questions/5525071/how-to-wait-until-an-element-exists function waitForElm(selector) { return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); } const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { observer.disconnect(); resolve(document.querySelector(selector)); } }); // If you get "parameter 1 is not of type 'Node'" error, see https://stackoverflow.com/a/77855838/492336 observer.observe(document.body, { childList: true, subtree: true, }); }); } async function onPlayerElementExist(callback) { await waitForElm("#movie_player"); callback(); } function isReadyToSetSavedProgress() { return ( !configData.savedProgressAlreadySet && playerExists() && getSavedVideoProgress() ); } function insertInfoElement(element) { const leftControls = document.querySelector(".ytp-left-controls"); leftControls.appendChild(element); const chaptersContainerElelement = document.querySelector( ".ytp-chapter-container" ); chaptersContainerElelement.style.flexBasis = "auto"; } function insertInfoElementInChaptersContainer(element) { const chaptersContainer = document.querySelector( '.ytp-chapter-container[style=""]' ); chaptersContainer.style.display = "flex"; chaptersContainer.appendChild(element); } function updateFloatingSettingsUi() { const settingsButton = document.querySelector(".ysrp-settings-button"); const settingsContainer = document.querySelector(".settings-container"); const { flip, computePosition } = window.FloatingUIDOM; computePosition(settingsButton, settingsContainer, { placement: "top", middleware: [flip()], }).then(({ x, y }) => { Object.assign(settingsContainer.style, { left: `${x}px`, top: `${y}px`, }); }); } function setFloatingSettingsUi() { const settingsButton = document.querySelector(".ysrp-settings-button"); const settingsContainer = state.floatingUi.settingsContainer const { autoUpdate } = window.FloatingUIDOM; settingsButton.addEventListener("click", () => { const exists = document.body.contains(settingsContainer) if (exists) { closeFloatingSettingsUi() } else { document.body.appendChild(settingsContainer); updateFloatingSettingsUi(); state.floatingUi.cleanUpFn = autoUpdate(settingsButton, settingsContainer, updateFloatingSettingsUi); document.addEventListener('click', closeFloatingSettingsUiOnClickOutside) } }); } function closeFloatingSettingsUiOnClickOutside(event) { const settingsButton = document.querySelector(".ysrp-settings-button"); const settingsContainer = state.floatingUi.settingsContainer if (settingsContainer && !settingsContainer.contains(event.target) && !settingsButton.contains(event.target)) { closeFloatingSettingsUi(); document.removeEventListener('click', closeFloatingSettingsUiOnClickOutside); } } function closeFloatingSettingsUi() { const settingsContainer = state.floatingUi.settingsContainer settingsContainer.remove() state.floatingUi.cleanUpFn() state.floatingUi.cleanUpFn = null } function createSettingsUI() { const videos = getSavedVideoList(); const videosCount = videos.length; const infoElContainer = document.querySelector(".last-save-info-container"); const infoElContainerPosition = infoElContainer.getBoundingClientRect(); const settingsContainer = document.createElement("div"); settingsContainer.addEventListener('click', (event) => {event.stopPropagation()}) state.floatingUi.settingsContainer = settingsContainer settingsContainer.classList.add("settings-container"); const settingsContainerHeader = document.createElement("div"); const settingsContainerHeaderTitle = document.createElement("h3"); settingsContainerHeaderTitle.textContent = "Saved Videos - (" + videosCount + ")"; settingsContainerHeader.style.display = "flex"; settingsContainerHeader.style.justifyContent = "space-between"; const settingsContainerBody = document.createElement("div"); settingsContainerBody.classList.add("settings-container-body"); const settingsContainerBodyStyle = { display: "flex", flex: "1", minHeight: "0", overflow: "auto", }; Object.assign(settingsContainerBody.style, settingsContainerBodyStyle); const videosList = document.createElement("ul"); videosList.style.display = "flex"; videosList.style.flexDirection = "column"; videosList.style.rowGap = "1rem"; videosList.style.listStyle = "none"; videosList.style.marginTop = "1rem"; videosList.style.flex = "1"; videos.forEach((video) => { const [key, value] = video; const { videoName } = JSON.parse(value); const videoEl = document.createElement("li"); const videoElText = document.createElement("span"); videoEl.style.display = "flex"; videoEl.style.alignItems = "center"; videoElText.textContent = videoName; videoElText.style.flex = "1"; const deleteButton = document.createElement("button"); const trashIcon = createIcon("trash", "#e74c3c"); deleteButton.style.background = "white"; deleteButton.style.border = "rgba(0, 0, 0, 0.3) 1px solid"; deleteButton.style.borderRadius = ".5rem"; deleteButton.style.marginLeft = "1rem"; deleteButton.style.cursor = "pointer"; deleteButton.addEventListener("click", () => { window.localStorage.removeItem(key); videosList.removeChild(videoEl); settingsContainerHeaderTitle.textContent = "Saved Videos - (" + videosList.children.length + ")"; }); deleteButton.appendChild(trashIcon); videoEl.appendChild(videoElText); videoEl.appendChild(deleteButton); videosList.appendChild(videoEl); }); const settingsContainerCloseButton = document.createElement("button"); settingsContainerCloseButton.style.background = "transparent"; settingsContainerCloseButton.style.border = "none"; settingsContainerCloseButton.style.cursor = "pointer"; const xmarkIcon = createIcon("xmark", "#e74c3c"); settingsContainerCloseButton.appendChild(xmarkIcon); settingsContainerCloseButton.addEventListener("click", () => { closeFloatingSettingsUi() }); const settingsContainerStyles = { all: "initial", position: "absolute", fontFamily: "inherit", flexDirection: "column", top: "0", display: "flex", boxShadow: "rgba(0, 0, 0, 0.24) 0px 3px 8px", border: "1px solid #d5d5d5", top: infoElContainerPosition.bottom + "px", left: infoElContainerPosition.left + "px", padding: "1rem", width: "50rem", height: "25rem", borderRadius: ".5rem", background: "white", zIndex: "3000", }; Object.assign(settingsContainer.style, settingsContainerStyles); settingsContainerBody.appendChild(videosList); settingsContainerHeader.appendChild(settingsContainerHeaderTitle); settingsContainerHeader.appendChild(settingsContainerCloseButton); settingsContainer.appendChild(settingsContainerHeader); settingsContainer.appendChild(settingsContainerBody); const savedVideos = getSavedVideoList(); const savedVideosList = document.createElement("ul"); } function createInfoUI() { const infoElContainer = document.createElement("div"); infoElContainer.classList.add("last-save-info-container"); const infoElText = document.createElement("span"); const settingsButton = document.createElement("button"); settingsButton.classList.add("ysrp-settings-button"); settingsButton.style.background = "white"; settingsButton.style.border = "rgba(0, 0, 0, 0.3) 1px solid"; settingsButton.style.borderRadius = ".5rem"; settingsButton.style.marginLeft = "1rem"; const infoEl = document.createElement("div"); infoEl.classList.add("last-save-info"); infoElText.textContent = "Last save: Loading..."; infoElText.classList.add("last-save-info-text"); infoEl.appendChild(infoElText); infoEl.appendChild(settingsButton) infoElContainer.style.all = "initial"; infoElContainer.style.fontFamily = "inherit"; infoElContainer.style.fontSize = "1.3rem"; infoElContainer.style.marginLeft = "0.5rem"; infoElContainer.style.display = "flex"; infoElContainer.style.alignItems = "center"; infoEl.style.textShadow = "none"; infoEl.style.background = "white"; infoEl.style.color = "black"; infoEl.style.padding = ".5rem"; infoEl.style.borderRadius = ".5rem"; infoElContainer.appendChild(infoEl); return infoElContainer; } async function onChaptersReadyToMount(callback) { await waitForElm('.ytp-chapter-container[style=""]'); callback(); } function addFontawesomeIcons() { const head = document.getElementsByTagName("HEAD")[0]; const iconsUi = document.createElement("link"); Object.assign(iconsUi, { rel: "stylesheet", type: "text/css", href: configData.dependenciesURLs.fontAwesomeIcons, }); head.appendChild(iconsUi); iconsUi.addEventListener("load", () => { const icon = document.createElement("span"); const settingsButton = document.querySelector('.ysrp-settings-button') settingsButton.appendChild(icon) icon.classList.add('fa-solid') icon.classList.add('fa-gear') }); } function sanitizeScriptUrl(url) { return configData.sanitizer ? configData.sanitizer.createScriptURL(url) : url; } function addFloatingUIDependency() { const floatingUiCore = document.createElement("script"); const floatingUiDom = document.createElement("script"); floatingUiCore.src = sanitizeScriptUrl(configData.dependenciesURLs.floatingUiCore); floatingUiDom.src = sanitizeScriptUrl(configData.dependenciesURLs.floatingUiDom); document.body.appendChild(floatingUiCore); document.body.appendChild(floatingUiDom); let floatingUiCoreLoaded = false; let floatingUiDomLoaded = false; floatingUiCore.addEventListener("load", () => { floatingUiCoreLoaded = true; if (floatingUiCoreLoaded && floatingUiDomLoaded) { setFloatingSettingsUi(); } }); floatingUiDom.addEventListener("load", () => { floatingUiDomLoaded = true; if (floatingUiCoreLoaded && floatingUiDomLoaded) { setFloatingSettingsUi(); } }); } function initializeDependencies() { addFontawesomeIcons(); // FIXME: floating ui is not working for now addFloatingUIDependency() } function initializeUI() { const infoEl = createInfoUI(); insertInfoElement(infoEl); createSettingsUI() initializeDependencies(); onChaptersReadyToMount(() => { insertInfoElementInChaptersContainer(infoEl); createSettingsUI(); }); } function initialize() { if ( window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy ) { const sanitizer = window.trustedTypes.createPolicy("default", { createHTML: (string, sink) => string, createScript: (string, sink) => string, createScriptURL: (string, sink) => string, }); configData.sanitizer = sanitizer; } onPlayerElementExist(() => { initializeUI(); if (isReadyToSetSavedProgress()) { setSavedProgress(); } }); setInterval(saveVideoProgress, configData.savingInterval); } initialize(); })();