From 4f78bb955b3b944f9b5f7b138c22cbb8f031e73b Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:13:00 -0600 Subject: [PATCH 1/2] Add custom overlay for YouTube iframes --- article_forge/Gemfile.lock | 1 + article_forge/articles.rb | 1 + article_forge/articles/androidauthority.rb | 17 +++++ .../articles/fixtures/androidauthority.html | 35 +++++++++ article_forge/public/assets/media.js | 15 ++++ article_forge/public/assets/play-arrow.svg | 1 + article_forge/style/stylesheet.scss | 38 +++++++++- capy/src/main/assets/media.js | 15 ++++ capy/src/main/assets/play-arrow.svg | 3 + capy/src/main/assets/stylesheet.css | 2 +- .../com/jocmp/capy/articles/CleanEmbeds.kt | 74 +++++++++++++++++++ .../jocmp/capy/articles/HtmlPostProcessor.kt | 1 + .../jocmp/capy/articles/CleanEmbedsTest.kt | 52 +++++++++++++ .../com/jocmp/capy/articles/CleanLinksTest.kt | 12 +-- .../com/jocmp/capy/articles/HtmlHelpers.kt | 4 +- .../com/jocmp/capy/articles/WrapTablesTest.kt | 2 +- .../article_partial_androidauthority.html | 19 +++++ 17 files changed, 281 insertions(+), 11 deletions(-) create mode 100644 article_forge/articles/androidauthority.rb create mode 100644 article_forge/articles/fixtures/androidauthority.html create mode 100644 article_forge/public/assets/play-arrow.svg create mode 100644 capy/src/main/assets/play-arrow.svg create mode 100644 capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt create mode 100644 capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt create mode 100644 capy/src/test/resources/article_partial_androidauthority.html diff --git a/article_forge/Gemfile.lock b/article_forge/Gemfile.lock index b8ad047c..34ed0f6c 100644 --- a/article_forge/Gemfile.lock +++ b/article_forge/Gemfile.lock @@ -109,6 +109,7 @@ GEM PLATFORMS arm64-darwin-23 + arm64-darwin-24 x86_64-linux DEPENDENCIES diff --git a/article_forge/articles.rb b/article_forge/articles.rb index 470e3909..b1676904 100644 --- a/article_forge/articles.rb +++ b/article_forge/articles.rb @@ -95,3 +95,4 @@ def self.extracted_content require "./articles/one_pezeshk" require "./articles/secretclub" require "./articles/andreyor" +require "./articles/androidauthority" diff --git a/article_forge/articles/androidauthority.rb b/article_forge/articles/androidauthority.rb new file mode 100644 index 00000000..aa48b075 --- /dev/null +++ b/article_forge/articles/androidauthority.rb @@ -0,0 +1,17 @@ +require "json" + +module Articles + class << self + def androidauthority + extracted = File.read("articles/fixtures/androidauthority.html") + + Article.new( + title: "Samsung Galaxy S25 series rumors and leaks: Everything we know so far", + feed_name: "Android Authority", + external_link: "https://www.androidauthority.com/samsung-galaxy-s25-3437280/", + byline: nil, + body: extracted + ) + end + end +end diff --git a/article_forge/articles/fixtures/androidauthority.html b/article_forge/articles/fixtures/androidauthority.html new file mode 100644 index 00000000..34652f9e --- /dev/null +++ b/article_forge/articles/fixtures/androidauthority.html @@ -0,0 +1,35 @@ +
+ The + Samsung Z Galaxy Fold 6 + + is a fairly unique phone. Not only is it one of the few + foldable phones + widely available across most major markets, but it manages to stand out from + the competition thanks toS Pen support. While the S Pen isn’t included, we highly recommend picking it up if you + want to unlock the device’s full potential. +
++ Of course, the S Pen and large screen won’t help you much if you don’t have + the right apps to pair with it. Thinking about pre-ordering a Galaxy Fold 6? + Let’s take a closer look at the best apps you can download on your new device + once you get it, all of which take advantage of the phone’s large screen and S + Pen functionality. +
diff --git a/article_forge/public/assets/media.js b/article_forge/public/assets/media.js index 56d0e37b..7a921192 100644 --- a/article_forge/public/assets/media.js +++ b/article_forge/public/assets/media.js @@ -14,13 +14,28 @@ function configureVideoTags() { function addImageClickListeners() { [...document.getElementsByTagName("img")].forEach((img) => { + if (img.classList.contains("iframe-embed__image")) { + return; + } + img.addEventListener("click", () => { Android.openImage(img.src, img.alt); }); }); } +function addEmbedListeners() { + [...document.querySelectorAll("div.iframe-embed")].forEach((div) => { + div.addEventListener("click", () => { + const iframe = document.createElement("iframe"); + iframe.src = div.getAttribute("data-iframe-src"); + div.replaceWith(iframe); + }); + }); +} + window.onload = () => { addImageClickListeners(); + addEmbedListeners(); configureVideoTags(); }; diff --git a/article_forge/public/assets/play-arrow.svg b/article_forge/public/assets/play-arrow.svg new file mode 100644 index 00000000..b065533e --- /dev/null +++ b/article_forge/public/assets/play-arrow.svg @@ -0,0 +1 @@ + diff --git a/article_forge/style/stylesheet.scss b/article_forge/style/stylesheet.scss index c336d30f..f8de4416 100644 --- a/article_forge/style/stylesheet.scss +++ b/article_forge/style/stylesheet.scss @@ -96,6 +96,39 @@ pre { padding: 1em; } +.iframe-embed { + display: block; + position: relative; + width: 100%; + border: 0; + border-radius: var(--corner-radius); + + .iframe-embed__image { + width: 100%; + aspect-ratio: 16 / 9; + object-fit: cover; + border-radius: var(--corner-radius); + } + + .iframe-embed__play-button { + background-color: rgba(0, 0, 0, 0.6); + backdrop-filter: blur(0.5em); + border-radius: 50%; + position: absolute; + left: 0; + right: 0; + bottom: 0; + top: 0; + margin: auto; + width: 4rem; + height: 4em !important; + background-image: url("/assets/play-arrow.svg"); + background-repeat: no-repeat; + background-size: 3em; + background-position: center; + } +} + img, figure, video, @@ -110,7 +143,10 @@ iframe { max-width: 100%; margin: 0 auto; - &[src*="youtube-nocookie.com"], &[src*="youtube.com"] { + &[src*="youtube-nocookie.com"], + &[src*="youtube.com"], + &[src*="youtu.be"] { + background-color: #000000; width: 100%; aspect-ratio: 16 / 9; border: 0; diff --git a/capy/src/main/assets/media.js b/capy/src/main/assets/media.js index 56d0e37b..7a921192 100644 --- a/capy/src/main/assets/media.js +++ b/capy/src/main/assets/media.js @@ -14,13 +14,28 @@ function configureVideoTags() { function addImageClickListeners() { [...document.getElementsByTagName("img")].forEach((img) => { + if (img.classList.contains("iframe-embed__image")) { + return; + } + img.addEventListener("click", () => { Android.openImage(img.src, img.alt); }); }); } +function addEmbedListeners() { + [...document.querySelectorAll("div.iframe-embed")].forEach((div) => { + div.addEventListener("click", () => { + const iframe = document.createElement("iframe"); + iframe.src = div.getAttribute("data-iframe-src"); + div.replaceWith(iframe); + }); + }); +} + window.onload = () => { addImageClickListeners(); + addEmbedListeners(); configureVideoTags(); }; diff --git a/capy/src/main/assets/play-arrow.svg b/capy/src/main/assets/play-arrow.svg new file mode 100644 index 00000000..e3d0a39f --- /dev/null +++ b/capy/src/main/assets/play-arrow.svg @@ -0,0 +1,3 @@ + diff --git a/capy/src/main/assets/stylesheet.css b/capy/src/main/assets/stylesheet.css index 3693eaf0..fb4dcf9c 100644 --- a/capy/src/main/assets/stylesheet.css +++ b/capy/src/main/assets/stylesheet.css @@ -1 +1 @@ -@font-face{font-family:"Atkinson Hyperlegible";font-style:normal;font-weight:normal;src:url("/res/font/atkinson_hyperlegible.ttf") format("truetype")}@font-face{font-family:"Inter";font-style:normal;font-weight:normal;src:url("/res/font/inter.ttf") format("truetype")}@font-face{font-family:"Jost";font-style:normal;font-weight:normal;src:url("/res/font/jost.ttf") format("truetype")}@font-face{font-family:"Literata";font-style:normal;font-weight:normal;src:url("/res/font/literata.ttf") format("truetype")}@font-face{font-family:"Poppins";font-style:normal;font-weight:normal;src:url("/res/font/poppins.ttf") format("truetype")}@font-face{font-family:"Vollkorn";font-style:normal;font-weight:normal;src:url("/res/font/vollkorn.ttf") format("truetype")}:root{--corner-radius: 0.1875rem}::selection{background-color:var(--color-primary-container)}body{font-family:sans-serif;word-wrap:break-word;margin:var(--article-top-margin) 0 0 0;padding:2rem 1rem;background-color:var(--color-surface);color:var(--color-on-surface);-webkit-tap-highlight-color:rgb(from var(--color-primary) r g b/30%)}@media only screen and (min-width: 769px){body{padding:2rem 4rem}}a:link,a:visited{color:var(--color-primary)}code,pre{background:var(--color-surface-container);border-radius:var(--corner-radius)}code{padding:.2em}pre{overflow-x:scroll;padding:1em}img,figure,video,div,object{max-width:100%;height:auto !important;margin:0 auto}iframe{max-width:100%;margin:0 auto}iframe[src*="youtube-nocookie.com"],iframe[src*="youtube.com"]{width:100%;aspect-ratio:16/9;border:0;border-radius:var(--corner-radius)}.table__wrapper{overflow-x:auto;width:100%}.table__wrapper table{border-spacing:0}.table__wrapper table table{margin-bottom:0;border:none}.table__wrapper td,.table__wrapper th{border:1px solid #999;padding:.5rem;text-align:left;word-wrap:break-word;overflow-x:auto}.table__wrapper td:has(pre){border:none;padding:0}.article__header{display:block;margin-bottom:1rem}.article__header:link,.article__header:visited{text-decoration:none}video{background-color:#000}video::-webkit-media-controls-fullscreen-button{display:none}.article__title h1{margin:0}#article-body-content{line-height:1.6em;margin-bottom:20vh}#article-body-content img{background-color:var(--color-surface-container-highest)}.article__body--font-default{font-family:sans-serif}.article__body--font-atkinson_hyperlegible{font-family:"Atkinson Hyperlegible"}.article__body--font-inter{font-family:"Inter"}.article__body--font-jost{font-family:"Jost"}.article__body--font-literata{font-family:"Literata"}.article__body--font-poppins{font-family:"Poppins"}.article__body--font-vollkorn{font-family:"Vollkorn"}.article__body--text-size-small{font-size:small}.article__body--text-size-medium{font-size:medium}.article__body--text-size-large{font-size:large}.article__body--text-size-x-large{font-size:x-large}.article__body--text-size-xx-large{font-size:xx-large} \ No newline at end of file +@font-face{font-family:"Atkinson Hyperlegible";font-style:normal;font-weight:normal;src:url("/res/font/atkinson_hyperlegible.ttf") format("truetype")}@font-face{font-family:"Inter";font-style:normal;font-weight:normal;src:url("/res/font/inter.ttf") format("truetype")}@font-face{font-family:"Jost";font-style:normal;font-weight:normal;src:url("/res/font/jost.ttf") format("truetype")}@font-face{font-family:"Literata";font-style:normal;font-weight:normal;src:url("/res/font/literata.ttf") format("truetype")}@font-face{font-family:"Poppins";font-style:normal;font-weight:normal;src:url("/res/font/poppins.ttf") format("truetype")}@font-face{font-family:"Vollkorn";font-style:normal;font-weight:normal;src:url("/res/font/vollkorn.ttf") format("truetype")}:root{--corner-radius: 0.1875rem}::selection{background-color:var(--color-primary-container)}body{font-family:sans-serif;word-wrap:break-word;margin:var(--article-top-margin) 0 0 0;padding:2rem 1rem;background-color:var(--color-surface);color:var(--color-on-surface);-webkit-tap-highlight-color:rgb(from var(--color-primary) r g b/30%)}@media only screen and (min-width: 769px){body{padding:2rem 4rem}}a:link,a:visited{color:var(--color-primary)}code,pre{background:var(--color-surface-container);border-radius:var(--corner-radius)}code{padding:.2em}pre{overflow-x:scroll;padding:1em}.iframe-embed{display:block;position:relative;width:100%;border:0;border-radius:var(--corner-radius)}.iframe-embed .iframe-embed__image{width:100%;aspect-ratio:16/9;object-fit:cover;border-radius:var(--corner-radius)}.iframe-embed .iframe-embed__play-button{background-color:rgba(0,0,0,.6);backdrop-filter:blur(0.5em);border-radius:50%;position:absolute;left:0;right:0;bottom:0;top:0;margin:auto;width:4rem;height:4em !important;background-image:url("/assets/play-arrow.svg");background-repeat:no-repeat;background-size:3em;background-position:center}img,figure,video,div,object{max-width:100%;height:auto !important;margin:0 auto}iframe{max-width:100%;margin:0 auto}iframe[src*="youtube-nocookie.com"],iframe[src*="youtube.com"],iframe[src*="youtu.be"]{background-color:#000;width:100%;aspect-ratio:16/9;border:0;border-radius:var(--corner-radius)}.table__wrapper{overflow-x:auto;width:100%}.table__wrapper table{border-spacing:0}.table__wrapper table table{margin-bottom:0;border:none}.table__wrapper td,.table__wrapper th{border:1px solid #999;padding:.5rem;text-align:left;word-wrap:break-word;overflow-x:auto}.table__wrapper td:has(pre){border:none;padding:0}.article__header{display:block;margin-bottom:1rem}.article__header:link,.article__header:visited{text-decoration:none}video{background-color:#000}video::-webkit-media-controls-fullscreen-button{display:none}.article__title h1{margin:0}#article-body-content{line-height:1.6em;margin-bottom:20vh}#article-body-content img{background-color:var(--color-surface-container-highest)}.article__body--font-default{font-family:sans-serif}.article__body--font-atkinson_hyperlegible{font-family:"Atkinson Hyperlegible"}.article__body--font-inter{font-family:"Inter"}.article__body--font-jost{font-family:"Jost"}.article__body--font-literata{font-family:"Literata"}.article__body--font-poppins{font-family:"Poppins"}.article__body--font-vollkorn{font-family:"Vollkorn"}.article__body--text-size-small{font-size:small}.article__body--text-size-medium{font-size:medium}.article__body--text-size-large{font-size:large}.article__body--text-size-x-large{font-size:x-large}.article__body--text-size-xx-large{font-size:xx-large} \ No newline at end of file diff --git a/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt b/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt new file mode 100644 index 00000000..459a3e7a --- /dev/null +++ b/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt @@ -0,0 +1,74 @@ +package com.jocmp.capy.articles + +import okhttp3.HttpUrl.Companion.toHttpUrl +import org.jsoup.nodes.Document +import org.jsoup.nodes.Element + +internal fun cleanEmbeds(document: Document) { + document.select("iframe").forEach { embed -> + val src = embed.attr("src") + val youtubeID = findYouTubeMatch(src) + + if (youtubeID != null) { + swapPlaceholder(document, embed, src, youtubeID) + } + } +} + +fun findYouTubeMatch(src: String): String? { + YOUTUBE_DOMAINS.forEach { + val match = it.find(src) + + if (match != null) { + return match.groupValues.getOrNull(1) + } + } + + return null +} + +private fun swapPlaceholder(document: Document, embed: Element, src: String, youtubeID: String) { + val placeholderImage = document.createElement("img").apply { + addClass("iframe-embed__image") + attr("src", imageURL(youtubeID)) + } + + val playButton = document.createElement("div").apply { + addClass("iframe-embed__play-button") + } + + val placeholder = document.createElement("div").apply { + addClass("iframe-embed") + attr("data-iframe-src", autoplaySrc(src)) + appendChild(placeholderImage) + appendChild(playButton) + } + + embed.replaceWith(placeholder) +} + +private fun imageURL(id: String): String { + return "https://img.youtube.com/vi/$id/hqdefault.jpg" +} + +private fun autoplaySrc(src: String): String { + return try { + src.toHttpUrl() + .newBuilder() + .setQueryParameter("autoplay", "1") + .toString() + } catch (e: IllegalArgumentException) { + src + } +} + +private val YOUTUBE_DOMAINS = listOf( + Regex(""".*?//www\.youtube-nocookie\.com/embed/(.*?)(\?|$)"""), + Regex(""".*?//www\.youtube\.com/embed/(.*?)(\?|$)"""), + Regex(""".*?//www\.youtube\.com/user/.*?#\w/\w/\w/\w/(.+)\b"""), + Regex(""".*?//www\.youtube\.com/v/(.*?)(#|\?|$)"""), + Regex(""".*?//www\.youtube\.com/watch\?(?:.*?&)?v=([^]*)(?:&|#|$)"""), + Regex(""".*?//youtube-nocookie\.com/embed/(.*?)(\?|$)"""), + Regex(""".*?//youtube\.com/embed/(.*?)(\?|$)"""), + Regex(""".*?//youtu\.be/(.*?)(\?|$)""") +) diff --git a/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt b/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt index 4cb35e77..90697272 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt @@ -4,6 +4,7 @@ import org.jsoup.nodes.Document object HtmlPostProcessor { fun clean(document: Document, hideImages: Boolean) { + cleanEmbeds(document) cleanStyles(document) cleanLinks(document) if (hideImages) { diff --git a/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt b/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt new file mode 100644 index 00000000..82c0b422 --- /dev/null +++ b/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt @@ -0,0 +1,52 @@ +package com.jocmp.capy.articles + +import com.jocmp.capy.articles.HtmlHelpers.html +import com.jocmp.capy.testFile +import kotlin.test.Test +import kotlin.test.assertEquals + +class CleanEmbedsTest { + @Test + fun youtubeEmbeds() { + val document = html( + testFile("article_partial_androidauthority.html").readText() + ) + + cleanEmbeds(document) + + HtmlHelpers.assertEquals(document) { + """ +The Samsung Z Galaxy Fold 6 is a fairly unique phone. Not only is it one of the few foldable phones widely available across most major markets, but it manages to stand out from the competition thanks toS Pen support. While the S Pen isn’t included, we highly recommend picking it up if you want to unlock the device’s full potential.
+Of course, the S Pen and large screen won’t help you much if you don’t have the right apps to pair with it. Thinking about pre-ordering a Galaxy Fold 6? Let’s take a closer look at the best apps you can download on your new device once you get it, all of which take advantage of the phone’s large screen and S Pen functionality.
+ """.trimIndent() + } + } + + @Test + fun `youtube match`() { + val result = + findYouTubeMatch("https://www.youtube.com/embed/FFLIqUhtbvI?autoplay=0&autohide=2border=0&wmode=opaque&enablejsapi=1rel=0&controls=1&showinfo=1") + + assertEquals(expected = "FFLIqUhtbvI", actual = result) + } + + @Test + fun `youtube nocookie match`() { + val result = findYouTubeMatch("https://www.youtube-nocookie.com/embed/mqjMWSnwnTM") + + assertEquals(expected = "mqjMWSnwnTM", actual = result) + } + + @Test + fun `youtu be match`() { + val result = findYouTubeMatch("https://youtu.be/mqjMWSnwnTM") + + assertEquals(expected = "mqjMWSnwnTM", actual = result) + } +} diff --git a/capy/src/test/java/com/jocmp/capy/articles/CleanLinksTest.kt b/capy/src/test/java/com/jocmp/capy/articles/CleanLinksTest.kt index 96b484a4..27f791f9 100644 --- a/capy/src/test/java/com/jocmp/capy/articles/CleanLinksTest.kt +++ b/capy/src/test/java/com/jocmp/capy/articles/CleanLinksTest.kt @@ -9,13 +9,13 @@ import kotlin.test.assertEquals class CleanLinksTest { @Test fun `makes all subsequent links lazy loaded`() { - val document = html { + val document = html( """The + Samsung Z Galaxy + Fold 6 + + is a fairly unique phone. Not only is it one of the few + foldable phones + widely available across most major markets, but it manages to stand out from the competition + thanks toS + Pen support. While the S Pen isn’t included, we highly recommend picking it up if you + want to unlock the device’s full potential. +
+Of course, the S Pen and large screen won’t help you much if you don’t have the right apps to + pair with it. Thinking about pre-ordering a Galaxy Fold 6? Let’s take a closer look at the best + apps you can download on your new device once you get it, all of which take advantage of the + phone’s large screen and S Pen functionality. +
From 32ad24d05a6398c6dcfc9a130cf7fa5c39633ff6 Mon Sep 17 00:00:00 2001 From: Josiah Campbell <9521010+jocmp@users.noreply.github.com> Date: Fri, 20 Dec 2024 00:01:31 -0600 Subject: [PATCH 2/2] Refactor embed cleaner in JS --- .../capyreader/app/ui/components/WebView.kt | 2 + .../articles/fixtures/androidauthority.html | 20 ++-- article_forge/public/assets/media.js | 91 +++++++++++++++++++ capy/src/main/assets/media.js | 88 ++++++++++++++++++ .../com/jocmp/capy/articles/CleanEmbeds.kt | 74 --------------- .../jocmp/capy/articles/HtmlPostProcessor.kt | 1 - .../java/com/jocmp/capy/articles/ParseHTML.kt | 7 +- .../jocmp/capy/articles/CleanEmbedsTest.kt | 52 ----------- .../article_partial_androidauthority.html | 19 ---- 9 files changed, 196 insertions(+), 158 deletions(-) delete mode 100644 capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt delete mode 100644 capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt delete mode 100644 capy/src/test/resources/article_partial_androidauthority.html diff --git a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt index 314f62ac..341d69d7 100644 --- a/app/src/main/java/com/capyreader/app/ui/components/WebView.kt +++ b/app/src/main/java/com/capyreader/app/ui/components/WebView.kt @@ -159,6 +159,8 @@ class WebViewState( scope.launch { withContext(Dispatchers.IO) { + + val html = renderer.render( article, hideImages = !showImages, diff --git a/article_forge/articles/fixtures/androidauthority.html b/article_forge/articles/fixtures/androidauthority.html index 34652f9e..2c3252cf 100644 --- a/article_forge/articles/fixtures/androidauthority.html +++ b/article_forge/articles/fixtures/androidauthority.html @@ -1,14 +1,14 @@The diff --git a/article_forge/public/assets/media.js b/article_forge/public/assets/media.js index 7a921192..b8725141 100644 --- a/article_forge/public/assets/media.js +++ b/article_forge/public/assets/media.js @@ -34,6 +34,97 @@ function addEmbedListeners() { }); } +/** + * @param {HTMLDivElement | Document} element + * @returns boolean + */ +function cleanEmbeds(element = document) { + const embeds = element.querySelectorAll("iframe"); + + for (const embed of embeds) { + const src = embed.getAttribute("src"); + const youtubeID = findYouTubeMatch(src); + + if (youtubeID !== null) { + swapPlaceholder(embed, src, youtubeID); + addEmbedListeners(); + + return true; + } + } + + return false; +} + +/** + * @param {string} src + * @returns string | null + */ +function findYouTubeMatch(src) { + for (const regex of YOUTUBE_DOMAINS) { + const match = src.match(regex); + if (match) { + return match[1]; + } + } + return null; +} + +/** + * @param {HTMLIFrameElement} embed + * @param {string} src + * @param {string} youtubeID + */ +function swapPlaceholder(embed, src, youtubeID) { + const placeholderImage = document.createElement("img"); + placeholderImage.classList.add("iframe-embed__image", "mercury-parser-keep"); + placeholderImage.setAttribute("src", imageURL(youtubeID)); + + const playButton = document.createElement("div"); + playButton.classList.add("iframe-embed__play-button"); + + const placeholder = document.createElement("div"); + placeholder.classList.add("iframe-embed"); + placeholder.setAttribute("data-iframe-src", autoplaySrc(src)); + placeholder.appendChild(placeholderImage); + placeholder.appendChild(playButton); + + embed.replaceWith(placeholder); +} + +function imageURL(id) { + return `https://img.youtube.com/vi/${id}/hqdefault.jpg`; +} + +/** + * @param {string} src + * @returns string + */ +function autoplaySrc(src) { + try { + const url = new URL(src); + url.searchParams.set("autoplay", "1"); + return url.toString(); + } catch (e) { + return src; + } +} + +const YOUTUBE_DOMAINS = [ + /.*?\/\/www\.youtube-nocookie\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/www\.youtube\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/www\.youtube\.com\/user\/.*?#\w\/\w\/\w\/\w\/(.+)\b/, + /.*?\/\/www\.youtube\.com\/v\/(.*?)(#|\?|$)/, + /.*?\/\/www\.youtube\.com\/watch\?(?:.*?&)?v=([^]*)(?:&|#|$)/, + /.*?\/\/youtube-nocookie\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/youtube\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/youtu\.be\/(.*?)(\?|$)/ +]; + +window.addEventListener("DOMContentLoaded", () => { + cleanEmbeds() +}); + window.onload = () => { addImageClickListeners(); addEmbedListeners(); diff --git a/capy/src/main/assets/media.js b/capy/src/main/assets/media.js index 7a921192..0bfad937 100644 --- a/capy/src/main/assets/media.js +++ b/capy/src/main/assets/media.js @@ -34,7 +34,95 @@ function addEmbedListeners() { }); } +/** + * @param {HTMLDivElement | Document} element + * @returns boolean + */ +function cleanEmbeds(element = document) { + const embeds = element.querySelectorAll("iframe"); + + for (const embed of embeds) { + const src = embed.getAttribute("src"); + const youtubeID = findYouTubeMatch(src); + + if (youtubeID !== null) { + swapPlaceholder(embed, src, youtubeID); + addEmbedListeners(); + + return true; + } + } + + return false; +} + +/** + * @param {string} src + * @returns string | null + */ +function findYouTubeMatch(src) { + for (const regex of YOUTUBE_DOMAINS) { + const match = src.match(regex); + if (match) { + return match[1]; + } + } + return null; +} + +/** + * @param {HTMLIFrameElement} embed + * @param {string} src + * @param {string} youtubeID + */ +function swapPlaceholder(embed, src, youtubeID) { + const placeholderImage = document.createElement("img"); + placeholderImage.classList.add("iframe-embed__image", "mercury-parser-keep"); + placeholderImage.setAttribute("src", imageURL(youtubeID)); + + const playButton = document.createElement("div"); + playButton.classList.add("iframe-embed__play-button"); + + const placeholder = document.createElement("div"); + placeholder.classList.add("iframe-embed"); + placeholder.setAttribute("data-iframe-src", autoplaySrc(src)); + placeholder.appendChild(placeholderImage); + placeholder.appendChild(playButton); + + embed.replaceWith(placeholder); +} + +function imageURL(id) { + return `https://img.youtube.com/vi/${id}/hqdefault.jpg`; +} + +/** + * @param {string} src + * @returns string + */ +function autoplaySrc(src) { + try { + const url = new URL(src); + url.searchParams.set("autoplay", "1"); + return url.toString(); + } catch (e) { + return src; + } +} + +const YOUTUBE_DOMAINS = [ + /.*?\/\/www\.youtube-nocookie\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/www\.youtube\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/www\.youtube\.com\/user\/.*?#\w\/\w\/\w\/\w\/(.+)\b/, + /.*?\/\/www\.youtube\.com\/v\/(.*?)(#|\?|$)/, + /.*?\/\/www\.youtube\.com\/watch\?(?:.*?&)?v=([^]*)(?:&|#|$)/, + /.*?\/\/youtube-nocookie\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/youtube\.com\/embed\/(.*?)(\?|$)/, + /.*?\/\/youtu\.be\/(.*?)(\?|$)/ +]; + window.onload = () => { + cleanEmbeds() addImageClickListeners(); addEmbedListeners(); configureVideoTags(); diff --git a/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt b/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt deleted file mode 100644 index 459a3e7a..00000000 --- a/capy/src/main/java/com/jocmp/capy/articles/CleanEmbeds.kt +++ /dev/null @@ -1,74 +0,0 @@ -package com.jocmp.capy.articles - -import okhttp3.HttpUrl.Companion.toHttpUrl -import org.jsoup.nodes.Document -import org.jsoup.nodes.Element - -internal fun cleanEmbeds(document: Document) { - document.select("iframe").forEach { embed -> - val src = embed.attr("src") - val youtubeID = findYouTubeMatch(src) - - if (youtubeID != null) { - swapPlaceholder(document, embed, src, youtubeID) - } - } -} - -fun findYouTubeMatch(src: String): String? { - YOUTUBE_DOMAINS.forEach { - val match = it.find(src) - - if (match != null) { - return match.groupValues.getOrNull(1) - } - } - - return null -} - -private fun swapPlaceholder(document: Document, embed: Element, src: String, youtubeID: String) { - val placeholderImage = document.createElement("img").apply { - addClass("iframe-embed__image") - attr("src", imageURL(youtubeID)) - } - - val playButton = document.createElement("div").apply { - addClass("iframe-embed__play-button") - } - - val placeholder = document.createElement("div").apply { - addClass("iframe-embed") - attr("data-iframe-src", autoplaySrc(src)) - appendChild(placeholderImage) - appendChild(playButton) - } - - embed.replaceWith(placeholder) -} - -private fun imageURL(id: String): String { - return "https://img.youtube.com/vi/$id/hqdefault.jpg" -} - -private fun autoplaySrc(src: String): String { - return try { - src.toHttpUrl() - .newBuilder() - .setQueryParameter("autoplay", "1") - .toString() - } catch (e: IllegalArgumentException) { - src - } -} - -private val YOUTUBE_DOMAINS = listOf( - Regex(""".*?//www\.youtube-nocookie\.com/embed/(.*?)(\?|$)"""), - Regex(""".*?//www\.youtube\.com/embed/(.*?)(\?|$)"""), - Regex(""".*?//www\.youtube\.com/user/.*?#\w/\w/\w/\w/(.+)\b"""), - Regex(""".*?//www\.youtube\.com/v/(.*?)(#|\?|$)"""), - Regex(""".*?//www\.youtube\.com/watch\?(?:.*?&)?v=([^]*)(?:&|#|$)"""), - Regex(""".*?//youtube-nocookie\.com/embed/(.*?)(\?|$)"""), - Regex(""".*?//youtube\.com/embed/(.*?)(\?|$)"""), - Regex(""".*?//youtu\.be/(.*?)(\?|$)""") -) diff --git a/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt b/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt index 90697272..4cb35e77 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/HtmlPostProcessor.kt @@ -4,7 +4,6 @@ import org.jsoup.nodes.Document object HtmlPostProcessor { fun clean(document: Document, hideImages: Boolean) { - cleanEmbeds(document) cleanStyles(document) cleanLinks(document) if (hideImages) { diff --git a/capy/src/main/java/com/jocmp/capy/articles/ParseHTML.kt b/capy/src/main/java/com/jocmp/capy/articles/ParseHTML.kt index 0bb36309..fe041e86 100644 --- a/capy/src/main/java/com/jocmp/capy/articles/ParseHTML.kt +++ b/capy/src/main/java/com/jocmp/capy/articles/ParseHTML.kt @@ -18,16 +18,19 @@ fun parseHtml(article: Article, document: Document, hideImages: Boolean): String extracted.id = "article-body-content" extracted.innerHTML = article.content; + let hasEmbed = cleanEmbeds(extracted); + let shouldAddImage = article.lead_image_url && !hideImages && - ![...extracted.querySelectorAll("img")].some(img => img.src.includes(article.lead_image_url)); + !hasEmbed && + ![...extracted.querySelectorAll("img")].some(img => img.src.includes(article.lead_image_url)); if (shouldAddImage) { let leadImage = document.createElement("img"); leadImage.src = article.lead_image_url; extracted.prepend(leadImage); } - + let content = document.getElementById("article-body-content"); content.replaceWith(extracted); }); diff --git a/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt b/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt deleted file mode 100644 index 82c0b422..00000000 --- a/capy/src/test/java/com/jocmp/capy/articles/CleanEmbedsTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package com.jocmp.capy.articles - -import com.jocmp.capy.articles.HtmlHelpers.html -import com.jocmp.capy.testFile -import kotlin.test.Test -import kotlin.test.assertEquals - -class CleanEmbedsTest { - @Test - fun youtubeEmbeds() { - val document = html( - testFile("article_partial_androidauthority.html").readText() - ) - - cleanEmbeds(document) - - HtmlHelpers.assertEquals(document) { - """ -
The Samsung Z Galaxy Fold 6 is a fairly unique phone. Not only is it one of the few foldable phones widely available across most major markets, but it manages to stand out from the competition thanks toS Pen support. While the S Pen isn’t included, we highly recommend picking it up if you want to unlock the device’s full potential.
-Of course, the S Pen and large screen won’t help you much if you don’t have the right apps to pair with it. Thinking about pre-ordering a Galaxy Fold 6? Let’s take a closer look at the best apps you can download on your new device once you get it, all of which take advantage of the phone’s large screen and S Pen functionality.
- """.trimIndent() - } - } - - @Test - fun `youtube match`() { - val result = - findYouTubeMatch("https://www.youtube.com/embed/FFLIqUhtbvI?autoplay=0&autohide=2border=0&wmode=opaque&enablejsapi=1rel=0&controls=1&showinfo=1") - - assertEquals(expected = "FFLIqUhtbvI", actual = result) - } - - @Test - fun `youtube nocookie match`() { - val result = findYouTubeMatch("https://www.youtube-nocookie.com/embed/mqjMWSnwnTM") - - assertEquals(expected = "mqjMWSnwnTM", actual = result) - } - - @Test - fun `youtu be match`() { - val result = findYouTubeMatch("https://youtu.be/mqjMWSnwnTM") - - assertEquals(expected = "mqjMWSnwnTM", actual = result) - } -} diff --git a/capy/src/test/resources/article_partial_androidauthority.html b/capy/src/test/resources/article_partial_androidauthority.html deleted file mode 100644 index 9ad57798..00000000 --- a/capy/src/test/resources/article_partial_androidauthority.html +++ /dev/null @@ -1,19 +0,0 @@ -The - Samsung Z Galaxy - Fold 6 - - is a fairly unique phone. Not only is it one of the few - foldable phones - widely available across most major markets, but it manages to stand out from the competition - thanks toS - Pen support. While the S Pen isn’t included, we highly recommend picking it up if you - want to unlock the device’s full potential. -
-Of course, the S Pen and large screen won’t help you much if you don’t have the right apps to - pair with it. Thinking about pre-ordering a Galaxy Fold 6? Let’s take a closer look at the best - apps you can download on your new device once you get it, all of which take advantage of the - phone’s large screen and S Pen functionality. -