DEV: Remove lazy-yt and replace with lazy-videos (#20722)
- Refactors the old plugin to remove jquery usage - Adds support for Vimeo videos (default on) and Tiktok (experimental and default off)
This commit is contained in:
parent
86f5abfa18
commit
afe3e36363
|
@ -38,7 +38,7 @@
|
|||
!/plugins/discourse-local-dates
|
||||
!/plugins/discourse-narrative-bot
|
||||
!/plugins/discourse-presence
|
||||
!/plugins/lazy-yt/
|
||||
!/plugins/discourse-lazy-videos/
|
||||
!/plugins/chat/
|
||||
!/plugins/poll/
|
||||
!/plugins/styleguide
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
#topic-progress,
|
||||
.quote-controls,
|
||||
.topic-timer-info,
|
||||
div.lazyYT,
|
||||
div.lazy-video-container,
|
||||
.post-info.edits,
|
||||
.post-action,
|
||||
.saving-text,
|
||||
|
|
|
@ -396,7 +396,7 @@ class SearchIndexer
|
|||
end
|
||||
|
||||
MENTION_CLASSES ||= %w[mention mention-group]
|
||||
ATTRIBUTES ||= %w[alt title href data-youtube-title]
|
||||
ATTRIBUTES ||= %w[alt title href data-video-title]
|
||||
|
||||
def start_element(_name, attributes = [])
|
||||
attributes = Hash[*attributes.flatten]
|
||||
|
|
|
@ -45,6 +45,7 @@ class Plugin::Metadata
|
|||
"discourse-graphviz",
|
||||
"discourse-group-tracker",
|
||||
"discourse-invite-tokens",
|
||||
"discourse-lazy-videos",
|
||||
"discourse-local-dates",
|
||||
"discourse-login-with-amazon",
|
||||
"discourse-logster-rate-limit-checker",
|
||||
|
@ -93,7 +94,6 @@ class Plugin::Metadata
|
|||
"discourse-zendesk-plugin",
|
||||
"docker_manager",
|
||||
"chat",
|
||||
"lazy-yt",
|
||||
"poll",
|
||||
"styleguide",
|
||||
],
|
||||
|
|
|
@ -433,13 +433,19 @@ module PrettyText
|
|||
|
||||
# extract Youtube links
|
||||
doc
|
||||
.css("div[data-youtube-id]")
|
||||
.css("div[data-video-id]")
|
||||
.each do |div|
|
||||
if div["data-youtube-id"].present?
|
||||
links << DetectedLink.new(
|
||||
"https://www.youtube.com/watch?v=#{div["data-youtube-id"]}",
|
||||
false,
|
||||
)
|
||||
if div["data-video-id"].present? && div["data-provider-name"].present?
|
||||
base_url =
|
||||
case div["data-provider-name"]
|
||||
when "youtube"
|
||||
"https://www.youtube.com/watch?v="
|
||||
when "vimeo"
|
||||
"https://vimeo.com/"
|
||||
when "tiktok"
|
||||
"https://m.tiktok.com/v/"
|
||||
end
|
||||
links << DetectedLink.new(base_url + div["data-video-id"], false)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ directory "plugins"
|
|||
|
||||
desc "install all official plugins (use GIT_WRITE=1 to pull with write access)"
|
||||
task "plugin:install_all_official" do
|
||||
skip = Set.new(%w[customer-flair lazy-yt poll])
|
||||
skip = Set.new(%w[customer-flair poll])
|
||||
|
||||
map = { "Canned Replies" => "https://github.com/discourse/discourse-canned-replies" }
|
||||
|
||||
|
|
|
@ -13,7 +13,13 @@
|
|||
{{#each this.cookedBodies as |cooked|}}
|
||||
{{#if cooked.needsCollapser}}
|
||||
<Collapser @header={{cooked.header}} @onToggle={{@onToggleCollapse}}>
|
||||
{{cooked.body}}
|
||||
{{#if cooked.videoAttributes}}
|
||||
<div class="chat-message-collapser-lazy-video">
|
||||
<LazyVideo @videoAttributes={{cooked.videoAttributes}} />
|
||||
</div>
|
||||
{{else}}
|
||||
{{cooked.body}}
|
||||
{{/if}}
|
||||
</Collapser>
|
||||
{{else}}
|
||||
{{cooked.body}}
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import domFromString from "discourse-common/lib/dom-from-string";
|
||||
import I18n from "I18n";
|
||||
|
||||
export default class ChatMessageCollapser extends Component {
|
||||
@service siteSettings;
|
||||
|
||||
get hasUploads() {
|
||||
return hasUploads(this.args.uploads);
|
||||
}
|
||||
|
@ -28,8 +31,8 @@ export default class ChatMessageCollapser extends Component {
|
|||
domFromString(this.args.cooked)
|
||||
);
|
||||
|
||||
if (hasYoutube(elements)) {
|
||||
return this.youtubeCooked(elements);
|
||||
if (hasLazyVideo(elements)) {
|
||||
return this.lazyVideoCooked(elements);
|
||||
}
|
||||
|
||||
if (hasImageOnebox(elements)) {
|
||||
|
@ -47,20 +50,26 @@ export default class ChatMessageCollapser extends Component {
|
|||
return [];
|
||||
}
|
||||
|
||||
youtubeCooked(elements) {
|
||||
lazyVideoCooked(elements) {
|
||||
return elements.reduce((acc, e) => {
|
||||
if (youtubePredicate(e)) {
|
||||
const id = e.dataset.youtubeId;
|
||||
const link = `https://www.youtube.com/watch?v=${escapeExpression(id)}`;
|
||||
const title = escapeExpression(e.dataset.youtubeTitle);
|
||||
const header = htmlSafe(
|
||||
`<a target="_blank" class="chat-message-collapser-link" rel="noopener noreferrer" href="${link}">${title}</a>`
|
||||
);
|
||||
const body = document.createElement("div");
|
||||
body.className = "chat-message-collapser-youtube";
|
||||
body.appendChild(e);
|
||||
if (this.siteSettings.lazy_videos_enabled && lazyVideoPredicate(e)) {
|
||||
const getVideoAttributes = requirejs(
|
||||
"discourse/plugins/discourse-lazy-videos/lib/lazy-video-attributes"
|
||||
).default;
|
||||
|
||||
acc.push({ header, body, needsCollapser: true });
|
||||
const videoAttributes = getVideoAttributes(e);
|
||||
|
||||
if (this.siteSettings[`lazy_${videoAttributes.providerName}_enabled`]) {
|
||||
const link = escapeExpression(videoAttributes.url);
|
||||
const title = videoAttributes.title;
|
||||
const header = htmlSafe(
|
||||
`<a target="_blank" class="chat-message-collapser-link" rel="noopener noreferrer" href="${link}">${title}</a>`
|
||||
);
|
||||
|
||||
acc.push({ header, body: e, videoAttributes, needsCollapser: true });
|
||||
} else {
|
||||
acc.push({ body: e, needsCollapser: false });
|
||||
}
|
||||
} else {
|
||||
acc.push({ body: e, needsCollapser: false });
|
||||
}
|
||||
|
@ -125,16 +134,12 @@ export default class ChatMessageCollapser extends Component {
|
|||
}
|
||||
}
|
||||
|
||||
function youtubePredicate(e) {
|
||||
return (
|
||||
e.classList.length &&
|
||||
e.classList.contains("onebox") &&
|
||||
e.classList.contains("lazyYT-container")
|
||||
);
|
||||
function lazyVideoPredicate(e) {
|
||||
return e.classList.contains("lazy-video-container");
|
||||
}
|
||||
|
||||
function hasYoutube(elements) {
|
||||
return elements.some((e) => youtubePredicate(e));
|
||||
function hasLazyVideo(elements) {
|
||||
return elements.some((e) => lazyVideoPredicate(e));
|
||||
}
|
||||
|
||||
function animatedImagePredicate(e) {
|
||||
|
@ -198,7 +203,7 @@ export function isCollapsible(cooked, uploads) {
|
|||
const elements = Array.prototype.slice.call(domFromString(cooked));
|
||||
|
||||
return (
|
||||
hasYoutube(elements) ||
|
||||
hasLazyVideo(elements) ||
|
||||
hasImageOnebox(elements) ||
|
||||
hasUploads(uploads) ||
|
||||
hasImage(elements) ||
|
||||
|
|
|
@ -34,20 +34,6 @@ export default {
|
|||
}
|
||||
);
|
||||
}
|
||||
if (siteSettings.lazy_yt_enabled) {
|
||||
api.decorateChatMessage(
|
||||
(element) => {
|
||||
element
|
||||
.querySelectorAll(".lazyYT:not(.lazyYT-video-loaded)")
|
||||
.forEach((iframe) => {
|
||||
$(iframe).lazyYT();
|
||||
});
|
||||
},
|
||||
{
|
||||
id: "lazy-yt",
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
initialize(container) {
|
||||
|
|
|
@ -21,9 +21,24 @@ $max_image_height: 150px;
|
|||
.chat-message-collapser
|
||||
.chat-message-collapser-header
|
||||
+ div
|
||||
.chat-message-collapser-youtube {
|
||||
.chat-message-collapser-lazy-video {
|
||||
object-fit: contain;
|
||||
height: $max_image_height;
|
||||
width: calc(#{$max_image_height} / 9 * 16);
|
||||
}
|
||||
|
||||
// Prevent overflow of old lazy-yt images
|
||||
// TODO: remove in December 2023
|
||||
.lazyYT.lazyYT-container {
|
||||
border: none;
|
||||
a {
|
||||
display: flex;
|
||||
}
|
||||
.ytp-thumbnail-image {
|
||||
object-fit: contain;
|
||||
height: $max_image_height;
|
||||
width: calc(#{$max_image_height} / 9 * 16);
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -88,16 +88,6 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
img.ytp-thumbnail-image,
|
||||
img.youtube-thumbnail {
|
||||
height: 100%;
|
||||
max-height: unset;
|
||||
|
||||
&:hover {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Automatic aspect-ratio mapping https://developer.mozilla.org/en-US/docs/Web/Media/images/aspect_ratio_mapping
|
||||
p img:not(.emoji) {
|
||||
max-width: 100%;
|
||||
|
|
|
@ -30,7 +30,7 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
|
|||
id: 1,
|
||||
message: "https://www.youtube.com/watch?v=aOWkVdU4NH0",
|
||||
cooked:
|
||||
'<div class="onebox lazyYT lazyYT-container" data-youtube-id="aOWkVdU4NH0" data-youtube-title="Picnic with my cat (shaved ice & lemonade)" data-parameters="feature=oembed&wmode=opaque"> <a href="https://www.youtube.com/watch?v=aOWkVdU4NH0" target="_blank" rel="nofollow ugc noopener"> <img class="ytp-thumbnail-image" src="/images/discourse-logo-sketch.png" title="Picnic with my cat (shaved ice & lemonade)"></a></div>',
|
||||
'<div class="youtube-onebox lazy-video-container" data-video-id="aOWkVdU4NH0" data-video-title="Picnic with my cat (shaved ice & lemonade)" data-provider-name="youtube"> <a href="https://www.youtube.com/watch?v=aOWkVdU4NH0" target="_blank" rel="nofollow ugc noopener"> <img class="youtube-thumbnail" src="https://img.youtube.com/vi/aOWkVdU4NH0/maxresdefault.jpg" title="Picnic with my cat (shaved ice & lemonade)"> </a> </div>',
|
||||
excerpt:
|
||||
'<a href="https://www.youtube.com/watch?v=aOWkVdU4NH0">[Picnic with my cat (shaved ice & lemonade…</a>',
|
||||
created_at: "2021-07-20T08:14:16.950Z",
|
||||
|
@ -106,8 +106,9 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
|
|||
);
|
||||
});
|
||||
|
||||
skip("can collapse and expand youtube chat", async function (assert) {
|
||||
const youtubeContainer = ".chat-message-container[data-id='1'] .lazyYT";
|
||||
skip("can collapse and expand videos in chat", async function (assert) {
|
||||
const videoContainer =
|
||||
".chat-message-container[data-id='1'] .lazy-video-container";
|
||||
const expandImage =
|
||||
".chat-message-container[data-id='1'] .chat-message-collapser-closed";
|
||||
const collapseImage =
|
||||
|
@ -115,19 +116,19 @@ acceptance("Discourse Chat - Chat live pane collapse", function (needs) {
|
|||
|
||||
await visit("/chat/c/cat/1");
|
||||
|
||||
assert.ok(visible(youtubeContainer));
|
||||
assert.ok(visible(videoContainer));
|
||||
assert.ok(visible(collapseImage), "the open arrow is shown");
|
||||
assert.notOk(exists(expandImage), "the close arrow is hidden");
|
||||
|
||||
await click(collapseImage);
|
||||
|
||||
assert.notOk(visible(youtubeContainer));
|
||||
assert.notOk(visible(videoContainer));
|
||||
assert.ok(visible(expandImage), "the close arrow is shown");
|
||||
assert.notOk(exists(collapseImage), "the open arrow is hidden");
|
||||
|
||||
await click(expandImage);
|
||||
|
||||
assert.ok(visible(youtubeContainer));
|
||||
assert.ok(visible(videoContainer));
|
||||
assert.ok(visible(collapseImage), "the open arrow is shown again");
|
||||
assert.notOk(exists(expandImage), "the close arrow is hidden again");
|
||||
});
|
||||
|
|
|
@ -10,9 +10,9 @@ import { module, test } from "qunit";
|
|||
|
||||
const youtubeCooked =
|
||||
"<p>written text</p>" +
|
||||
'<div class="onebox lazyYT-container" data-youtube-id="ytId1" data-youtube-title="Cats are great">Vid 1</div>' +
|
||||
'<div class="youtube-onebox lazy-video-container" data-video-id="ytId1" data-video-title="Cats are great" data-provider-name="youtube"> <a href="https://www.youtube.com/watch?v=ytId1"></a>Vid 1</div>' +
|
||||
"<p>more written text</p>" +
|
||||
'<div class="onebox lazyYT-container" data-youtube-id="ytId2" data-youtube-title="Kittens are great">Vid 2</div>' +
|
||||
'<div class="youtube-onebox lazy-video-container" data-video-id="ytId2" data-video-title="Kittens are great" data-provider-name="youtube"> <a href="https://www.youtube.com/watch?v=ytId2"></a>Vid 2</div>' +
|
||||
"<p>and even more</p>";
|
||||
|
||||
const animatedImageCooked =
|
||||
|
@ -71,7 +71,13 @@ module(
|
|||
setupRenderingTest(hooks);
|
||||
|
||||
test("escapes youtube header", async function (assert) {
|
||||
this.set("cooked", youtubeCooked.replace("ytId1", evilString));
|
||||
this.set(
|
||||
"cooked",
|
||||
youtubeCooked.replace(
|
||||
"https://www.youtube.com/watch?v=ytId1",
|
||||
`https://www.youtube.com/watch?v=${evilString}`
|
||||
)
|
||||
);
|
||||
await render(hbs`<ChatMessageCollapser @cooked={{this.cooked}} />`);
|
||||
|
||||
assert.true(
|
||||
|
@ -124,7 +130,7 @@ module(
|
|||
|
||||
await render(hbs`<ChatMessageCollapser @cooked={{this.cooked}} />`);
|
||||
|
||||
const youtubeDivs = queryAll(".onebox");
|
||||
const youtubeDivs = queryAll(".youtube-onebox");
|
||||
|
||||
assert.strictEqual(
|
||||
youtubeDivs.length,
|
||||
|
@ -138,11 +144,11 @@ module(
|
|||
);
|
||||
|
||||
assert.false(
|
||||
visible(".onebox[data-youtube-id='ytId1']"),
|
||||
visible(".youtube-onebox[data-video-id='ytId1']"),
|
||||
"first youtube preview hidden"
|
||||
);
|
||||
assert.true(
|
||||
visible(".onebox[data-youtube-id='ytId2']"),
|
||||
visible(".youtube-onebox[data-video-id='ytId2']"),
|
||||
"second youtube preview still visible"
|
||||
);
|
||||
|
||||
|
@ -160,11 +166,11 @@ module(
|
|||
);
|
||||
|
||||
assert.true(
|
||||
visible(".onebox[data-youtube-id='ytId1']"),
|
||||
visible(".youtube-onebox[data-video-id='ytId1']"),
|
||||
"first youtube preview still visible"
|
||||
);
|
||||
assert.false(
|
||||
visible(".onebox[data-youtube-id='ytId2']"),
|
||||
visible(".youtube-onebox[data-video-id='ytId2']"),
|
||||
"second youtube preview hidden"
|
||||
);
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ module("Discourse Chat | Component | chat-message-text", function (hooks) {
|
|||
test("shows collapsed", async function (assert) {
|
||||
this.set(
|
||||
"cooked",
|
||||
'<div class="onebox lazyYT lazyYT-container" data-youtube-id="WaT_rLGuUr8" data-youtube-title="Japanese Katsu Curry (Pork Cutlet)"/>'
|
||||
'<div class="youtube-onebox lazy-video-container" data-video-id="WaT_rLGuUr8" data-video-title="Japanese Katsu Curry (Pork Cutlet)" data-provider-name="youtube"/>'
|
||||
);
|
||||
|
||||
await render(
|
||||
|
@ -51,7 +51,10 @@ module("Discourse Chat | Component | chat-message-text", function (hooks) {
|
|||
});
|
||||
|
||||
test("shows edits - collapsible message", async function (assert) {
|
||||
this.set("cooked", '<div class="onebox lazyYT-container"></div>');
|
||||
this.set(
|
||||
"cooked",
|
||||
'<div class="youtube-onebox lazy-video-container"></div>'
|
||||
);
|
||||
|
||||
await render(
|
||||
hbs`<ChatMessageText @cooked={{this.cooked}} @edited={{true}} />`
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
## Discourse Lazy Videos
|
||||
|
||||
Adds lazy loading support for embedded videos
|
||||
|
||||
### Supported providers
|
||||
|
||||
- YouTube
|
||||
- Vimeo
|
||||
|
||||
### Experimental
|
||||
|
||||
- TikTok
|
|
@ -0,0 +1,11 @@
|
|||
{{#if @providerName}}
|
||||
<iframe
|
||||
src={{this.iframeSrc}}
|
||||
title={{@title}}
|
||||
allowFullScreen
|
||||
scrolling="no"
|
||||
frameborder="0"
|
||||
seamless="seamless"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
|
||||
></iframe>
|
||||
{{/if}}
|
|
@ -0,0 +1,14 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class LazyVideo extends Component {
|
||||
get iframeSrc() {
|
||||
switch (this.args.providerName) {
|
||||
case "youtube":
|
||||
return `https://www.youtube.com/embed/${this.args.videoId}?autoplay=1`;
|
||||
case "vimeo":
|
||||
return `https://player.vimeo.com/video/${this.args.videoId}?autoplay=1`;
|
||||
case "tiktok":
|
||||
return `https://www.tiktok.com/embed/v2/${this.args.videoId}`;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<div
|
||||
class={{concat-class
|
||||
"lazy-video-container"
|
||||
(concat @videoAttributes.providerName "-onebox")
|
||||
(if this.isLoaded "video-loaded")
|
||||
}}
|
||||
data-video-id={{@videoAttributes.id}}
|
||||
data-video-title={{@videoAttributes.title}}
|
||||
data-provider-name={{@videoAttributes.providerName}}
|
||||
>
|
||||
{{#if this.isLoaded}}
|
||||
<LazyIframe
|
||||
@providerName={{@videoAttributes.providerName}}
|
||||
@title={{@videoAttributes.title}}
|
||||
@videoId={{@videoAttributes.id}}
|
||||
/>
|
||||
{{else}}
|
||||
<div
|
||||
class={{concat-class "video-thumbnail" @videoAttributes.providerName}}
|
||||
tabindex="0"
|
||||
{{on "click" this.loadEmbed}}
|
||||
{{on "keypress" this.loadEmbed}}
|
||||
>
|
||||
<img
|
||||
class={{concat @videoAttributes.providerName "-thumbnail"}}
|
||||
src={{@videoAttributes.thumbnail}}
|
||||
title={{@videoAttributes.title}}
|
||||
loading="lazy"
|
||||
/>
|
||||
<div
|
||||
class={{concat-class
|
||||
"icon"
|
||||
(concat @videoAttributes.providerName "-icon")
|
||||
}}
|
||||
></div>
|
||||
</div>
|
||||
<div class="title-container">
|
||||
<div class="title-wrapper">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="title-link"
|
||||
href={{@videoAttributes.url}}
|
||||
title={{@videoAttributes.title}}
|
||||
>
|
||||
{{@videoAttributes.title}}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
|
@ -0,0 +1,23 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { tracked } from "@glimmer/tracking";
|
||||
|
||||
export default class LazyVideo extends Component {
|
||||
@tracked isLoaded = false;
|
||||
|
||||
@action
|
||||
loadEmbed() {
|
||||
if (!this.isLoaded) {
|
||||
this.isLoaded = true;
|
||||
this.args.onLoadedVideo?.();
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
onKeyPress(event) {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
this.loadEmbed();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,46 @@
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import getVideoAttributes from "../lib/lazy-video-attributes";
|
||||
import { hbs } from "ember-cli-htmlbars";
|
||||
|
||||
function initLazyEmbed(api) {
|
||||
api.decorateCookedElement(
|
||||
(cooked, helper) => {
|
||||
if (cooked.classList.contains("d-editor-preview")) {
|
||||
return;
|
||||
}
|
||||
|
||||
const lazyContainers = cooked.querySelectorAll(".lazy-video-container");
|
||||
|
||||
lazyContainers.forEach((container) => {
|
||||
const siteSettings = api.container.lookup("site-settings:main");
|
||||
const videoAttributes = getVideoAttributes(container);
|
||||
|
||||
if (siteSettings[`lazy_${videoAttributes.providerName}_enabled`]) {
|
||||
const onLoadedVideo = () => {
|
||||
const postId = cooked.closest("article")?.dataset?.postId;
|
||||
if (postId) {
|
||||
api.preventCloak(parseInt(postId, 10));
|
||||
}
|
||||
};
|
||||
|
||||
const lazyVideo = helper.renderGlimmer(
|
||||
"p.lazy-video-wrapper",
|
||||
hbs`<LazyVideo @videoAttributes={{@data.param}} @onLoadedVideo={{@data.onLoadedVideo}}/>`,
|
||||
{ param: videoAttributes, onLoadedVideo }
|
||||
);
|
||||
|
||||
container.replaceWith(lazyVideo);
|
||||
}
|
||||
});
|
||||
},
|
||||
{ onlyStream: true, id: "discourse-lazy-videos" }
|
||||
);
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "discourse-lazy-videos",
|
||||
|
||||
initialize() {
|
||||
withPluginApi("1.6.0", initLazyEmbed);
|
||||
},
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
export default function getVideoAttributes(cooked) {
|
||||
if (!cooked.classList.contains("lazy-video-container")) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const url = cooked.querySelector("a")?.getAttribute("href");
|
||||
const thumbnail = cooked.querySelector("img")?.getAttribute("src");
|
||||
const title = cooked.dataset.videoTitle;
|
||||
const providerName = cooked.dataset.providerName;
|
||||
const id = cooked.dataset.videoId;
|
||||
|
||||
return { url, thumbnail, title, providerName, id };
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
.lazy-video-container {
|
||||
z-index: z("base");
|
||||
position: relative;
|
||||
display: block;
|
||||
height: 0;
|
||||
padding: 0 0 56.25% 0;
|
||||
background-color: #000;
|
||||
margin-bottom: 12px;
|
||||
|
||||
.video-thumbnail {
|
||||
cursor: pointer;
|
||||
overflow: hidden;
|
||||
height: 0;
|
||||
padding: 0 0 56.25% 0;
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
.icon {
|
||||
transform: translate(-50%, -50%) scale(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: 5px auto Highlight;
|
||||
outline: 5px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
&:active {
|
||||
outline: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.title-container {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 60px;
|
||||
overflow: hidden;
|
||||
background: linear-gradient(rgba(0, 0, 0, 0.6), rgba(255, 0, 0, 0));
|
||||
|
||||
.title-wrapper {
|
||||
overflow: hidden;
|
||||
padding-inline: 20px;
|
||||
padding-block: 10px;
|
||||
|
||||
.title-link {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
font-family: Arial, sans-serif;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
transition: 150ms;
|
||||
|
||||
// Default play button
|
||||
background: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'><path fill='#181818' d='M512 256c0 141.4-114.6 256-256 256S0 397.4 0 256S114.6 0 256 0S512 114.6 512 256zM188.3 147.1c-7.6 4.2-12.3 12.3-12.3 20.9V344c0 8.7 4.7 16.7 12.3 20.9s16.8 4.1 24.3-.5l144-88c7.1-4.4 11.5-12.1 11.5-20.5s-4.4-16.1-11.5-20.5l-144-88c-7.4-4.5-16.7-4.7-24.3-.5z'/></svg>"
|
||||
);
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
|
||||
&.youtube-icon {
|
||||
width: 68px;
|
||||
height: 48px;
|
||||
background: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 68 48'><path d='M66.52,7.74c-0.78-2.93-2.49-5.41-5.42-6.19C55.79,.13,34,0,34,0S12.21,.13,6.9,1.55 C3.97,2.33,2.27,4.81,1.48,7.74C0.06,13.05,0,24,0,24s0.06,10.95,1.48,16.26c0.78,2.93,2.49,5.41,5.42,6.19 C12.21,47.87,34,48,34,48s21.79-0.13,27.1-1.55c2.93-0.78,4.64-3.26,5.42-6.19C67.94,34.95,68,24,68,24S67.94,13.05,66.52,7.74z' fill='#f00'></path><path d='M 45,24 27,14 27,34' fill='#fff'></path></svg>"
|
||||
);
|
||||
}
|
||||
|
||||
&.vimeo-icon {
|
||||
width: 77px;
|
||||
height: 44px;
|
||||
background: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 37.042 21.167'><g transform='translate(.026013)'><rect x='-.026013' y='8.8818e-16' width='37.042' height='21.167' rx='1.3229' ry='1.3229' fill='#00adef' stroke-width='.33658'/><g transform='matrix(.39688 0 0 .39688 10.557 2.6459)' display='block' fill='none'><path d='m31.666 20c0 0.5928-0.3148 1.141-0.8269 1.4397l-20 11.667c-0.5155 0.3007-1.1524 0.3029-1.6699 0.0056-0.51749-0.2972-0.83656-0.8484-0.83656-1.4452v-23.333c0-0.59677 0.31907-1.148 0.83656-1.4452s1.1544-0.29509 1.6699 0.00561l20 11.667c0.5121 0.2987 0.8269 0.8469 0.8269 1.4396z' fill='#fff'/></g></g></svg>"
|
||||
);
|
||||
}
|
||||
|
||||
&.tiktok-icon {
|
||||
width: 58px;
|
||||
height: 64px;
|
||||
background: svg-uri(
|
||||
"<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 29 32'> <g stroke='none' stroke-width='1' fill='none' fill-rule='evenodd' > <g id='编组-2' transform='translate(0.979236, 0.000000)' fill-rule='nonzero' > <path d='M10.7907645,12.33 L10.7907645,11.11 C10.3672629,11.0428887 9.93950674,11.0061284 9.51076448,10.9999786 C5.35996549,10.9912228 1.68509679,13.6810205 0.438667694,17.6402658 C-0.807761399,21.5995112 0.663505842,25.9093887 4.07076448,28.28 C1.51848484,25.5484816 0.809799545,21.5720834 2.26126817,18.1270053 C3.71273679,14.6819273 7.05329545,12.4115428 10.7907645,12.33 L10.7907645,12.33 Z' id='路径' fill='#25F4EE' ></path> <path d='M11.0207645,26.15 C13.3415287,26.1468776 15.2491662,24.3185414 15.3507645,22 L15.3507645,1.31 L19.1307645,1.31 C19.0536068,0.877682322 19.0167818,0.439130992 19.0207645,0 L13.8507645,0 L13.8507645,20.67 C13.764798,23.0003388 11.8526853,24.846212 9.52076448,24.85 C8.82390914,24.844067 8.13842884,24.6726969 7.52076448,24.35 C8.33268245,25.4749154 9.63346203,26.1438878 11.0207645,26.15 Z' id='路径' fill='#25F4EE' ></path> <path d='M26.1907645,8.33 L26.1907645,7.18 C24.79964,7.18047625 23.4393781,6.76996242 22.2807645,6 C23.2964446,7.18071769 24.6689622,7.99861177 26.1907645,8.33 L26.1907645,8.33 Z' id='路径' fill='#25F4EE' ></path> <path d='M22.2807645,6 C21.1394675,4.70033161 20.5102967,3.02965216 20.5107645,1.3 L19.1307645,1.3 C19.4909812,3.23268519 20.6300383,4.93223067 22.2807645,6 L22.2807645,6 Z' id='路径' fill='#FE2C55' ></path> <path d='M9.51076448,16.17 C7.51921814,16.1802178 5.79021626,17.544593 5.31721201,19.4791803 C4.84420777,21.4137677 5.74860956,23.4220069 7.51076448,24.35 C6.55594834,23.0317718 6.42106871,21.2894336 7.16162883,19.8399613 C7.90218896,18.3904889 9.39306734,17.4787782 11.0207645,17.48 C11.4547752,17.4854084 11.8857908,17.5527546 12.3007645,17.68 L12.3007645,12.42 C11.8769919,12.3565056 11.4492562,12.3230887 11.0207645,12.32 L10.7907645,12.32 L10.7907645,16.32 C10.3736368,16.2081544 9.94244934,16.1576246 9.51076448,16.17 Z' id='路径' fill='#FE2C55' ></path> <path d='M26.1907645,8.33 L26.1907645,12.33 C23.61547,12.3250193 21.107025,11.5098622 19.0207645,10 L19.0207645,20.51 C19.0097352,25.7544158 14.7551919,30.0000116 9.51076448,30 C7.56312784,30.0034556 5.66240321,29.4024912 4.07076448,28.28 C6.72698674,31.1368108 10.8608257,32.0771989 14.4914706,30.6505586 C18.1221155,29.2239183 20.5099375,25.7208825 20.5107645,21.82 L20.5107645,11.34 C22.604024,12.8399663 25.1155724,13.6445013 27.6907645,13.64 L27.6907645,8.49 C27.1865925,8.48839535 26.6839313,8.43477816 26.1907645,8.33 Z' id='路径' fill='#FE2C55' ></path> <path d='M19.0207645,20.51 L19.0207645,10 C21.1134087,11.5011898 23.6253623,12.3058546 26.2007645,12.3 L26.2007645,8.3 C24.6792542,7.97871265 23.3034403,7.17147491 22.2807645,6 C20.6300383,4.93223067 19.4909812,3.23268519 19.1307645,1.3 L15.3507645,1.3 L15.3507645,22 C15.2751521,23.8467664 14.0381991,25.4430201 12.268769,25.9772302 C10.4993389,26.5114403 8.58570942,25.8663815 7.50076448,24.37 C5.73860956,23.4420069 4.83420777,21.4337677 5.30721201,19.4991803 C5.78021626,17.564593 7.50921814,16.2002178 9.50076448,16.19 C9.934903,16.1938693 10.3661386,16.2612499 10.7807645,16.39 L10.7807645,12.39 C7.0223379,12.4536691 3.65653929,14.7319768 2.20094561,18.1976761 C0.745351938,21.6633753 1.47494493,25.6617476 4.06076448,28.39 C5.66809542,29.4755063 7.57158782,30.0378224 9.51076448,30 C14.7551919,30.0000116 19.0097352,25.7544158 19.0207645,20.51 Z' id='路径' fill='#000000' ></path> </g> </g> </svg> "
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TikTok iframe isn't fluid
|
||||
.lazy-video-container.tiktok-onebox {
|
||||
width: 332px;
|
||||
height: 745px;
|
||||
padding: 0;
|
||||
|
||||
.video-thumbnail.tiktok img {
|
||||
height: 745px;
|
||||
}
|
||||
|
||||
iframe {
|
||||
min-width: 332px;
|
||||
height: 742px;
|
||||
background-color: #fff;
|
||||
border-top: 3px solid #fff;
|
||||
border-radius: 9px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
en:
|
||||
site_settings:
|
||||
lazy_videos_enabled: "Enable the Lazy Videos plugin"
|
|
@ -0,0 +1,17 @@
|
|||
plugins:
|
||||
lazy_videos_enabled:
|
||||
default: true
|
||||
client: true
|
||||
hidden: true
|
||||
lazy_youtube_enabled:
|
||||
default: true
|
||||
client: true
|
||||
hidden: false
|
||||
lazy_vimeo_enabled:
|
||||
default: true
|
||||
client: true
|
||||
hidden: false
|
||||
lazy_tiktok_enabled:
|
||||
default: false
|
||||
client: true
|
||||
hidden: false
|
|
@ -0,0 +1,14 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class RebakeLazyYtPosts < ActiveRecord::Migration[7.0]
|
||||
def up
|
||||
execute <<~SQL
|
||||
UPDATE posts SET baked_version = 0
|
||||
WHERE cooked LIKE '%lazyYT-container%'
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
# do nothing
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "onebox"
|
||||
|
||||
class Onebox::Engine::TiktokOnebox
|
||||
include Onebox::Engine
|
||||
alias_method :default_onebox_to_html, :to_html
|
||||
|
||||
def to_html
|
||||
if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_tiktok_enabled &&
|
||||
oembed_data.embed_product_id
|
||||
thumbnail_url = oembed_data.thumbnail_url
|
||||
escaped_title = ERB::Util.html_escape(oembed_data.title)
|
||||
|
||||
<<~HTML
|
||||
<div class="tiktok-onebox lazy-video-container"
|
||||
data-video-id="#{oembed_data.embed_product_id}"
|
||||
data-video-title="#{escaped_title}"
|
||||
data-provider-name="tiktok">
|
||||
<a href="#{url}" target="_blank">
|
||||
<img class="tiktok-thumbnail"
|
||||
src="#{thumbnail_url}"
|
||||
title="#{escaped_title}">
|
||||
</a>
|
||||
</div>
|
||||
HTML
|
||||
else
|
||||
default_onebox_to_html
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "onebox"
|
||||
|
||||
class Onebox::Engine::VimeoOnebox
|
||||
include Onebox::Engine
|
||||
alias_method :default_onebox_to_html, :to_html
|
||||
|
||||
def to_html
|
||||
if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_vimeo_enabled
|
||||
video_id = oembed_data[:video_id]
|
||||
thumbnail_url = "https://vumbnail.com/#{oembed_data[:video_id]}.jpg"
|
||||
escaped_title = ERB::Util.html_escape(og_data.title)
|
||||
|
||||
<<~HTML
|
||||
<div class="vimeo-onebox lazy-video-container"
|
||||
data-video-id="#{video_id}"
|
||||
data-video-title="#{escaped_title}"
|
||||
data-provider-name="vimeo">
|
||||
<a href="https://vimeo.com/#{video_id}" target="_blank">
|
||||
<img class="vimeo-thumbnail"
|
||||
src="#{thumbnail_url}"
|
||||
title="#{escaped_title}">
|
||||
</a>
|
||||
</div>
|
||||
HTML
|
||||
else
|
||||
default_onebox_to_html
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "onebox"
|
||||
|
||||
class Onebox::Engine::YoutubeOnebox
|
||||
include Onebox::Engine
|
||||
alias_method :default_onebox_to_html, :to_html
|
||||
|
||||
def to_html
|
||||
if SiteSetting.lazy_videos_enabled && SiteSetting.lazy_youtube_enabled && video_id &&
|
||||
!params["list"]
|
||||
result = parse_embed_response
|
||||
result ||= get_opengraph.data
|
||||
|
||||
thumbnail_url = "https://img.youtube.com/vi/#{video_id}/maxresdefault.jpg"
|
||||
|
||||
begin
|
||||
Onebox::Helpers.fetch_response(thumbnail_url)
|
||||
rescue StandardError
|
||||
thumbnail_url = result[:image]
|
||||
end
|
||||
|
||||
escaped_title = ERB::Util.html_escape(video_title)
|
||||
|
||||
<<~HTML
|
||||
<div class="youtube-onebox lazy-video-container"
|
||||
data-video-id="#{video_id}"
|
||||
data-video-title="#{escaped_title}"
|
||||
data-provider-name="youtube">
|
||||
<a href="https://www.youtube.com/watch?v=#{video_id}" target="_blank">
|
||||
<img class="youtube-thumbnail"
|
||||
src="#{thumbnail_url}"
|
||||
title="#{escaped_title}">
|
||||
</a>
|
||||
</div>
|
||||
HTML
|
||||
else
|
||||
default_onebox_to_html
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# name: discourse-lazy-videos
|
||||
# about: Lazy loading for embedded videos
|
||||
# version: 0.1
|
||||
# authors: Jan Cernik
|
||||
# url: https://github.com/discourse/discourse-lazy-videos
|
||||
|
||||
hide_plugin if self.respond_to?(:hide_plugin)
|
||||
enabled_site_setting :lazy_videos_enabled
|
||||
|
||||
register_asset "stylesheets/lazy-videos.scss"
|
||||
|
||||
require_relative "lib/lazy-videos/lazy_youtube"
|
||||
require_relative "lib/lazy-videos/lazy_vimeo"
|
||||
require_relative "lib/lazy-videos/lazy_tiktok"
|
||||
|
||||
after_initialize do
|
||||
on(:reduce_cooked) do |fragment|
|
||||
fragment
|
||||
.css(".lazy-video-container")
|
||||
.each do |video|
|
||||
title = video["data-video-title"]
|
||||
href = video.at_css("a")["href"]
|
||||
video.replace("<p><a href=\"#{href}\">#{title}</a></p>")
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
require "pretty_text"
|
||||
|
||||
RSpec.describe PrettyText do
|
||||
let(:post) { Fabricate(:post) }
|
||||
|
||||
it "replaces lazy videos in emails" do
|
||||
cooked_html = <<~HTML
|
||||
<div class="youtube-onebox lazy-video-container" data-video-id="kPRA0W1kECg" data-video-title="15 Sorting Algorithms in 6 Minutes" data-provider-name="youtube">
|
||||
<a href="https://www.youtube.com/watch?v=kPRA0W1kECg" target="_blank" rel="noopener">
|
||||
<img class="youtube-thumbnail" src="thumbnail.jpeg" title="15 Sorting Algorithms in 6 Minutes" width="690" height="388">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="vimeo-onebox lazy-video-container" data-video-id="786646692" data-video-title="Dear Rich" data-provider-name="vimeo">
|
||||
<a href="https://vimeo.com/786646692" target="_blank" rel="noopener">
|
||||
<img class="vimeo-thumbnail" src="thumbnail.jpeg" title="Dear Rich" width="640" height="360">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
HTML
|
||||
|
||||
email_formated = <<~HTML
|
||||
<p><a href="https://www.youtube.com/watch?v=kPRA0W1kECg">15 Sorting Algorithms in 6 Minutes</a></p>
|
||||
<p><a href="https://vimeo.com/786646692">Dear Rich</a></p>
|
||||
HTML
|
||||
|
||||
expect(PrettyText.format_for_email(cooked_html, post)).to match_html(email_formated)
|
||||
end
|
||||
end
|
|
@ -0,0 +1,49 @@
|
|||
import { setupRenderingTest } from "discourse/tests/helpers/component-test";
|
||||
import hbs from "htmlbars-inline-precompile";
|
||||
import { module, test } from "qunit";
|
||||
import { click, render } from "@ember/test-helpers";
|
||||
|
||||
module("Discourse Lazy Videos | Component | lazy-video", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
this.attributes = {
|
||||
url: "https://www.youtube.com/watch?v=kPRA0W1kECg",
|
||||
thumbnail: "thumbnail.jpeg",
|
||||
title: "15 Sorting Algorithms in 6 Minutes",
|
||||
providerName: "youtube",
|
||||
id: "kPRA0W1kECg",
|
||||
};
|
||||
|
||||
test("displays the correct video title", async function (assert) {
|
||||
await render(hbs`<LazyVideo @videoAttributes={{this.attributes}} />`);
|
||||
|
||||
assert.dom(".title-link").hasText(this.attributes.title);
|
||||
});
|
||||
|
||||
test("displays the correct provider icon", async function (assert) {
|
||||
await render(hbs`<LazyVideo @videoAttributes={{this.attributes}} />`);
|
||||
|
||||
assert.dom(".icon.youtube-icon").exists();
|
||||
});
|
||||
|
||||
test("loads the iframe when clicked", async function (assert) {
|
||||
await render(hbs`<LazyVideo @videoAttributes={{this.attributes}}/>`);
|
||||
assert.dom(".lazy-video-container.video-loaded").doesNotExist();
|
||||
|
||||
await click(".video-thumbnail.youtube");
|
||||
assert.dom(".lazy-video-container.video-loaded iframe").exists();
|
||||
});
|
||||
|
||||
test("accepts an optional onLoadedVideo callback function", async function (assert) {
|
||||
this.set("foo", 1);
|
||||
this.set("onLoadedVideo", () => this.set("foo", 2));
|
||||
|
||||
await render(
|
||||
hbs`<LazyVideo @videoAttributes={{this.attributes}} @onLoadedVideo={{this.onLoadedVideo}} />`
|
||||
);
|
||||
assert.strictEqual(this.foo, 1);
|
||||
|
||||
await click(".video-thumbnail.youtube");
|
||||
assert.strictEqual(this.foo, 2);
|
||||
});
|
||||
});
|
|
@ -1,3 +0,0 @@
|
|||
# lazy-yt
|
||||
|
||||
Lazy load YouTube videos plugin for [Discourse](http://discourse.org), highly inspired by the [lazyYT](https://github.com/tylerpearson/lazyYT) jQuery plugin.
|
|
@ -1,33 +0,0 @@
|
|||
import { withPluginApi } from "discourse/lib/plugin-api";
|
||||
import initLazyYt from "../lib/lazyYT";
|
||||
|
||||
export default {
|
||||
name: "apply-lazyYT",
|
||||
initialize() {
|
||||
withPluginApi("0.1", (api) => {
|
||||
initLazyYt($);
|
||||
api.decorateCooked(
|
||||
($elem) => {
|
||||
const iframes = $(".lazyYT", $elem);
|
||||
if (iframes.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
$(".lazyYT", $elem).lazyYT({
|
||||
onPlay(e, $el) {
|
||||
// don't cloak posts that have playing videos in them
|
||||
const postId = parseInt(
|
||||
$el.closest("article").data("post-id"),
|
||||
10
|
||||
);
|
||||
if (postId) {
|
||||
api.preventCloak(postId);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
{ id: "discourse-lazyyt" }
|
||||
);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,179 +0,0 @@
|
|||
/*!
|
||||
* lazyYT (lazy load YouTube videos)
|
||||
* v1.0.1 - 2014-12-30
|
||||
* (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
* http://creativecommons.org/licenses/by-sa/4.0/
|
||||
* Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors
|
||||
*
|
||||
* Usage: <div class="lazyYT" data-youtube-id="laknj093n" data-parameters="rel=0">loading...</div>
|
||||
*
|
||||
* Note: Discourse has forked this from the original, beware when updating the file.
|
||||
*
|
||||
*/
|
||||
|
||||
import escape from "discourse-common/lib/escape";
|
||||
|
||||
export default function initLazyYt($) {
|
||||
"use strict";
|
||||
|
||||
function setUp($el, settings) {
|
||||
let width = $el.data("width"),
|
||||
height = $el.data("height"),
|
||||
ratio = $el.data("ratio") ? $el.data("ratio") : settings.default_ratio,
|
||||
id = $el.data("youtube-id"),
|
||||
title = $el.data("youtube-title"),
|
||||
padding_bottom,
|
||||
innerHtml = [],
|
||||
$thumb,
|
||||
thumb_img,
|
||||
youtube_parameters = $el.data("parameters") || "";
|
||||
|
||||
ratio = ratio.split(":");
|
||||
|
||||
// width and height might override default_ratio value
|
||||
if (typeof width === "number" && typeof height === "number") {
|
||||
$el.width(width);
|
||||
padding_bottom = height + "px";
|
||||
} else if (typeof width === "number") {
|
||||
$el.width(width);
|
||||
padding_bottom = (width * ratio[1]) / ratio[0] + "px";
|
||||
} else {
|
||||
width = $el.width();
|
||||
|
||||
// no width means that container is fluid and will be the size of its parent
|
||||
if (width === 0) {
|
||||
width = $el.parent().width();
|
||||
}
|
||||
|
||||
padding_bottom = (ratio[1] / ratio[0]) * 100 + "%";
|
||||
}
|
||||
|
||||
//
|
||||
// This HTML will be placed inside 'lazyYT' container
|
||||
|
||||
innerHtml.push('<div class="ytp-thumbnail" tabIndex="0">');
|
||||
|
||||
// Play button from YouTube (exactly as it is in YouTube)
|
||||
innerHtml.push('<div class="ytp-large-play-button"');
|
||||
if (width <= 640) {
|
||||
innerHtml.push(' style="transform: scale(0.563888888888889);"');
|
||||
}
|
||||
innerHtml.push(">");
|
||||
innerHtml.push("<svg>");
|
||||
innerHtml.push(
|
||||
'<path fill-rule="evenodd" clip-rule="evenodd" fill="#1F1F1F" class="ytp-large-play-button-svg" d="M84.15,26.4v6.35c0,2.833-0.15,5.967-0.45,9.4c-0.133,1.7-0.267,3.117-0.4,4.25l-0.15,0.95c-0.167,0.767-0.367,1.517-0.6,2.25c-0.667,2.367-1.533,4.083-2.6,5.15c-1.367,1.4-2.967,2.383-4.8,2.95c-0.633,0.2-1.316,0.333-2.05,0.4c-0.767,0.1-1.3,0.167-1.6,0.2c-4.9,0.367-11.283,0.617-19.15,0.75c-2.434,0.034-4.883,0.067-7.35,0.1h-2.95C38.417,59.117,34.5,59.067,30.3,59c-8.433-0.167-14.05-0.383-16.85-0.65c-0.067-0.033-0.667-0.117-1.8-0.25c-0.9-0.133-1.683-0.283-2.35-0.45c-2.066-0.533-3.783-1.5-5.15-2.9c-1.033-1.067-1.9-2.783-2.6-5.15C1.317,48.867,1.133,48.117,1,47.35L0.8,46.4c-0.133-1.133-0.267-2.55-0.4-4.25C0.133,38.717,0,35.583,0,32.75V26.4c0-2.833,0.133-5.95,0.4-9.35l0.4-4.25c0.167-0.966,0.417-2.05,0.75-3.25c0.7-2.333,1.567-4.033,2.6-5.1c1.367-1.434,2.967-2.434,4.8-3c0.633-0.167,1.333-0.3,2.1-0.4c0.4-0.066,0.917-0.133,1.55-0.2c4.9-0.333,11.283-0.567,19.15-0.7C35.65,0.05,39.083,0,42.05,0L45,0.05c2.467,0,4.933,0.034,7.4,0.1c7.833,0.133,14.2,0.367,19.1,0.7c0.3,0.033,0.833,0.1,1.6,0.2c0.733,0.1,1.417,0.233,2.05,0.4c1.833,0.566,3.434,1.566,4.8,3c1.066,1.066,1.933,2.767,2.6,5.1c0.367,1.2,0.617,2.284,0.75,3.25l0.4,4.25C84,20.45,84.15,23.567,84.15,26.4z M33.3,41.4L56,29.6L33.3,17.75V41.4z"></path>'
|
||||
);
|
||||
innerHtml.push(
|
||||
'<polygon fill-rule="evenodd" clip-rule="evenodd" fill="#FFFFFF" points="33.3,41.4 33.3,17.75 56,29.6"></polygon>'
|
||||
);
|
||||
innerHtml.push("</svg>");
|
||||
innerHtml.push("</div>"); // end of .ytp-large-play-button
|
||||
|
||||
innerHtml.push("</div>"); // end of .ytp-thumbnail
|
||||
|
||||
// Video title (info bar)
|
||||
innerHtml.push('<div class="html5-info-bar">');
|
||||
innerHtml.push('<div class="html5-title">');
|
||||
innerHtml.push('<div class="html5-title-text-wrapper">');
|
||||
innerHtml.push(
|
||||
'<a class="html5-title-text" target="_blank" tabindex="3100" href="https://www.youtube.com/watch?v=',
|
||||
escape(id),
|
||||
'">'
|
||||
);
|
||||
if (title === undefined || title === null || title === "") {
|
||||
innerHtml.push("youtube.com/watch?v=" + escape(id));
|
||||
} else {
|
||||
innerHtml.push(escape(title));
|
||||
}
|
||||
innerHtml.push("</a>");
|
||||
innerHtml.push("</div>"); // .html5-title
|
||||
innerHtml.push("</div>"); // .html5-title-text-wrapper
|
||||
innerHtml.push("</div>"); // end of Video title .html5-info-bar
|
||||
|
||||
let prefetchedThumbnail = $el[0].querySelector(".ytp-thumbnail-image");
|
||||
|
||||
$el
|
||||
.css({
|
||||
"padding-bottom": padding_bottom,
|
||||
})
|
||||
.html(innerHtml.join(""));
|
||||
|
||||
if (width > 640) {
|
||||
thumb_img = "maxresdefault.jpg";
|
||||
} else if (width > 480) {
|
||||
thumb_img = "sddefault.jpg";
|
||||
} else if (width > 320) {
|
||||
thumb_img = "hqdefault.jpg";
|
||||
} else if (width > 120) {
|
||||
thumb_img = "mqdefault.jpg";
|
||||
} else if (width === 0) {
|
||||
// sometimes it fails on fluid layout
|
||||
thumb_img = "hqdefault.jpg";
|
||||
} else {
|
||||
thumb_img = "default.jpg";
|
||||
}
|
||||
|
||||
if (prefetchedThumbnail) {
|
||||
$el.find(".ytp-thumbnail").append(prefetchedThumbnail);
|
||||
} else {
|
||||
// Fallback for old posts which were baked before the lazy-yt onebox prefetched a thumbnail
|
||||
$el
|
||||
.find(".ytp-thumbnail")
|
||||
.append(
|
||||
$(
|
||||
[
|
||||
'<img class="ytp-thumbnail-image" src="https://img.youtube.com/vi/',
|
||||
escape(id),
|
||||
"/",
|
||||
thumb_img,
|
||||
'">',
|
||||
].join("")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$thumb = $el
|
||||
.find(".ytp-thumbnail")
|
||||
.addClass("lazyYT-image-loaded")
|
||||
.on("keypress click", function (e) {
|
||||
// Only support Enter for keypress
|
||||
if (e.type === "keypress" && e.keyCode !== 13) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
!$el.hasClass("lazyYT-video-loaded") &&
|
||||
$thumb.hasClass("lazyYT-image-loaded")
|
||||
) {
|
||||
$el
|
||||
.html(
|
||||
'<iframe src="//www.youtube.com/embed/' +
|
||||
escape(id) +
|
||||
"?autoplay=1&" +
|
||||
youtube_parameters +
|
||||
'" frameborder="0" allowfullscreen></iframe>'
|
||||
)
|
||||
.addClass("lazyYT-video-loaded");
|
||||
}
|
||||
|
||||
if (settings.onPlay) {
|
||||
settings.onPlay(e, $el);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$.fn.lazyYT = function (newSettings) {
|
||||
let defaultSettings = {
|
||||
default_ratio: "16:9",
|
||||
callback: null, // TODO: execute callback if given
|
||||
container_class: "lazyYT-container",
|
||||
};
|
||||
let settings = Object.assign(defaultSettings, newSettings);
|
||||
|
||||
return this.each(function () {
|
||||
let $el = $(this).addClass(settings.container_class);
|
||||
setUp($el, settings);
|
||||
});
|
||||
};
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
/*!
|
||||
* lazyYT (lazy load YouTube videos)
|
||||
* v1.0.1 - 2014-12-30
|
||||
* (CC) This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
|
||||
* http://creativecommons.org/licenses/by-sa/4.0/
|
||||
* Contributors: https://github.com/tylerpearson/lazyYT/graphs/contributors || https://github.com/daugilas/lazyYT/graphs/contributors
|
||||
*/
|
||||
|
||||
.lazyYT-container {
|
||||
position: relative;
|
||||
z-index: z("base");
|
||||
display: block;
|
||||
height: 0;
|
||||
padding: 0 0 56.25% 0;
|
||||
overflow: hidden;
|
||||
background-color: #000000;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.lazyYT-container iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Video Title (YouTube style)
|
||||
*/
|
||||
|
||||
.lazyYT-container .html5-info-bar {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
z-index: 935;
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
overflow: hidden;
|
||||
font-family: Arial, sans-serif;
|
||||
font-size: 12px;
|
||||
color: #fff;
|
||||
background-color: rgba(0, 0, 0, 0.8);
|
||||
-webkit-transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
|
||||
-moz-transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
|
||||
transition: opacity 0.25s cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.lazyYT-container .html5-title {
|
||||
padding-right: 6px;
|
||||
padding-left: 12px;
|
||||
}
|
||||
|
||||
.lazyYT-container .html5-title-text-wrapper {
|
||||
overflow: hidden;
|
||||
-o-text-overflow: ellipsis;
|
||||
text-overflow: ellipsis;
|
||||
word-wrap: normal;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.lazyYT-container .html5-title-text {
|
||||
width: 100%;
|
||||
font-size: 13px;
|
||||
line-height: 30px;
|
||||
color: #ccc;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.lazyYT-container .html5-title-text:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*
|
||||
* Thumbnail
|
||||
*/
|
||||
|
||||
.ytp-thumbnail {
|
||||
padding-bottom: inherit;
|
||||
cursor: pointer;
|
||||
background-position: 50% 50%;
|
||||
background-repeat: no-repeat;
|
||||
-webkit-background-size: cover;
|
||||
-moz-background-size: cover;
|
||||
-o-background-size: cover;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
/*
|
||||
* Play button (YouTube style)
|
||||
*/
|
||||
|
||||
.ytp-large-play-button {
|
||||
position: absolute;
|
||||
top: 50% !important;
|
||||
left: 50% !important;
|
||||
width: 86px !important;
|
||||
height: 60px !important;
|
||||
padding: 0 !important;
|
||||
margin: -29px 0 0 -42px !important;
|
||||
font-size: normal !important;
|
||||
font-weight: normal !important;
|
||||
line-height: 1 !important;
|
||||
opacity: 0.9;
|
||||
z-index: 935;
|
||||
}
|
||||
|
||||
.ytp-large-play-button-svg {
|
||||
opacity: 0.9;
|
||||
fill: #1f1f1f;
|
||||
}
|
||||
|
||||
.lazyYT-image-loaded:hover .ytp-large-play-button-svg,
|
||||
.lazyYT-image-loaded:focus .ytp-large-play-button-svg,
|
||||
.ytp-large-play-button:focus .ytp-large-play-button-svg {
|
||||
opacity: 1;
|
||||
fill: #cc181e;
|
||||
}
|
||||
|
||||
.ytp-thumbnail-image {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
.lazyYT {
|
||||
max-width: 100%;
|
||||
}
|
|
@ -1,3 +0,0 @@
|
|||
en:
|
||||
site_settings:
|
||||
lazy_yt_enabled: "Enable the LazyYT plugin"
|
|
@ -1,5 +0,0 @@
|
|||
plugins:
|
||||
lazy_yt_enabled:
|
||||
default: true
|
||||
client: false
|
||||
hidden: true
|
|
@ -1,74 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# name: lazy-yt
|
||||
# about: Uses the lazyYT plugin to lazy load Youtube videos
|
||||
# version: 1.0.1
|
||||
# authors: Arpit Jalan
|
||||
# url: https://github.com/discourse/discourse/tree/main/plugins/lazy-yt
|
||||
|
||||
hide_plugin if self.respond_to?(:hide_plugin)
|
||||
enabled_site_setting :lazy_yt_enabled
|
||||
|
||||
require "onebox"
|
||||
|
||||
# stylesheet
|
||||
register_asset "stylesheets/lazyYT.css"
|
||||
register_asset "stylesheets/lazyYT_mobile.scss", :mobile
|
||||
|
||||
# freedom patch YouTube Onebox
|
||||
class Onebox::Engine::YoutubeOnebox
|
||||
include Onebox::Engine
|
||||
alias_method :yt_onebox_to_html, :to_html
|
||||
|
||||
def to_html
|
||||
if SiteSetting.lazy_yt_enabled && video_id && !params["list"]
|
||||
size_restricted = [params["width"], params["height"]].any?
|
||||
video_width = (params["width"] && params["width"].to_i <= 695) ? params["width"] : 690 # embed width
|
||||
video_height = (params["height"] && params["height"].to_i <= 500) ? params["height"] : 388 # embed height
|
||||
size_tags = ["width=\"#{video_width}\"", "height=\"#{video_height}\""]
|
||||
|
||||
result = parse_embed_response
|
||||
result ||= get_opengraph.data
|
||||
|
||||
thumbnail_url = result[:image] || "https://img.youtube.com/vi/#{video_id}/hqdefault.jpg"
|
||||
|
||||
# Put in the LazyYT div instead of the iframe
|
||||
escaped_title = ERB::Util.html_escape(video_title)
|
||||
|
||||
<<~HTML
|
||||
<div class="onebox lazyYT lazyYT-container"
|
||||
data-youtube-id="#{video_id}"
|
||||
data-youtube-title="#{escaped_title}"
|
||||
#{size_restricted ? size_tags.map { |t| "data-#{t}" }.join(" ") : ""}
|
||||
data-parameters="#{embed_params}">
|
||||
<a href="https://www.youtube.com/watch?v=#{video_id}" target="_blank">
|
||||
<img class="ytp-thumbnail-image"
|
||||
src="#{thumbnail_url}"
|
||||
#{size_restricted ? size_tags.join(" ") : ""}
|
||||
title="#{escaped_title}">
|
||||
</a>
|
||||
</div>
|
||||
HTML
|
||||
else
|
||||
yt_onebox_to_html
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
after_initialize do
|
||||
on(:reduce_cooked) do |fragment|
|
||||
fragment
|
||||
.css(".lazyYT")
|
||||
.each do |yt|
|
||||
begin
|
||||
youtube_id = yt["data-youtube-id"]
|
||||
parameters = yt["data-parameters"]
|
||||
uri = URI("https://www.youtube.com/embed/#{youtube_id}?autoplay=1&#{parameters}")
|
||||
yt.replace %{<p><a href="#{uri.to_s}">https://#{uri.host}#{uri.path}</a></p>}
|
||||
rescue URI::InvalidURIError
|
||||
# remove any invalid/weird URIs
|
||||
yt.remove
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1019,12 +1019,30 @@ RSpec.describe PrettyText do
|
|||
expect(extract_urls(html)).to eq(["https://example.com"])
|
||||
end
|
||||
|
||||
it "should lazyYT videos" do
|
||||
expect(
|
||||
extract_urls(
|
||||
"<div class=\"lazyYT\" data-youtube-id=\"yXEuEUQIP3Q\" data-youtube-title=\"Mister Rogers defending PBS to the US Senate\" data-width=\"480\" data-height=\"270\" data-parameters=\"feature=oembed&wmode=opaque\"></div>",
|
||||
),
|
||||
).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"])
|
||||
context "when lazy-videos" do
|
||||
it "should extract youtube url" do
|
||||
expect(
|
||||
extract_urls(
|
||||
"<div class=\"lazy-video-container\" data-video-id=\"yXEuEUQIP3Q\" data-video-title=\"Mister Rogers defending PBS to the US Senate\" data-provider-name=\"youtube\"></div>",
|
||||
),
|
||||
).to eq(["https://www.youtube.com/watch?v=yXEuEUQIP3Q"])
|
||||
end
|
||||
|
||||
it "should extract vimeo url" do
|
||||
expect(
|
||||
extract_urls(
|
||||
"<div class=\"lazy-video-container\" data-video-id=\"786646692\" data-video-title=\"Dear Rich\" data-provider-name=\"vimeo\"></div>",
|
||||
),
|
||||
).to eq(["https://vimeo.com/786646692"])
|
||||
end
|
||||
|
||||
it "should extract tiktok url" do
|
||||
expect(
|
||||
extract_urls(
|
||||
"<div class=\"lazy-video-container\" data-video-id=\"6718335390845095173\" data-video-title=\"Scramble up ur name &amp; I’ll try to guess it😍❤️ #foryoupage #petsoftiktok...\" data-provider-name=\"tiktok\"></div>",
|
||||
),
|
||||
).to eq(["https://m.tiktok.com/v/6718335390845095173"])
|
||||
end
|
||||
end
|
||||
|
||||
it "should extract links to posts" do
|
||||
|
|
|
@ -28,7 +28,7 @@ RSpec.describe SearchIndexer do
|
|||
|
||||
it "extract youtube title" do
|
||||
html =
|
||||
"<div class=\"lazyYT\" data-youtube-id=\"lmFgeFh2nlw\" data-youtube-title=\"Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]\" data-width=\"480\" data-height=\"270\" data-parameters=\"feature=oembed&wmode=opaque\"></div>"
|
||||
"<div class=\"lazy-video-container\" data-video-id=\"lmFgeFh2nlw\" data-video-title=\"Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]\" data-provider-name=\"youtube\"></div>"
|
||||
scrubbed = SearchIndexer::HtmlScrubber.scrub(html)
|
||||
expect(scrubbed).to eq(
|
||||
"Metallica Mixer Explains Missing Bass on 'And Justice for All' [Exclusive]",
|
||||
|
|
Loading…
Reference in New Issue