diff options
author | Ajay <[email protected]> | 2023-03-10 03:49:01 -0500 |
---|---|---|
committer | Ajay <[email protected]> | 2023-03-10 03:49:01 -0500 |
commit | 758f0b7526f5e3774fa723433c7cbc47d21badd5 (patch) | |
tree | 852a26f8f71d61aade3af35f07faabeb594f447d /src | |
parent | 3ace3b96506fbcb4648e36129bfc6e735d73d811 (diff) | |
download | SponsorBlock-758f0b7526f5e3774fa723433c7cbc47d21badd5.tar.gz SponsorBlock-758f0b7526f5e3774fa723433c7cbc47d21badd5.zip |
Show Full-Video Labels on thumbnails
Co-authored-by: mini-bomba <[email protected]>
Diffstat (limited to 'src')
-rw-r--r-- | src/components/CategoryPillComponent.tsx | 25 | ||||
-rw-r--r-- | src/content.ts | 38 | ||||
-rw-r--r-- | src/js-components/previewBar.ts | 3 | ||||
-rw-r--r-- | src/utils/thumbnails.ts | 122 | ||||
-rw-r--r-- | src/utils/videoLabels.ts | 65 |
5 files changed, 231 insertions, 22 deletions
diff --git a/src/components/CategoryPillComponent.tsx b/src/components/CategoryPillComponent.tsx index a008e396..123f3e93 100644 --- a/src/components/CategoryPillComponent.tsx +++ b/src/components/CategoryPillComponent.tsx @@ -115,28 +115,15 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP } private getColor(): string { - const configObject = Config.config.barTypes["preview-" + this.state.segment?.category] - || Config.config.barTypes[this.state.segment?.category]; - return configObject?.color; + // Handled by setCategoryColorCSSVariables() of content.ts + const category = this.state.segment?.category; + return `var(--sb-category-preview-${category}, var(--sb-category-${category}))`; } private getTextColor(): string { - const color = this.getColor(); - if (!color) return null; - - const existingCalculatedColor = Config.config.categoryPillColors[this.state.segment?.category]; - if (existingCalculatedColor && existingCalculatedColor.lastColor === color) { - return existingCalculatedColor.textColor; - } else { - const luminance = GenericUtils.getLuminance(color); - const textColor = luminance > 128 ? "black" : "white"; - Config.config.categoryPillColors[this.state.segment?.category] = { - lastColor: color, - textColor - }; - - return textColor; - } + // Handled by setCategoryColorCSSVariables() of content.ts + const category = this.state.segment?.category; + return `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`; } private openTooltip(): void { diff --git a/src/content.ts b/src/content.ts index 6e719c31..b73be606 100644 --- a/src/content.ts +++ b/src/content.ts @@ -43,11 +43,16 @@ import { StorageChangesObject } from "@ajayyy/maze-utils/lib/config"; import { findValidElement } from "@ajayyy/maze-utils/lib/dom" import { getHash, HashedValue } from "@ajayyy/maze-utils/lib/hash"; import { generateUserID } from "@ajayyy/maze-utils/lib/setup"; +import { setThumbnailListener, updateAll } from "@ajayyy/maze-utils/lib/thumbnailManagement"; +import { labelThumbnails, setupThumbnailPageLoadListener } from "./utils/thumbnails"; const utils = new Utils(); -// Hack to get the CSS loaded on permission-based sites (Invidious) -utils.wait(() => Config.isReady(), 5000, 10).then(addCSS); +utils.wait(() => Config.isReady(), 5000, 10).then(() => { + // Hack to get the CSS loaded on permission-based sites (Invidious) + addCSS(); + setCategoryColorCSSVariables(); +}); const skipBuffer = 0.003; @@ -108,6 +113,8 @@ setupVideoModule({ }, resetValues }, () => Config); +setThumbnailListener(labelThumbnails); +setupThumbnailPageLoadListener(); //the video id of the last preview bar update let lastPreviewBarUpdate: VideoID; @@ -332,6 +339,12 @@ function contentConfigUpdateListener(changes: StorageChangesObject) { case "categorySelections": sponsorsLookup(); break; + case "barTypes": + setCategoryColorCSSVariables(); + break; + case "fullVideoSegments": + updateAll(); + break; } } } @@ -2466,4 +2479,25 @@ function checkForPreloadedSegment() { Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); } +} + +// Generate and inject a stylesheet that creates CSS variables with configured category colors +function setCategoryColorCSSVariables() { + let styleContainer = document.getElementById("sbCategoryColorStyle"); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = "sbCategoryColorStyle"; + document.head.appendChild(styleContainer) + } + + let css = ":root {" + for (const [category, config] of Object.entries(Config.config.barTypes)) { + css += `--sb-category-${category}: ${config.color};`; + + const luminance = GenericUtils.getLuminance(config.color); + css += `--sb-category-text-${category}: ${luminance > 128 ? "black" : "white"};`; + } + css += "}"; + + styleContainer.innerText = css; }
\ No newline at end of file diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index 30d54d3a..bec83333 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -331,7 +331,8 @@ class PreviewBar { const fullCategoryName = (unsubmitted ? 'preview-' : '') + category; bar.setAttribute('sponsorblock-category', fullCategoryName); - bar.style.backgroundColor = Config.config.barTypes[fullCategoryName]?.color; + // Handled by setCategoryColorCSSVariables() of content.ts + bar.style.backgroundColor = `var(--sb-category-${fullCategoryName})`; if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[fullCategoryName]?.opacity; bar.style.position = "absolute"; diff --git a/src/utils/thumbnails.ts b/src/utils/thumbnails.ts new file mode 100644 index 00000000..a0009e7c --- /dev/null +++ b/src/utils/thumbnails.ts @@ -0,0 +1,122 @@ +import { waitFor } from "@ajayyy/maze-utils"; +import { newThumbnails } from "@ajayyy/maze-utils/lib/thumbnailManagement"; +import { isOnInvidious, parseYouTubeVideoIDFromURL } from "@ajayyy/maze-utils/lib/video"; +import Config from "../config"; +import { getVideoLabel } from "./videoLabels"; + +export async function labelThumbnails(thumbnails: HTMLImageElement[]): Promise<void> { + await Promise.all(thumbnails.map((t) => labelThumbnail(t))); +} + +export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> { + if (!Config.config?.fullVideoSegments) { + hideThumbnailLabel(thumbnail); + return null; + } + + const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement + if (!link || link.nodeName !== "A" || !link.href) return null; // no link found + const videoID = parseYouTubeVideoIDFromURL(link.href)?.videoID; + if (!videoID) { + hideThumbnailLabel(thumbnail); + return null; + } + + const category = await getVideoLabel(videoID); + if (!category) { + hideThumbnailLabel(thumbnail); + return null; + } + + const { overlay, text } = createOrGetThumbnail(thumbnail); + + overlay.style.setProperty('--category-color', `var(--sb-category-preview-${category}, var(--sb-category-${category}))`); + overlay.style.setProperty('--category-text-color', `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`); + text.innerText = chrome.i18n.getMessage(`category_${category}`); + overlay.classList.add("sponsorThumbnailLabelVisible"); + + return overlay; +} + +function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null { + return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null; +} + +function hideThumbnailLabel(thumbnail: HTMLImageElement): void { + const oldLabel = getOldThumbnailLabel(thumbnail); + if (oldLabel) { + oldLabel.classList.remove("sponsorThumbnailLabelVisible"); + } +} + +function createOrGetThumbnail(thumbnail: HTMLImageElement): { overlay: HTMLElement; text: HTMLElement } { + const oldElement = getOldThumbnailLabel(thumbnail); + if (oldElement) { + return { + overlay: oldElement as HTMLElement, + text: oldElement.querySelector("span") as HTMLElement + }; + } + + const overlay = document.createElement("div") as HTMLElement; + overlay.classList.add("sponsorThumbnailLabel"); + + const icon = createSBIconElement(); + const text = document.createElement("span"); + overlay.appendChild(icon); + overlay.appendChild(text); + thumbnail.appendChild(overlay); + + return { + overlay, + text + }; +} + +function createSBIconElement(): SVGSVGElement { + const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + svg.setAttribute("viewBox", "0 0 565.15 568"); + const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); + use.setAttribute("href", "#SponsorBlockIcon"); + svg.appendChild(use); + return svg; +} + + +// Inserts the icon svg definition, so it can be used elsewhere +function insertSBIconDefinition() { + const container = document.createElement("span"); + + // svg from /public/icons/PlayerStartIconSponsorBlocker.svg, with useless stuff removed + container.innerHTML = ` +<svg viewBox="0 0 565.15 568" style="display: none"> + <defs> + <g id="SponsorBlockIcon"> + <path d="M282.58,568a65,65,0,0,1-34.14-9.66C95.41,463.94,2.54,300.46,0,121A64.91,64.91,0,0,1,34,62.91a522.56,522.56,0,0,1,497.16,0,64.91,64.91,0,0,1,34,58.12c-2.53,179.43-95.4,342.91-248.42,437.3A65,65,0,0,1,282.58,568Zm0-548.31A502.24,502.24,0,0,0,43.4,80.22a45.27,45.27,0,0,0-23.7,40.53c2.44,172.67,91.81,330,239.07,420.83a46.19,46.19,0,0,0,47.61,0C453.64,450.73,543,293.42,545.45,120.75a45.26,45.26,0,0,0-23.7-40.54A502.26,502.26,0,0,0,282.58,19.69Z"/> + <path d="M 284.70508 42.693359 A 479.9 479.9 0 0 0 54.369141 100.41992 A 22.53 22.53 0 0 0 42.669922 120.41992 C 45.069922 290.25992 135.67008 438.63977 270.83008 522.00977 A 22.48 22.48 0 0 0 294.32031 522.00977 C 429.48031 438.63977 520.08047 290.25992 522.48047 120.41992 A 22.53 22.53 0 0 0 510.7793 100.41992 A 479.9 479.9 0 0 0 284.70508 42.693359 z M 220.41016 145.74023 L 411.2793 255.93945 L 220.41016 366.14062 L 220.41016 145.74023 z "/> + </g> + </defs> +</svg>`; + document.body.appendChild(container.children[0]); +} + +export function setupThumbnailPageLoadListener(): void { + const onLoad = () => { + insertSBIconDefinition(); + + // Label thumbnails on load if on Invidious (wait for variable initialization before checking) + waitFor(() => isOnInvidious() !== undefined).then(() => { + if (isOnInvidious()) newThumbnails(); + }); + }; + + if (document.readyState === "complete") { + onLoad(); + } else { + window.addEventListener("load", onLoad); + } + + waitFor(() => Config.isReady(), 5000, 10).then(() => { + newThumbnails(); + }); +}
\ No newline at end of file diff --git a/src/utils/videoLabels.ts b/src/utils/videoLabels.ts new file mode 100644 index 00000000..ca12c6e4 --- /dev/null +++ b/src/utils/videoLabels.ts @@ -0,0 +1,65 @@ +import { Category, VideoID } from "../types"; +import { getHash } from "@ajayyy/maze-utils/lib/hash"; +import Utils from "../utils"; +import { logWarn } from "./logger"; + +const utils = new Utils(); + +export interface LabelCacheEntry { + timestamp: number; + videos: Record<VideoID, Category>; +} + +const labelCache: Record<string, LabelCacheEntry> = {}; +const cacheLimit = 1000; + +async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry | null> { + // Check cache + const cachedEntry = labelCache[hashPrefix]; + if (cachedEntry) { + return cachedEntry; + } + + const response = await utils.asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}`); + if (response.status !== 200) { + // No video labels or server down + labelCache[hashPrefix] = { + timestamp: Date.now(), + videos: {}, + }; + return null; + } + + try { + const data = JSON.parse(response.responseText); + + const newEntry: LabelCacheEntry = { + timestamp: Date.now(), + videos: Object.fromEntries(data.map(video => [video.videoID, video.segments[0].category])), + }; + labelCache[hashPrefix] = newEntry; + + if (Object.keys(labelCache).length > cacheLimit) { + // Remove oldest entry + const oldestEntry = Object.entries(labelCache).reduce((a, b) => a[1].timestamp < b[1].timestamp ? a : b); + delete labelCache[oldestEntry[0]]; + } + + return newEntry; + } catch (e) { + logWarn(`Error parsing video labels: ${e}`); + + return null; + } +} + +export async function getVideoLabel(videoID: VideoID): Promise<Category | null> { + const prefix = (await getHash(videoID, 1)).slice(0, 3); + const result = await getLabelHashBlock(prefix); + + if (result) { + return result.videos[videoID] ?? null; + } + + return null; +}
\ No newline at end of file |