diff --git a/src/constants.js b/src/constants.js index 7c61599a0ea7b..c641a9489d24f 100644 --- a/src/constants.js +++ b/src/constants.js @@ -174,6 +174,7 @@ const KeyboardShortcuts = { VOLUME_DOWN: 'arrowdown', STATS: 'd', TAKE_SCREENSHOT: 'u', + UNSKIP_SPONSOR_BLOCK: 'enter', }, PLAYBACK: { PLAY: 'k', diff --git a/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue b/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue index 2f666993ce242..becf929541cf0 100644 --- a/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue +++ b/src/renderer/components/FtKeyboardShortcutPrompt/FtKeyboardShortcutPrompt.vue @@ -144,6 +144,7 @@ const localizedShortcutNameToShortcutsMappings = computed(() => { [t('KeyboardShortcutPrompt.Volume Down'), ['VOLUME_DOWN']], [t('KeyboardShortcutPrompt.Take Screenshot'), ['TAKE_SCREENSHOT']], [t('KeyboardShortcutPrompt.Stats'), ['STATS']], + [t('KeyboardShortcutPrompt.Unskip Sponsor Block'), ['UNSKIP_SPONSOR_BLOCK']], [t('KeyboardShortcutPrompt.Play'), ['PLAY']], [t('KeyboardShortcutPrompt.Large Rewind'), ['LARGE_REWIND']], diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css index e1f3d79055cfd..5575f2132c0ec 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.css @@ -145,6 +145,12 @@ border-radius: 20px; background-color: rgb(0 0 0 / 80%); color: #fff; + text-align: center; +} + +.unskipInstruction { + font-size: 0.8em; + opacity: 0.8; } :deep(.markerContainer) { diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js index 02ee2146bf928..3db6f6b494480 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.js @@ -479,9 +479,10 @@ export default defineComponent({ /** * Yes a map would be much more suitable for this (unlike objects they retain the order that items were inserted), * but Vue 2 doesn't support reactivity on Maps, so we have to use an array instead - * @type {import('vue').Ref<{uuid: string, translatedCategory: string, timeoutId: number}[]>} + * @type {import('vue').Ref<{uuid: string, translatedCategory: string, timeoutId: number, startTime: number, endTime: number, unskipped: boolean}[]>} */ const skippedSponsorBlockSegments = ref([]) + const ignoredSponsorBlockSegments = reactive(new Set()) async function setupSponsorBlock() { let segments, averageDuration @@ -511,6 +512,14 @@ export default defineComponent({ * @param {number} currentTime */ function skipSponsorBlockSegments(currentTime) { + skippedSponsorBlockSegments.value = skippedSponsorBlockSegments.value.filter(segment => { + if (segment.unskipped && (currentTime >= segment.endTime || currentTime < segment.startTime)) { + ignoredSponsorBlockSegments.delete(segment.uuid) + return false + } + return true + }) + const { autoSkip } = sponsorSkips.value if (autoSkip.size === 0) { @@ -523,7 +532,7 @@ export default defineComponent({ const skippedSegments = [] sponsorBlockSegments.forEach(segment => { - if (autoSkip.has(segment.category) && currentTime < segment.endTime && + if (!ignoredSponsorBlockSegments.has(segment.uuid) && autoSkip.has(segment.category) && currentTime < segment.endTime && (segment.startTime <= currentTime || // if we already have a segment to skip, check if there are any that are less than 150ms later, // so that we can skip them all in one go (especially useful on slow connections) @@ -550,26 +559,49 @@ export default defineComponent({ video_.currentTime = newTime if (sponsorBlockShowSkippedToast.value) { - skippedSegments.forEach(({ uuid, category }) => { - // if the element already exists, just update the timeout, instead of creating a duplicate - // can happen at the end of the video sometimes + skippedSegments.forEach(({ uuid, category, startTime, endTime }) => { + const removeSegment = () => { + const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) + if (index !== -1) { + const segment = skippedSponsorBlockSegments.value[index] + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + skippedSponsorBlockSegments.value.splice(index, 1) + } + ignoredSponsorBlockSegments.delete(uuid) + } + const existingSkip = skippedSponsorBlockSegments.value.find(skipped => skipped.uuid === uuid) if (existingSkip) { clearTimeout(existingSkip.timeoutId) - - existingSkip.timeoutId = setTimeout(() => { - const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) - skippedSponsorBlockSegments.value.splice(index, 1) - }, 2000) + clearInterval(existingSkip.intervalId) + + existingSkip.timeLeft = 4 + existingSkip.segmentTimeLeft = 0 + existingSkip.intervalId = setInterval(() => { + existingSkip.timeLeft-- + }, 1000) + existingSkip.timeoutId = setTimeout(removeSegment, 4000) } else { - skippedSponsorBlockSegments.value.push({ + const segmentObj = { uuid, translatedCategory: translateSponsorBlockCategory(category), - timeoutId: setTimeout(() => { - const index = skippedSponsorBlockSegments.value.findIndex(skipped => skipped.uuid === uuid) - skippedSponsorBlockSegments.value.splice(index, 1) - }, 2000) - }) + startTime, + endTime, + unskipped: false, + timeLeft: 4, + segmentTimeLeft: 0, + intervalId: null, + timeoutId: null + } + + skippedSponsorBlockSegments.value.push(segmentObj) + const reactiveSegmentObj = skippedSponsorBlockSegments.value.find(skipped => skipped.uuid === uuid) + + reactiveSegmentObj.intervalId = setInterval(() => { + reactiveSegmentObj.timeLeft-- + }, 1000) + reactiveSegmentObj.timeoutId = setTimeout(removeSegment, 4000) } }) } @@ -1227,6 +1259,14 @@ export default defineComponent({ if (useSponsorBlock.value && sponsorBlockSegments.length > 0 && canSeek()) { skipSponsorBlockSegments(currentTime) } + + if (useSponsorBlock.value) { + for (const segment of skippedSponsorBlockSegments.value) { + if (segment.unskipped) { + segment.segmentTimeLeft = Math.max(0, segment.endTime - currentTime) + } + } + } } } @@ -2292,6 +2332,40 @@ export default defineComponent({ } switch (event.key.toLowerCase()) { + case KeyboardShortcuts.VIDEO_PLAYER.GENERAL.UNSKIP_SPONSOR_BLOCK: { + if (skippedSponsorBlockSegments.value.length > 0) { + event.preventDefault() + const segment = skippedSponsorBlockSegments.value[skippedSponsorBlockSegments.value.length - 1] + if (segment.unskipped) { + video_.currentTime = segment.endTime + segment.unskipped = false + segment.timeLeft = 4 + segment.segmentTimeLeft = 0 + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + segment.intervalId = setInterval(() => { + segment.timeLeft-- + }, 1000) + segment.timeoutId = setTimeout(() => { + const index = skippedSponsorBlockSegments.value.indexOf(segment) + if (index !== -1) { + skippedSponsorBlockSegments.value.splice(index, 1) + } + ignoredSponsorBlockSegments.delete(segment.uuid) + }, 4000) + ignoredSponsorBlockSegments.delete(segment.uuid) + } else { + video_.currentTime = segment.startTime + segment.unskipped = true + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + segment.timeLeft = null + segment.segmentTimeLeft = Math.max(0, segment.endTime - video_.currentTime) + ignoredSponsorBlockSegments.add(segment.uuid) + } + } + break + } case ' ': case 'spacebar': // older browsers might return spacebar instead of a space character case KeyboardShortcuts.VIDEO_PLAYER.PLAYBACK.PLAY: @@ -3171,7 +3245,10 @@ export default defineComponent({ navigator.mediaSession.playbackState = 'none' } - skippedSponsorBlockSegments.value.forEach(segment => clearTimeout(segment.timeoutId)) + skippedSponsorBlockSegments.value.forEach(segment => { + clearTimeout(segment.timeoutId) + clearInterval(segment.intervalId) + }) window.removeEventListener('online', onlineHandler) window.removeEventListener('offline', offlineHandler) diff --git a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue index 8f49114874dfb..a45fafa327682 100644 --- a/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue +++ b/src/renderer/components/ft-shaka-video-player/ft-shaka-video-player.vue @@ -116,11 +116,15 @@ class="skippedSegmentsWrapper" >
{{ $t('Video.Player.Skipped segment', { segmentCategory: translatedCategory }) }}
+
+
+ {{ unskipped ? $t('Video.Player.Press Enter to reskip', { timeLeft: Math.ceil(segmentTimeLeft) }) : $t('Video.Player.Press Enter to unskip', { timeLeft }) }}
+