diff --git a/src/features/expandos/css/index.css b/src/features/expandos/css/index.css index 32403e9..6a2954b 100644 --- a/src/features/expandos/css/index.css +++ b/src/features/expandos/css/index.css @@ -2,6 +2,10 @@ text-decoration: underline !important; } +.lg-sub-html a.caption-link { + color: var(--md-sys-color-primary); +} + .ol-post-container .expando-button { display: none; } diff --git a/src/features/expandos/expandoProvider.ts b/src/features/expandos/expandoProvider.ts index 66b6922..2243fac 100644 --- a/src/features/expandos/expandoProvider.ts +++ b/src/features/expandos/expandoProvider.ts @@ -1,8 +1,13 @@ -import { OrderedMap } from "immutable"; +export type GalleryEntryData = { + imageSrc: string; + caption?: string; + outbound_url?: string; +} export default interface ExpandoProvider { sitename: string; urlregex: RegExp; usesDataSet?: boolean; - createGalleryData: (post: HTMLDivElement) => Promise>; + canHandlePost: (post: HTMLDivElement) => boolean; + createGalleryData: (post: HTMLDivElement) => Promise; } diff --git a/src/features/expandos/index.ts b/src/features/expandos/index.ts index 2ccf13f..27f483c 100644 --- a/src/features/expandos/index.ts +++ b/src/features/expandos/index.ts @@ -1,12 +1,12 @@ -import "./css/index.css" +import "./css/index.css"; import { OLFeature } from "../base"; -import ExpandoProvider from "./expandoProvider"; +import ExpandoProvider, { GalleryEntryData } from "./expandoProvider"; import RedditGallery from "./redditGallery"; import iReddIt from "./ireddit"; import YoutubeExpando from "./youtube"; -import "video.js" +import "video.js"; import "lightgallery/css/lightgallery.css"; import lightGallery from "lightgallery"; import { LightGallery } from "lightgallery/lightgallery"; @@ -19,21 +19,39 @@ import { allowBodyScroll, preventBodyScroll } from "../../utility/bodyScroll"; const expandoProviders: Array = [ new RedditGallery(), new iReddIt(), - new YoutubeExpando() + new YoutubeExpando(), ]; +enum ClosingState { + open, + closed, + closeByHistoryNavigation, +} + +type Gallery = { + data: GalleryData; + lightGallery: LightGallery; +}; + +type GalleryData = { + id: string; + entries: GalleryEntryData[]; +}; + export default class Expandos extends OLFeature { moduleName = "Expandos"; moduleId = "expandos"; async init() { - window.addEventListener("popstate", (event) => { - if (this.activeGallery !== null) { - this.activeGallery.closeGallery(true); - this.activeGallery = null; - } - }); + window.addEventListener("popstate", this.onPopState.bind(this)); + addEventListener("DOMContentLoaded", () => + this.closeAndOpenGallery(history.state) + ); } async onPost(post: HTMLDivElement) { + const postId = post.dataset.fullname; + if (typeof postId !== "string") { + throw new Error("Post does not have an id!"); + } const thumbnailLink = post.querySelector(".thumbnail"); if (!thumbnailLink) return; const expando_btn = @@ -47,29 +65,44 @@ export default class Expandos extends OLFeature { thumbnailDiv.addEventListener("click", async (e) => { e.preventDefault(); - const expando_btn_R = - post.querySelector(".expando-button"); const gallery = await this.getGallery(post); - this.activeGallery = gallery; if (gallery) { - preventBodyScroll(); - history.pushState({"galleryId": post.dataset.fullname}, '', "#gallery"); - gallery.openGallery(); - } else if (expando_btn_R) { - console.log("Clicking on expando btn", expando_btn) + history.pushState( + gallery.data, + "", + `#gallery_${gallery.data.id}` + ); + this.openGallery(gallery); + return; + } + + const expando_btn_R = + post.querySelector(".expando-button"); + if (expando_btn_R) { + console.log("Clicking on expando btn", expando_btn); expando_btn_R.click(); } else { - console.error("Couldnt find expando button!") + console.error("Couldnt find expando button!"); } }); } - private galleries: { [id: string]: LightGallery | null } = {}; - private activeGallery: LightGallery | null = null; + private galleries: { [id: string]: Gallery | null } = {}; + private activeGallery: Gallery | null = null; + private closingState: ClosingState = ClosingState.closed; + + private openGallery(gallery: Gallery) { + if (this.activeGallery !== null) { + throw new Error("There is already an active gallery!"); + } + preventBodyScroll(); + this.closingState = ClosingState.open; + gallery.lightGallery.openGallery(); + this.activeGallery = gallery; + } private async getGallery(post: HTMLDivElement) { const postId = post.dataset.fullname; - const url = post.dataset.url; if (!postId) { throw "data-fullname attribute is missing from post!"; } @@ -78,51 +111,106 @@ export default class Expandos extends OLFeature { } for (const expandoProvider of expandoProviders) { - if (url && expandoProvider.urlregex.test(url)) { - const gallery = await this.createGallery(expandoProvider, post); - this.galleries[postId] = gallery; - return gallery; + if (expandoProvider.canHandlePost(post)) { + const galleryEntries = await expandoProvider.createGalleryData( + post + ); + return this.createGallery(galleryEntries, postId); } } console.warn( - `Couldn't find expando provider for URL ${url}, falling back to RES/reddit` + `Couldn't find expando provider for URL ${post.dataset.url}, falling back to RES/reddit` ); this.galleries[postId] = null; return null; } - private async createGallery( - expandoProvider: ExpandoProvider, - post: HTMLDivElement - ) { - const imgLinks = await expandoProvider.createGalleryData(post); - const gallery = document.createElement("div"); - for (const [imgLink, imgDesc] of imgLinks) { + private createGallery(galleryEntries: GalleryEntryData[], postId: string) { + const lg = this.createLightGallery(galleryEntries); + const gallery = { + data: { id: postId, entries: galleryEntries }, + lightGallery: lg, + }; + this.galleries[postId] = gallery; + return gallery; + } + + private createLightGallery(galleryEntries: GalleryEntryData[]) { + const galleryDiv = document.createElement("div"); + for (const { imageSrc, caption, outbound_url } of galleryEntries) { const imageAnchorEl = document.createElement("a"); - if (expandoProvider.usesDataSet) { - imageAnchorEl.dataset.src = imgLink - imageAnchorEl.dataset.lgSize = "1280-720" - } else { - imageAnchorEl.href = imgLink; + // if (useDataSet) { + // imageAnchorEl.dataset.src = imageSrc; + // imageAnchorEl.dataset.lgSize = "1280-720"; + // } else { + // imageAnchorEl.href = imageSrc; + // } + imageAnchorEl.href = imageSrc; + + let captionHtml = ""; + if (caption) { + const captionDiv = document.createElement("div"); + captionDiv.innerText = caption; + captionHtml += captionDiv.outerHTML; } - imageAnchorEl.dataset.subHtml = imgDesc; + if (outbound_url) { + const outboundAnchor = document.createElement("a"); + outboundAnchor.href = outbound_url; + outboundAnchor.innerText = outbound_url; + outboundAnchor.classList.add("caption-link"); + captionHtml += outboundAnchor.outerHTML; + } + imageAnchorEl.dataset.subHtml = captionHtml; + const imageEl = document.createElement("img"); imageEl.referrerPolicy = "no-referrer"; - imageEl.src = imgLink; + imageEl.src = imageSrc; imageAnchorEl.append(imageEl); - console.log("Anchor:", imageAnchorEl) - gallery.appendChild(imageAnchorEl); + galleryDiv.appendChild(imageAnchorEl); } - gallery.addEventListener("lgAfterClose", function () { - allowBodyScroll(); - }); - const lg = lightGallery(gallery, { + galleryDiv.addEventListener( + "lgAfterClose", + this.onGalleryClose.bind(this) + ); + const lg = lightGallery(galleryDiv, { plugins: [lgVideo, lgZoom], speed: 250, mobileSettings: {}, - videojs: true + videojs: true, }); return lg; } + + private onPopState(ev: PopStateEvent) { + this.closeAndOpenGallery(ev.state); + } + + private closeAndOpenGallery(state: GalleryData | null) { + const galleryId = state?.id; + + if (this.activeGallery) { + this.closingState = ClosingState.closeByHistoryNavigation; + this.activeGallery?.lightGallery.closeGallery(true); + } + + if (galleryId) { + const gallery = + this.galleries[galleryId] ?? + this.createGallery(state.entries, state.id); + this.openGallery(gallery); + } + } + + private onGalleryClose() { + this.activeGallery = null; + allowBodyScroll(); + const wasClosedByHistoryNav = + this.closingState !== ClosingState.closeByHistoryNavigation; + + this.closingState = ClosingState.closed; + if (wasClosedByHistoryNav) { + history.back(); + } + } } diff --git a/src/features/expandos/ireddit.ts b/src/features/expandos/ireddit.ts index 64abd79..16b34ad 100644 --- a/src/features/expandos/ireddit.ts +++ b/src/features/expandos/ireddit.ts @@ -1,15 +1,20 @@ -import { OrderedMap } from "immutable"; import ExpandoProvider from "./expandoProvider"; export default class iReddIt implements ExpandoProvider { sitename = "i.redd.it"; urlregex = new RegExp(/https:\/\/i\.redd\.it\/.{13}.{3,}/); + canHandlePost(post: HTMLDivElement) { + const url = post.dataset.url; + return !!(url && this.urlregex.test(url)); + } async createGalleryData(post: HTMLDivElement) { - if (post.dataset.url) { - return OrderedMap([[post.dataset.url, ""]]); - } - else { - return OrderedMap(); + const url = post.dataset.url; + if (url) { + return [{ + imageSrc: url + }]; + } else { + return []; } } } diff --git a/src/features/expandos/redditGallery.ts b/src/features/expandos/redditGallery.ts index 5d55355..4761236 100644 --- a/src/features/expandos/redditGallery.ts +++ b/src/features/expandos/redditGallery.ts @@ -1,87 +1,59 @@ -import { OrderedMap } from "immutable"; import ExpandoProvider from "./expandoProvider"; -type ImageMetadata = { - // y: number; - // x: number; - u: string; -}; - -function isImageMetadata(item: unknown): item is ImageMetadata { - return item instanceof Object && "u" in item && typeof item.u === "string"; -} +// type ImageMetadata = { +// // y: number; +// // x: number; +// u: string; +// }; type MediaMetadataItem = { // status: string, // e: string, m: string; - p: unknown[]; //ImageMetadata[]; + // p: unknown[]; //ImageMetadata[]; // s: ImageMetadata, id: string; }; -function isMediaMetadataItem(item: unknown): item is MediaMetadataItem { - return ( - item instanceof Object && - "m" in item && - typeof item.m === "string" && - "p" in item && - Array.isArray(item.p) && - "id" in item && - typeof item.id === "string" - ); -} - export default class RedditGallery implements ExpandoProvider { sitename = "Reddit Gallery"; urlregex = new RegExp(/https:\/\/www\.reddit\.com\/gallery\/.{7}/); + canHandlePost(post: HTMLDivElement) { + const isGallery = post.dataset.isGallery; + return isGallery === "true"; + } async createGalleryData(post: HTMLDivElement) { - let imgMap = OrderedMap(); - const unsortedImgMap = new Map>(); - - const commentsLink = post.querySelector(".comments"); - if (!commentsLink) { - console.error("Couldn't find comments link!"); - return imgMap; - } - - const postUrl = commentsLink.href + ".json"; - console.log("postUrl json url", postUrl); - const response = await fetch(postUrl, { + const postLink = post.dataset.permalink; + const jsonUrl = `https://old.reddit.com${postLink}.json`; + console.log(jsonUrl); + const response = await fetch(jsonUrl, { referrerPolicy: "no-referrer", }); - let postData = await response.json(); - if (Array.isArray(postData)) { - postData = postData[0]; + if (!response.ok) { + console.log(response); + throw new Error("Error in fetching gallery json data"); } - console.log(postData); - postData = postData.data.children[0].data - for (const imgData of Object.values( - postData.media_metadata - )) { - if (!isMediaMetadataItem(imgData)) continue; - const lastImageMetadata = imgData.p.at(-1); - if (!isImageMetadata(lastImageMetadata)) continue; + const responseJson = await response.json(); + const postData = responseJson[0].data.children[0].data; + console.debug("postData is", postData); + + const galleryItems: { + media_id: string; + caption?: string; + outbound_url?: string; + }[] = postData.gallery_data.items; + const mediaMetadata = postData.media_metadata; + return galleryItems.map(({ media_id, caption, outbound_url }) => { + return { + imageSrc: this.getImageSrc(mediaMetadata[media_id]), + caption, + outbound_url + }; + }); + } - const link = document.createElement("a"); - link.classList.add("galleryLink") - const imgFileExtension = imgData.m.split("/")[1]; - link.href = `https://i.redd.it/${imgData.id}.${imgFileExtension}`; // there is probably a better way! - link.innerText = "link to original image"; - unsortedImgMap.set(imgData.id, [lastImageMetadata.u, link.outerHTML]); - } - for (const { media_id, caption } of postData.gallery_data.items) { - console.log("looking up", media_id) - if (unsortedImgMap.has(media_id)) { - // @ts-ignore - const galleryItem: [desc: string, html: string] = unsortedImgMap.get(media_id) - const desc = document.createElement("div"); - if (caption !== undefined) { - desc.innerText = caption - } - imgMap = imgMap.set(galleryItem[0], desc.outerHTML + galleryItem[1]) - } - } - return imgMap; + private getImageSrc(imgData: MediaMetadataItem) { + const imgFileExtension = imgData.m.split("/")[1]; + return `https://i.redd.it/${imgData.id}.${imgFileExtension}`; // there is probably a better way! } } diff --git a/src/features/expandos/youtube.ts b/src/features/expandos/youtube.ts index e49b133..75f0c1e 100644 --- a/src/features/expandos/youtube.ts +++ b/src/features/expandos/youtube.ts @@ -1,4 +1,3 @@ -import { OrderedMap } from "immutable"; import ExpandoProvider from "./expandoProvider"; export default class YoutubeExpando implements ExpandoProvider { @@ -7,16 +6,20 @@ export default class YoutubeExpando implements ExpandoProvider { // grabbed from https://stackoverflow.com/a/67255602 urlregex = new RegExp(/^(?:https?:)?(?:\/\/)?(?:youtu\.be\/|(?:www\.|m\.)?youtube\.com\/(?:watch|v|embed)(?:\.php)?(?:\?.*v=|\/))([a-zA-Z0-9\_-]{7,15})(?:[\?&][a-zA-Z0-9\_-]+=[a-zA-Z0-9\_-]+)*(?:[&\/\#].*)?$/); usesDataSet = true; + canHandlePost(post: HTMLDivElement) { + const url = post.dataset.url; + return !!(url && this.urlregex.test(url)); + } - async createGalleryData(post: HTMLDivElement): Promise> { - let videoMap = OrderedMap(); + async createGalleryData(post: HTMLDivElement) { let videoUrl = (post.dataset.url as string) const regexed = this.urlregex.exec(videoUrl); if (regexed && regexed?.length > 1) { const vid = regexed[1] videoUrl = `https://youtu.be/${vid}?mute=0` } - videoMap = videoMap.set(videoUrl, '') - return videoMap; + return [{ + imageSrc: videoUrl + }]; }; } \ No newline at end of file