您需要先安装一个扩展,例如 篡改猴、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.1 // @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* // @grant none // ==/UserScript== (function () { 'strict' var configData = { savedProgressAlreadySet: false, savingInterval: 1500, 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 FontAwesomeIcons = { trash: ['fa-solid', 'fa-trash-can'] } function createIcon(iconName, color) { const icon = document.createElement('i') const cssClasses = FontAwesomeIcons[iconName] icon.classList.add(...cssClasses) icon.style.color = color 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 currentTime = executeFnInPageContext(() => { const player = document.querySelector('#movie_player') return player.getCurrentTime() }) return currentTime } function getVideoName() { const videoName = executeFnInPageContext(() => { const player = document.querySelector('#movie_player') return player.getVideoData().title }) return videoName } function getVideoId() { if (configData.currentVideoId) { return configData.currentVideoId } const id = executeFnInPageContext(() => { const player = document.querySelector('#movie_player') return player.getVideoData().video_id }) return id } function playerExists() { const exists = executeFnInPageContext(() => { const player = document.querySelector('#movie_player') return Boolean(player) }) return exists } function setVideoProgress(progress) { window.eval('var progress =' + progress) executeFnInPageContext(() => { const player = document.querySelector('#movie_player') player.seekTo(window.progress) }) window.eval('delete progress') } function updateLastSaved(videoProgress) { const lastSaveEl = document.querySelector('.last-save-info-text') if (lastSaveEl) { lastSaveEl.innerHTML = "Last save at " + fancyTimeFormat(videoProgress) } } function saveVideoProgress() { const videoProgress = getVideoCurrentTime() const videoId = getVideoId() configData.currentVideoId = videoId configData.lastSaveTime = Date.now() updateLastSaved(videoProgress) 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) } 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 = document.querySelector('.settings-container') executeFnInPageContext(updateFloatingSettingsUi) settingsButton.addEventListener('click', () => { settingsContainer.style.display = settingsContainer.style.display === 'none' ? 'flex' : 'none' if (settingsContainer.style.display === 'flex') { executeFnInPageContext(updateFloatingSettingsUi) } }) } 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.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: 'scroll' } 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' 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.textContent = 'x' settingsContainerCloseButton.addEventListener('click', () => { settingsContainer.style.display = 'none' }) const settingsContainerStyles = { all: 'initial', position: 'absolute', fontFamily: 'inherit', flexDirection: 'column', top: '0', display: 'none', 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) document.body.appendChild(settingsContainer) 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 at :" 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 addFloatingUIDependency() { const floatingUiCore = document.createElement('script') const floatingUiDom = document.createElement('script') floatingUiCore.src = configData.dependenciesURLs.floatingUiCore floatingUiDom.src = 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() addFloatingUIDependency() } function initializeUI() { const infoEl = createInfoUI() insertInfoElement(infoEl) createSettingsUI() initializeDependencies() onChaptersReadyToMount(() => { insertInfoElementInChaptersContainer(infoEl) createSettingsUI() }) } function initialize() { onPlayerElementExist(() => { initializeUI() if (isReadyToSetSavedProgress()) { setSavedProgress() } }) setInterval(saveVideoProgress, configData.savingInterval) } initialize() })();