您需要先安装一个扩展,例如 篡改猴、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/core@1.6.0',
- floatingUiDom: 'https://cdn.jsdelivr.net/npm/@floating-ui/dom@1.6.3',
- 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()
- })();