diff --git a/README.md b/README.md index 9591f5eb..65cc8990 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Logo - +
Logo by @munadikieh

@@ -72,7 +72,7 @@ You must have [Node.js 16](https://nodejs.org/) and npm installed. - You can also run `npm run build` (for Chrome) or `npm run build:firefox` (for Firefox) to generate a production build. -4. The built extension is now in `dist/`. You can load it in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest) or in Firefox as a [temporary extension](https://developer.mozilla.org/en-US/docs/Tools/about:debugging#loading_a_temporary_extension). +4. The built extension is now in `dist/`. You can load this folder directly in Chrome as an [unpacked extension](https://developer.chrome.com/docs/extensions/mv3/getstarted/#manifest), or convert it to a zip file to load it as a [temporary extension](https://developer.mozilla.org/en-US/docs/Tools/about:debugging#loading_a_temporary_extension) in Firefox. ### Developing with a clean profile and hot reloading diff --git a/ci/invidiouslist.json b/ci/invidiouslist.json index 4638f7c3..6617abf6 100644 --- a/ci/invidiouslist.json +++ b/ci/invidiouslist.json @@ -1 +1 @@ -["yewtu.be","invidious.snopyta.org","vid.puffyan.us","invidious.kavin.rocks","invidio.xamh.de","inv.riverside.rocks","invidious-us.kavin.rocks","invidious.osi.kr","tube.cthd.icu","yt.artemislena.eu","youtube.076.ne.jp","invidious.namazso.eu"] \ No newline at end of file +["yewtu.be","vid.puffyan.us","invidious.snopyta.org","invidious.kavin.rocks","invidio.xamh.de","inv.riverside.rocks","invidious-us.kavin.rocks","invidious.osi.kr","tube.cthd.icu","invidious.flokinet.to","yt.artemislena.eu","youtube.076.ne.jp","invidious.namazso.eu"] \ No newline at end of file diff --git a/manifest/manifest.json b/manifest/manifest.json index 3ec101fa..4c78d64f 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -1,7 +1,7 @@ { "name": "__MSG_fullName__", "short_name": "SponsorBlock", - "version": "4.1.6", + "version": "4.2.1", "default_locale": "en", "description": "__MSG_Description__", "homepage_url": "https://sponsor.ajay.app", diff --git a/package-lock.json b/package-lock.json index a6d80792..419c2e2f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4256,24 +4256,6 @@ "node": ">=0.10.0" } }, - "node_modules/copy-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/copy-webpack-plugin/node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -9990,8 +9972,9 @@ } }, "node_modules/minimist": { - "version": "1.2.5", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "node_modules/mkdirp": { @@ -10007,9 +9990,9 @@ } }, "node_modules/moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", "dev": true, "optional": true, "engines": { @@ -17279,16 +17262,6 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, - "schema-utils": { - "version": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, "slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -21575,8 +21548,9 @@ } }, "minimist": { - "version": "1.2.5", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==", "dev": true }, "mkdirp": { @@ -21589,9 +21563,9 @@ } }, "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==", + "version": "2.29.2", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", + "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==", "dev": true, "optional": true }, diff --git a/public/_locales/cs/messages.json b/public/_locales/cs/messages.json index e616cf6d..bb65ddc4 100644 --- a/public/_locales/cs/messages.json +++ b/public/_locales/cs/messages.json @@ -586,13 +586,13 @@ "message": "Titulky nebo když se objeví konečné karty YouTube. Není pro závěry s informacemi." }, "category_preview": { - "message": "Náhled/shrnutí" + "message": "Náhled / shrnutí" }, "category_preview_description": { "message": "Rychlé shrnutí předchozích epizod nebo náhled toho, co se objeví v aktuálním videu. Myšleno pro upravené sloučené klipy, ne pro mluvená shrnutí." }, "category_filler": { - "message": "Výplň/vtipy" + "message": "Výplň / vtipy" }, "category_filler_description": { "message": "Výplňové scény přidané jen jako přídavek nebo humor, které nejsou vyžadovány pro pochopení hlavního obsahu videa. Toto by nemělo zahrnovat segmenty poskytující kontext nebo podrobnosti na pozadí." diff --git a/public/_locales/it/messages.json b/public/_locales/it/messages.json index 8fa21d58..0e253383 100644 --- a/public/_locales/it/messages.json +++ b/public/_locales/it/messages.json @@ -562,7 +562,7 @@ "description": "Short description for this category" }, "category_interaction": { - "message": "Promemoria di Interazione (Sottoscrizione)" + "message": "Promemoria d'Interazione (Iscrizione)" }, "category_interaction_description": { "message": "Quando nel punto centrale del contenuto è presente un breve promemoria per aggiunta di mi piace, iscrizione o seguito. Se dovesse risultare esteso o riguardare qualcosa di specifico, potrebbe essere un'autopromozione." diff --git a/public/_locales/pt_PT/messages.json b/public/_locales/pt_PT/messages.json index 012946e8..ac1d4db3 100644 --- a/public/_locales/pt_PT/messages.json +++ b/public/_locales/pt_PT/messages.json @@ -118,6 +118,34 @@ "submitCheck": { "message": "Tem a certeza que pretende submeter?" }, + "whitelistChannel": { + "message": "Meter canal na Lista Branca" + }, + "removeFromWhitelist": { + "message": "Remover canal da Lista Branca" + }, + "voteOnTime": { + "message": "Votar em um segmento" + }, + "Submissions": { + "message": "Submissões" + }, + "savedPeopleFrom": { + "message": "Salvaste pessoas de " + }, + "viewLeaderboard": { + "message": "Tabela de Classificação" + }, + "recordTimesDescription": { + "message": "Enviar" + }, + "submissionEditHint": { + "message": "A edição da seção aparecerá depois que você clicar em enviar", + "description": "Appears in the popup to inform them that editing has been moved to the video player." + }, + "popupHint": { + "message": "Dica: Você pode configurar atalhos de teclado para enviar nas opções" + }, "clearTimesButton": { "message": "Limpar Intervalos" }, @@ -127,9 +155,15 @@ "publicStats": { "message": "Isto é usado na página pública de estatísticas que mostra o quanto já contríbuíu. Veje-a" }, + "Username": { + "message": "Nome de Utilizador" + }, "setUsername": { "message": "Criar nomde de utilizador" }, + "copyPublicID": { + "message": "Copiar UserID Publico" + }, "discordAdvert": { "message": "Junte-se ao discord oficial para sugerir dicas e sugestões!" }, @@ -148,18 +182,48 @@ "hideButtonsDescription": { "message": "Isto esconde os botões que aparecem no player do Youtube para submeter patrocínios. Entendemos que possa ser\n incómodo a algumas pessoas. Em vez de usar esses botões pode usar os do popup. Para esconder a mensagem que aparece, \n ususe o botão na mesma que diz \"Don't show this again\". Pode sempre reactivar estas definições novamente." }, + "showSkipButton": { + "message": "Mantenha o Botão Saltar para Destaque no Player" + }, "showInfoButton": { "message": "Mostrar botão de Informações no player do Youtube" }, "hideInfoButton": { "message": "Esconder botão de Informações no player do Youtube" }, + "autoHideInfoButton": { + "message": "Ocultar automaticamente o Botão de Informação" + }, "hideDeleteButton": { "message": "Esconder botão de Apagar no player do Youtube" }, "showDeleteButton": { "message": "Mostrar botão de Apagar no player do Youtube" }, + "enableViewTracking": { + "message": "Ativar Rastreamento de Contagem de Saltos" + }, + "whatViewTracking": { + "message": "Esse recurso rastreia quais segmentos você pulou para permitir que os usuários saibam o quanto seu envio ajudou outras pessoas e é usado como métrica junto com votos positivos para garantir que o spam não entre no banco de dados. A extensão envia uma mensagem ao servidor cada vez que você pular um segmento. Espero que a maioria das pessoas não altere essa configuração para que os números de visualização sejam precisos. :)" + }, + "enableViewTrackingInPrivate": { + "message": "Ativar o Rastreamento de Contagem de Saltos nas Guias Privadas/Anônimas" + }, + "enableTrackDownvotes": { + "message": "Guardar segmentos de votos negativos" + }, + "whatTrackDownvotes": { + "message": "Quaisquer segmentos que você votar negativo permanecerão ocultos mesmo após a atualização" + }, + "trackDownvotesWarning": { + "message": "Aviso: Ao desabilitar isso excluirá todos os votos negativos armazenados anteriormente" + }, + "enableQueryByHashPrefix": { + "message": "Consulta por Prefixo de Hash" + }, + "whatQueryByHashPrefix": { + "message": "Em vez de solicitar segmentos do servidor usando o ID do Vídeo, são enviados os primeiros 4 caracteres do hash do ID do Vídeo. Este servidor enviará de volta dados para todos os vídeos com hashes semelhantes." + }, "showNotice": { "message": "Mostrar notificação outra vez" }, diff --git a/public/content.css b/public/content.css index 64e0c56e..3864947f 100644 --- a/public/content.css +++ b/public/content.css @@ -391,6 +391,10 @@ div:hover > .sponsorBlockChapterBar { filter: brightness(80%); } +.segmentSummary { + outline: none !important; +} + .submitButton { background-color:#ec1c1c; -moz-border-radius:28px; @@ -666,6 +670,7 @@ input::-webkit-inner-spin-button { line-height: 1.5em; color: white; font-size: 12px; + z-index: 1000; } .sponsorBlockTooltip a { @@ -725,4 +730,10 @@ input::-webkit-inner-spin-button { .sponsorBlockCategoryPill:hover .categoryPillClose { display: inherit; +} + +/* tweak for mobile duration */ +#sponsorBlockDurationAfterSkips.ytm-time-display { + padding-left: 4px; + margin: 0px; } \ No newline at end of file diff --git a/public/popup.css b/public/popup.css index a02430ca..b51d3ed5 100644 --- a/public/popup.css +++ b/public/popup.css @@ -7,14 +7,26 @@ } /* - * IDs on container element (when inserted in page), element, - * element and main container + * Container when popup displayed in-page */ #sponsorBlockPopupContainer { + position: relative; margin-bottom: 16px; } +/* + * Disable fixed popup width when displayed in-page + */ + +#sponsorBlockPopupContainer #sponsorBlockPopupBody { + width: auto; +} + +/* + * Main containers + */ + #sponsorBlockPopupHTML { color-scheme: dark; } @@ -41,6 +53,25 @@ transition: none !important; } +/* + * Close popup button when displayed in-page + */ + +.sbCloseButton { + background: transparent; + border: 0; + padding: 8px; + cursor: pointer; + position: absolute; + top: 5px; + right: 5px; + opacity: 0.5; +} + +.sbCloseButton:hover { + opacity: 1; +} + /* * Header logo */ @@ -82,6 +113,10 @@ *
wrapper around each segment */ +.votingButtons { + font-family: Arial, Helvetica, sans-serif; +} + .votingButtons[open] { padding-bottom: 5px; } diff --git a/src/components/SubmissionNoticeComponent.tsx b/src/components/SubmissionNoticeComponent.tsx index 08b60dd5..13955157 100644 --- a/src/components/SubmissionNoticeComponent.tsx +++ b/src/components/SubmissionNoticeComponent.tsx @@ -102,7 +102,7 @@ class SubmissionNoticeComponent extends React.Component window.open("https://wiki.sponsor.ajay.app/index.php/Guidelines")}> + onClick={() => window.open("https://wiki.sponsor.ajay.app/w/Guidelines")}> {chrome.i18n.getMessage(Config.config.submissionCountSinceCategories > 3 ? "guidelines" : "readTheGuidelines")} diff --git a/src/content.ts b/src/content.ts index e7047120..807e462b 100644 --- a/src/content.ts +++ b/src/content.ts @@ -35,12 +35,19 @@ let sponsorVideoID: VideoID = null; const skipNotices: SkipNotice[] = []; let activeSkipKeybindElement: ToggleSkippable = null; -// JSON video info +// JSON video info let videoInfo: VideoInfo = null; // The channel this video is about let channelIDInfo: ChannelIDInfo; // Locked Categories in this tab, like: ["sponsor","intro","outro"] let lockedCategories: Category[] = []; +// Used to calculate a more precise "virtual" video time +let lastKnownVideoTime: { videoTime: number, preciseTime: number } = { + videoTime: null, + preciseTime: null +}; +// It resumes with a slightly later time on chromium +let lastTimeFromWaitingEvent = null; // Skips are scheduled to ensure precision. // Skips are rescheduled every seeking event. @@ -301,7 +308,7 @@ async function videoIDChange(id) { // If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up if (Config.config.checkForUnlistedVideos) { - const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled." + const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled." + " Due to a change in how segment fetching works, this setting is not needed anymore as it cannot leak your video ID to the server." + " It instead sends just the first 4 characters of a longer hash of the videoID to the server, and filters through a subset of the database." + " More info about this implementation can be found here: https://github.com/ajayyy/SponsorBlockServer/issues/25" @@ -327,13 +334,13 @@ async function videoIDChange(id) { let controlsContainer = null; utils.wait(() => { - controlsContainer = document.getElementById("player-control-container") + controlsContainer = document.getElementById("player-control-container") return controlsContainer !== null }).then(() => { - observer.observe(document.getElementById("player-control-container"), { - attributes: true, - childList: true, - subtree: true + observer.observe(document.getElementById("player-control-container"), { + attributes: true, + childList: true, + subtree: true }); }).catch(); } else { @@ -455,7 +462,17 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: } if (!video) return; - if (currentTime === undefined || currentTime === null) currentTime = video.currentTime; + if (currentTime === undefined || currentTime === null) { + const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime ? + (performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null); + if ((lastTimeFromWaitingEvent || !utils.isFirefox()) + && !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6){ + currentTime = virtualTime; + } else { + currentTime = video.currentTime; + } + } + lastTimeFromWaitingEvent = null; previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime); @@ -509,9 +526,9 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) { if (forceVideoTime >= skipTime[0] && forceVideoTime < skipTime[1]) { skipToTime({ - v: video, - skipTime, - skippingSegments, + v: video, + skipTime, + skippingSegments, openNotice: skipInfo.openNotice }); @@ -531,24 +548,30 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: startSponsorSchedule(forcedIncludeIntersectingSegments, forcedSkipTime, forcedIncludeNonIntersectingSegments); }; - if (timeUntilSponsor <= 0) { - skippingFunction(); + if (timeUntilSponsor < 0.003) { + skippingFunction(currentTime); } else { const delayTime = timeUntilSponsor * 1000 * (1 / video.playbackRate); - if (delayTime < 300 && utils.isFirefox() && !isSafari()) { + if (delayTime < 300) { // For Firefox, use interval instead of timeout near the end to combat imprecise video time const startIntervalTime = performance.now(); - const startVideoTime = video.currentTime; + const startVideoTime = Math.max(currentTime, video.currentTime); currentSkipInterval = setInterval(() => { const intervalDuration = performance.now() - startIntervalTime; if (intervalDuration >= delayTime || video.currentTime >= skipTime[0]) { clearInterval(currentSkipInterval); - skippingFunction(Math.max(video.currentTime, startVideoTime + intervalDuration / 1000)); + if (!utils.isFirefox() && !video.muted) { + // Workaround for more accurate skipping on Chromium + video.muted = true; + video.muted = false; + } + + skippingFunction(Math.max(video.currentTime, startVideoTime + video.playbackRate * intervalDuration / 1000)); } - }, 5); + }, 1); } else { // Schedule for right before to be more precise than normal timeout - currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 30)); + currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 100)); } } } @@ -563,8 +586,8 @@ function inMuteSegment(currentTime: number): boolean { */ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean { const currentVideoID = getYouTubeVideoID(document); - if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime - && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) + if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime + && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) && !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) { // Something has really gone wrong console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be."); @@ -582,13 +605,13 @@ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boole function setupVideoMutationListener() { const videoContainer = document.querySelector(".html5-video-container"); if (!videoContainer || videoMutationObserver !== null || onInvidious) return; - + videoMutationObserver = new MutationObserver(refreshVideoAttachments); - videoMutationObserver.observe(videoContainer, { - attributes: true, - childList: true, - subtree: true + videoMutationObserver.observe(videoContainer, { + attributes: true, + childList: true, + subtree: true }); } @@ -630,32 +653,36 @@ function setupVideoListeners() { if (!firstEvent && video.currentTime === 0) return; firstEvent = false; + updateVirtualTime(); + if (switchingVideos) { switchingVideos = false; // If already segments loaded before video, retry to skip starting segments if (sponsorTimes) startSkipScheduleCheckingForStartSponsors(); } - + // Check if an ad is playing updateAdFlag(); - + // Make sure it doesn't get double called with the playing event if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); lastCheckVideoTime = video.currentTime; - + startSponsorSchedule(); } - + }); video.addEventListener('playing', () => { + updateVirtualTime(); + // Make sure it doesn't get double called with the play event if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); lastCheckVideoTime = video.currentTime; - + startSponsorSchedule(); } }); @@ -664,7 +691,10 @@ function setupVideoListeners() { // Reset lastCheckVideoTime lastCheckTime = Date.now(); lastCheckVideoTime = video.currentTime; - + + updateVirtualTime(); + lastTimeFromWaitingEvent = null; + startSponsorSchedule(); } else { previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, video.currentTime); @@ -673,26 +703,41 @@ function setupVideoListeners() { video.addEventListener('ratechange', () => startSponsorSchedule()); // Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740) video.addEventListener('videoSpeed_ratechange', () => startSponsorSchedule()); - video.addEventListener('pause', () => { + const paused = () => { // Reset lastCheckVideoTime lastCheckVideoTime = -1; lastCheckTime = 0; - + + lastKnownVideoTime = { + videoTime: null, + preciseTime: null + } + lastTimeFromWaitingEvent = video.currentTime; + cancelSponsorSchedule(); - }); - + }; + video.addEventListener('pause', paused); + video.addEventListener('waiting', paused); + startSponsorSchedule(); } } +function updateVirtualTime() { + lastKnownVideoTime = { + videoTime: video.currentTime, + preciseTime: performance.now() + }; +} + function setupSkipButtonControlBar() { if (!skipButtonControlBar) { skipButtonControlBar = new SkipButtonControlBar({ skip: (segment) => skipToTime({ - v: video, - skipTime: segment.segment, - skippingSegments: [segment], - openNotice: true, + v: video, + skipTime: segment.segment, + skippingSegments: [segment], + openNotice: true, forceAutoSkip: true }), onMobileYouTube @@ -731,7 +776,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { const hashPrefix = (await utils.getHash(id, 1)).slice(0, 4) as VideoID & HashedValue; const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { categories, - actionTypes: getEnabledActionTypes(), + actionTypes: getEnabledActionTypes(), userAgent: `${chrome.runtime.id}`, ...extraRequestData }); @@ -744,7 +789,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { ...segment, source: SponsorSourceType.Server })); - if (!recievedSegments || !recievedSegments.length) { + if (!recievedSegments || !recievedSegments.length) { // return if no video found retryFetch(); return; @@ -813,7 +858,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { } else if (response?.status === 404) { retryFetch(); } - + lookupVipInformation(id); } @@ -903,8 +948,8 @@ function retryFetch(): void { } /** - * Only should be used when it is okay to skip a sponsor when in the middle of it - * + * Only should be used when it is okay to skip a sponsor when in the middle of it + * * Ex. When segments are first loaded */ function startSkipScheduleCheckingForStartSponsors() { @@ -915,7 +960,7 @@ function startSkipScheduleCheckingForStartSponsors() { let found = false; let startingSegment: SponsorTime = null; for (const time of sponsorTimes) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; startingSegment = time; @@ -925,7 +970,7 @@ function startSkipScheduleCheckingForStartSponsors() { } if (!found) { for (const time of sponsorTimesSubmitting) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; startingSegment = time; @@ -944,8 +989,8 @@ function startSkipScheduleCheckingForStartSponsors() { if (skipOption !== CategorySkipOption.ShowOverlay) { skipToTime({ v: video, - skipTime: time.segment, - skippingSegments: [time], + skipTime: time.segment, + skippingSegments: [time], openNotice: true, unskipTime: video.currentTime }); @@ -968,7 +1013,7 @@ function startSkipScheduleCheckingForStartSponsors() { /** * Get the video info for the current tab from YouTube - * + * * TODO: Replace */ async function getVideoInfo(): Promise { @@ -990,10 +1035,10 @@ function getYouTubeVideoID(document: Document): string | boolean { const url = document.URL; // clips should never skip, going from clip to full video has no indications. if (url.includes("youtube.com/clip/")) return false; + // skip to document and don't hide if on /embed/ + if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false); // skip to URL if matches youtube watch or invidious or matches youtube pattern if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url); - // skip to document and don't hide if on /embed/ - if (url.includes("/embed/")) return getYouTubeVideoIDFromDocument(document, false); // skip to document if matches pattern if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document); // not sure, try URL then document @@ -1016,9 +1061,9 @@ function getYouTubeVideoIDFromURL(url: string): string | boolean { //Attempt to parse url let urlObject: URL = null; - try { + try { urlObject = new URL(url); - } catch (e) { + } catch (e) { console.error("[SB] Unable to parse URL: " + url); return false; } @@ -1117,11 +1162,12 @@ function updatePreviewBar(): void { async function whitelistCheck() { const whitelistedChannels = Config.config.whitelistedChannels; - const getChannelID = () => videoInfo?.videoDetails?.channelId - ?? document.querySelector(".ytd-channel-name a")?.getAttribute("href")?.replace(/\/.+\//, "") // YouTube - ?? document.querySelector(".ytp-title-channel-logo")?.getAttribute("href")?.replace(/https:\/.+\//, "") // YouTube Embed - ?? document.querySelector("a > .channel-profile")?.parentElement?.getAttribute("href")?.replace(/\/.+\//, "") // Invidious - ?? document.querySelector("a.slim-owner-icon-and-title")?.getAttribute("href")?.replace(/\/.+\//, ""); // Mobile YouTube + const getChannelID = () => + (document.querySelector("a.ytd-video-owner-renderer") // YouTube + ?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed + ?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious + ?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube + ?.getAttribute("href")?.match(/\/channel\/(UC[a-zA-Z0-9_-]{22})/)[1]; try { await utils.wait(() => !!getChannelID(), 6000, 20); @@ -1140,7 +1186,7 @@ async function whitelistCheck() { } //see if this is a whitelisted channel - if (whitelistedChannels != undefined && + if (whitelistedChannels != undefined && channelIDInfo.status === ChannelIDStatus.Found && whitelistedChannels.includes(channelIDInfo.id)) { channelWhitelisted = true; } @@ -1152,24 +1198,24 @@ async function whitelistCheck() { /** * Returns info about the next upcoming sponsor skip */ -function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): +function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} { - const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = + const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true); const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime)); const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex); - const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = + const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); const { scheduledTimes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false); const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime)); const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex); - if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || + if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || sponsorStartTimes[minSponsorTimeIndex] < unsubmittedSponsorStartTimes[minUnsubmittedSponsorTimeIndex]) { return { array: submittedArray, @@ -1189,20 +1235,20 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool /** * This returns index if the skip option is not AutoSkip - * + * * Finds the last endTime that occurs in a segment that the given * segment skips into that is part of an AutoSkip category. - * - * Used to find where a segment should truely skip to if there are intersecting submissions due to + * + * Used to find where a segment should truely skip to if there are intersecting submissions due to * them having different categories. - * - * @param sponsorTimes + * + * @param sponsorTimes * @param index Index of the given sponsor - * @param hideHiddenSponsors + * @param hideHiddenSponsors */ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideHiddenSponsors = true): number { // Only combine segments for AutoSkip - if (index == -1 || + if (index == -1 || !shouldAutoSkip(sponsorTimes[index]) || sponsorTimes[index].actionType !== ActionType.Skip) { return index; @@ -1215,7 +1261,7 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH const currentSegment = sponsorTimes[i].segment; const latestEndTime = sponsorTimes[latestEndTimeIndex].segment[1]; - if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime + if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) && shouldAutoSkip(sponsorTimes[i]) && sponsorTimes[i].actionType === ActionType.Skip) { @@ -1235,11 +1281,11 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH /** * Gets just the start times from a sponsor times array. * Optionally specify a minimum - * - * @param sponsorTimes + * + * @param sponsorTimes * @param minimum * @param hideHiddenSponsors - * @param includeIntersectingSegments If true, it will include segments that start before + * @param includeIntersectingSegments If true, it will include segments that start before * the current time, but end after */ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean, @@ -1274,7 +1320,7 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: scheduledTimes.push(possibleTimes[i].scheduledTime); includedTimes.push(possibleTimes[i]); - } + } } return { includedTimes, scheduledTimes }; @@ -1282,8 +1328,8 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: /** * Skip to exact time in a video and autoskips - * - * @param time + * + * @param time */ function previewTime(time: number, unpause = true) { video.currentTime = time; @@ -1308,7 +1354,7 @@ function sendTelemetryAndCount(skippingSegments: SponsorTime[], secondsSkipped: Config.config.skipCount = Config.config.skipCount + 1; counted = true; } - + if (fullSkip) utils.asyncRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + segment.UUID); } } @@ -1321,7 +1367,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u // There will only be one submission if it is manual skip const autoSkip: boolean = forceAutoSkip || shouldAutoSkip(skippingSegments[0]); - if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) + if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) && v.currentTime !== skipTime[1]) { switch(skippingSegments[0].actionType) { case ActionType.Poi: @@ -1362,8 +1408,8 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u }) } - if (!autoSkip - && skippingSegments.length === 1 + if (!autoSkip + && skippingSegments.length === 1 && skippingSegments[0].actionType === ActionType.Poi) { skipButtonControlBar.enable(skippingSegments[0]); if (onMobileYouTube || Config.config.skipKeybind == null) skipButtonControlBar.setShowKeybindHint(false); @@ -1396,7 +1442,7 @@ function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null) { //add a tiny bit of time to make sure it is not skipped again video.currentTime = unskipTime ?? segment.segment[0] + 0.001; } - + } function reskipSponsorTime(segment: SponsorTime) { @@ -1407,7 +1453,7 @@ function reskipSponsorTime(segment: SponsorTime) { const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0); const segmentDuration = segment.segment[1] - segment.segment[0]; const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; - + video.currentTime = segment.segment[1]; sendTelemetryAndCount([segment], skippedTime, fullSkip); startSponsorSchedule(true, segment.segment[1], false); @@ -1459,8 +1505,8 @@ function shouldAutoSkip(segment: SponsorTime): boolean { } function shouldSkip(segment: SponsorTime): boolean { - return (segment.actionType !== ActionType.Full - && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) + return (segment.actionType !== ActionType.Full + && utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay) || (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic")); } @@ -1478,10 +1524,10 @@ async function createButtons(): Promise { createButton("info", "openPopup", openInfoMenu, "PlayerInfoIconSponsorBlocker.svg"); const controlsContainer = getControls(); - if (Config.config.autoHideInfoButton && !onInvidious && controlsContainer + if (Config.config.autoHideInfoButton && !onInvidious && controlsContainer && playerButtons["info"]?.button && !controlsWithEventListeners.includes(controlsContainer)) { controlsWithEventListeners.push(controlsContainer); - + AnimationUtils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer); } } @@ -1576,7 +1622,7 @@ function startOrEndTimingNewSegment() { const existingSegment = getIncompleteSegment(); const existingTime = existingSegment.segment[0]; const currentTime = roundedTime; - + // Swap timestamps if the user put the segment end before the start existingSegment.segment = [Math.min(existingTime, currentTime), Math.max(existingTime, currentTime)]; } @@ -1680,17 +1726,19 @@ function openInfoMenu() { popup.innerHTML = htmlData; //close button - const closeButton = document.createElement("div"); - closeButton.innerText = chrome.i18n.getMessage("closePopup"); - closeButton.classList.add("smallLink"); - closeButton.setAttribute("align", "center"); + const closeButton = document.createElement("button"); + const closeButtonIcon = document.createElement("img"); + closeButtonIcon.src = chrome.extension.getURL("icons/close.png"); + closeButtonIcon.width = 15; + closeButtonIcon.height = 15; + closeButton.appendChild(closeButtonIcon); + closeButton.setAttribute("title", chrome.i18n.getMessage("closePopup")); + closeButton.classList.add("sbCloseButton"); closeButton.addEventListener("click", closeInfoMenu); - // Theme based color - closeButton.style.color = "var(--yt-spec-text-primary)"; //add the close button popup.prepend(closeButton); - + const parentNodes = document.querySelectorAll("#secondary"); let parentNode = null; for (let i = 0; i < parentNodes.length; i++) { @@ -1801,7 +1849,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo } else { skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText)) } - + skipNotice.resetVoteButtonInfo.bind(skipNotice)(); } } @@ -1827,7 +1875,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): // Count this as a skip Config.config.minutesSaved = Config.config.minutesSaved + factor * (sponsorTimes[sponsorIndex].segment[1] - sponsorTimes[sponsorIndex].segment[0]) / 60; - + Config.config.skipCount = Config.config.skipCount + factor; } @@ -1889,8 +1937,8 @@ function submitSponsorTimes() { submissionNotice.close(); submissionNotice = null; return; - } - + } + if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) { submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage); } @@ -1926,9 +1974,9 @@ async function sendSubmitMessage() { for (let i = 0; i < sponsorTimesSubmitting.length; i++) { const duration = sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0]; if (duration > 0 && duration < Config.config.minDuration) { - const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + + const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + getSegmentsMessage(sponsorTimesSubmitting); - + if(!confirm(confirmShort)) return; } } @@ -2033,10 +2081,10 @@ function hotkeyListener(e: KeyboardEvent): void { || document.activeElement?.id?.toLowerCase()?.includes("editable")) return; const key: Keybind = { - key: e.key, - code: e.code, - alt: e.altKey, - ctrl: e.ctrlKey, + key: e.key, + code: e.code, + alt: e.altKey, + ctrl: e.ctrlKey, shift: e.shiftKey }; @@ -2097,7 +2145,7 @@ function sendRequestToCustomServer(type, fullAddress, callback) { xmlhttp.onreadystatechange = function () { callback(xmlhttp, false); }; - + xmlhttp.onerror = function() { callback(xmlhttp, true); }; @@ -2120,14 +2168,15 @@ function updateAdFlag(): void { } function showTimeWithoutSkips(skippedDuration: number): void { - if (onMobileYouTube || onInvidious) return; + if (onInvidious) return; if (isNaN(skippedDuration) || skippedDuration < 0) { skippedDuration = 0; } // YouTube player time display - const display = document.querySelector(".ytp-time-display.notranslate"); + const displayClass = onMobileYouTube ? "ytm-time-display" : "ytp-time-display.notranslate" + const display = document.querySelector(`.${displayClass}`); if (!display) return; const durationID = "sponsorBlockDurationAfterSkips"; @@ -2137,19 +2186,19 @@ function showTimeWithoutSkips(skippedDuration: number): void { if (duration === null) { duration = document.createElement('span'); duration.id = durationID; - duration.classList.add("ytp-time-duration"); + duration.classList.add(displayClass); display.appendChild(duration); } - const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration) + const durationAfterSkips = GenericUtils.getFormattedTime(video?.duration - skippedDuration); duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")"; } function checkForPreloadedSegment() { if (loadedPreloadedSegment) return; - + loadedPreloadedSegment = true; const hashParams = getHashParams(); @@ -2177,4 +2226,4 @@ function checkForPreloadedSegment() { Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); } -} \ No newline at end of file +} diff --git a/src/popup.ts b/src/popup.ts index c0c13eca..e7394c79 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -190,7 +190,7 @@ async function runThePopup(messageListener?: MessageListener): Promise { PageElements.sponsorTimesViewsDisplayEndWord.innerText = chrome.i18n.getMessage("Segment"); } PageElements.sponsorTimesViewsDisplay.innerText = viewCount.toLocaleString(); - PageElements.sponsorTimesViewsContainer.style.display = "unset"; + PageElements.sponsorTimesViewsContainer.style.display = "block"; } showDonateWidget(viewCount); @@ -222,7 +222,7 @@ async function runThePopup(messageListener?: MessageListener): Promise { } PageElements.sponsorTimesSkipsDoneDisplay.innerText = Config.config.skipCount.toLocaleString(); - PageElements.sponsorTimesSkipsDoneContainer.style.display = "unset"; + PageElements.sponsorTimesSkipsDoneContainer.style.display = "block"; } //get the amount of time this user has saved. diff --git a/src/utils.ts b/src/utils.ts index 8e59b95d..930ca2b1 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -354,13 +354,15 @@ export default class Utils { findReferenceNode(): HTMLElement { const selectors = [ + "#player-container-id", // Mobile YouTube "#movie_player", "#c4-player", // Channel Trailer "#player-container", // Preview on hover "#main-panel.ytmusic-player-page", // YouTube music "#player-container .video-js", // Invidious ".main-video-section > .video-container" // Cloudtube - ] + ]; + let referenceNode = findValidElementFromSelector(selectors) if (referenceNode == null) { //for embeds