Skip to content

Instantly share code, notes, and snippets.

@jmarsh24
Created March 8, 2022 22:42
Show Gist options
  • Save jmarsh24/5a53efbb41c6f41026eeab122efedf91 to your computer and use it in GitHub Desktop.
Save jmarsh24/5a53efbb41c6f41026eeab122efedf91 to your computer and use it in GitHub Desktop.
import { Controller } from "@hotwired/stimulus";
export default class extends Controller {
static targets = [
"button",
"startTime",
"endTime",
"playbackSpeed",
"source",
];
static values = {
source: String,
originalText: String,
successDurationValue: Number,
startTime: Number,
endTime: Number,
url: String,
rootUrl: String,
playbackSpeed: Number,
};
initialize() {
this.successDurationValue = 2000;
}
connect() {
this.originalText = this.buttonTarget.innerHTML;
this.playbackSpeedValue = this.playbackSpeedTarget.value;
this.startTimeValue = this.parseTime(this.startTimeTarget.value);
this.endTimeValue = this.parseTime(this.endTimeTarget.value);
this.urlValueUpdate();
}
copy() {
navigator.clipboard.writeText(this.urlValue);
this.copied();
}
copied() {
this.buttonTarget.innerText = this.data.get("successContent");
setTimeout(() => {
this.buttonTarget.innerHTML = this.originalText;
}, this.successDurationValue);
}
parseTime(time) {
var timeArray = time.toString().split(":");
var timeInSeconds = 0;
if (timeArray.length == 2) {
timeInSeconds = +timeArray[0] * 60 + +timeArray[1];
}
if (timeArray.length == 1) {
timeInSeconds = timeArray[0];
}
return parseInt(timeInSeconds);
}
urlValueUpdate() {
this.urlValue = `${this.data.get("rootUrl")}watch?v=${this.data.get(
"videoId"
)}`;
if (
(this.startTimeValue > 0) &
(this.endTimeValue > 0) &
(this.playbackSpeedValue != 1) &
(this.startTimeValue < this.endTimeValue)
) {
this.urlValue = `${this.urlValue}&start=${this.startTimeValue}&end=${this.endTimeValue}&speed=${this.playbackSpeedValue}`;
} else if (
(this.startTimeValue > 0) &
(this.endTimeValue > 0) &
(this.playbackSpeedValue == 1) &
(this.startTimeValue < this.endTimeValue)
) {
this.urlValue = `${this.urlValue}&start=${this.startTimeValue}&end=${this.endTimeValue}`;
} else if (
(this.startTimeValue > 0) &
(this.endTimeValue == 0) &
(this.playbackSpeedValue != 1)
) {
this.urlValue = `${this.urlValue}&start=${this.startTimeValue}&speed=${this.playbackSpeedValue}`;
} else if (
(this.startTimeValue == 0) &
(this.endTimeValue == 0) &
(this.playbackSpeedValue != 1)
) {
this.urlValue = `${this.urlValue}&speed=${this.playbackSpeedValue}`;
} else if (this.startTimeValue > 0) {
this.urlValue = `${this.urlValue}&start=${this.startTimeValue}`;
}
this.sourceTarget.value = this.urlValue;
}
changeValue() {
this.playbackSpeedValue = this.playbackSpeedTarget.value;
this.startTimeValue = this.parseTime(this.startTimeTarget.value);
this.endTimeValue = this.parseTime(this.endTimeTarget.value);
this.urlValueUpdate();
this.sourceTarget.value = this.urlValue;
history.pushState(
{},
"",
`watch?v=${this.data.get("videoId")}&start=${this.startTimeValue}&end=${
this.endTimeValue
}&speed=${this.playbackSpeedValue}`
);
}
}
<% content_for :meta_title, "#{primary_title(@video.display.dancer_names,
@video.title,
@video.display.any_song_attributes,
@video.youtube_id)}" %>
<% content_for :meta_description, "#{@video.display.any_song_attributes}" %>
<% content_for :meta_image, "https://img.youtube.com/vi/#{@video.youtube_id}/hqdefault.jpg" %>
<%= render 'shared/header' %>
<div data-controller="hotkeys" data-hotkeys-bindings-value='{
"space": "#hotkey->youtube#playPause",
"shift + 1": "#hotkey->youtube#setTime1",
"shift + 2": "#hotkey->youtube#setTime2",
"backspace": "#hotkey->youtube#reset",
"m": "#hotkey->youtube#toggleMute",
"+": "#hotkey->youtube#increaseVolume",
"-": "#hotkey->youtube#decreaseVolume",
"f": "#hotkey->youtube#playFullscreen ",
"left": "#hotkey->youtube#seekBackward",
"right": "#hotkey->youtube#seekForward",
"shift + . ": "#hotkey->youtube#increasePlaybackRate",
"shift + , ": "#hotkey->youtube#decreasePlaybackRate"}'></div>
<div id="hotkey" data-controller="youtube"
data-youtube-video-id-value="<%= @video.youtube_id %>"
data-youtube-start-seconds-value="<%= @start_value %>"
data-youtube-end-seconds-value="<%= @end_value %>"
data-youtube-playback-speed-value="<%= @playback_speed %>">
<div class="video-responsive-background">
<div class="video-responsive-container">
<div class="video-responsive"
>
<div data-youtube-target="frame"></div>
</div>
</div>
</div>
<%= turbo_frame_tag dom_id(@video) do %>
<div class="video-info-container">
<%= render partial: "videos/show/video_info_primary" %>
<div class="video-info-container-secondary">
<div class="video-info-details-main">
<%= render partial: "videos/show/video_info_details" %>
<div class="container">
<h3>Comments</h3>
<div id="comments">
<% if user_signed_in? %>
<%= render partial: "comments/form", locals: { commentable: @video } %>
<% if params[:comment] %>
<p>Single comment thread. <%= link_to "View all comments", url_for() %></p>
<% end %>
<% else %>
<div style="margin-bottom: 16px; font-size: 12px;">
<%= link_to "Sign up", new_user_registration_path, style: "font-size: 0.8rem", 'data-turbo-frame': "_top" %> or <%= link_to "Login", new_user_session_path, style: "font-size: 12px;", 'data-turbo-frame': "_top" %> to reply
</div>
<% end %>
<%= render @comments, continue_thread: 5 %>
<% if @yt_comments.present? %>
<div id="youtube-comments">
<h3>Youtube Comments</h3>
<% @yt_comments.each do |comment| %>
<%= render partial: "yt_comments/comment", locals: { comment: comment } %>
<% end %>
</div>
<% end %>
</div>
</div>
</div>
<% if @video.song.present? %>
<%= render partial: "videos/show/lyrics" if @video.song.lyrics.present? %>
<% end %>
<div class="recommended-videos-section">
<% unless @videos_from_this_performance.empty? %>
<div class="recommended-videos-card" data-toggle-target="recommendedPerformanceVideos">
<div class="recommended-videos__header">
<h2>Videos from this Performance</h2>
</div>
<%= render partial: "videos/show/recommended_videos", locals: { videos: @videos_from_this_performance } %>
</div>
<% if @videos_from_this_performance.size > 3 %>
<div class="show-more-container">
<%= button_tag type: "button",
class: "button",
style: "width: 100%;",
data: { action: "toggle#toggleRecommendedPerformanceVideos" } do %>
<%= fa_icon "angle-down", data: { 'toggle-target': "recommendedPerformanceVideosDownButton" } %>
<%= fa_icon "angle-up", class: "isHidden", data: { 'toggle-target': "recommendedPerformanceVideosUpButton" } %>
<% end %>
</div>
<% end %>
<% end %>
<% unless @videos_with_same_event.empty? %>
<div class="recommended-videos-card" data-toggle-target="recommendedEventVideos">
<div class="recommended-videos__header">
<h2>Videos from <%= @video.event.title %></h2>
</div>
<%= render partial: "videos/show/recommended_videos", locals: { videos: @videos_with_same_event } %>
</div>
<% if @videos_with_same_event.size > 3 %>
<div class="show-more-container">
<%= button_tag type: "button",
class: "button",
style: "width: 100%;",
data: { action: "toggle#toggleRecommendedEventVideos" } do %>
<%= fa_icon "angle-down", data: { 'toggle-target': "recommendedEventVideosDownButton" } %>
<%= fa_icon "angle-up", class: "isHidden", data: { 'toggle-target': "recommendedEventVideosUpButton" } %>
<% end %>
</div>
<% end %>
<% end %>
<% unless @videos_with_same_song.empty? %>
<div class="recommended-videos-card" data-toggle-target="recommendedSongVideos">
<div class="recommended-videos__header">
<h2>Videos with <br>
<i>"<%= @video.song.title.titleize %>" &nbsp;</i><%= "by #{@video.song.artist.titleize}" %></h2>
</div>
<%= render partial: "videos/show/recommended_videos", locals: { videos: @videos_with_same_song } %>
</div>
<% if @videos_with_same_song.size > 3 %>
<div class="show-more-container">
<%= button_tag type: "button",
class: "button",
style: "width: 100%;",
data: { action: "toggle#toggleRecommendedSongVideos" } do %>
<%= fa_icon "angle-down", data: { 'toggle-target': "recommendedSongVideosDownButton" } %>
<%= fa_icon "angle-up", class: "isHidden", data: { 'toggle-target': "recommendedSongVideosUpButton" } %>
<% end %>
</div>
<% end %>
<% end %>
<% unless @videos_with_same_channel.empty? %>
<div class="recommended-videos-card" data-toggle-target="recommendedChannelVideos">
<div class="recommended-videos__header">
<h2>Videos from <%= @video.channel.title %></h2>
</div>
<%= render partial: "videos/show/recommended_videos", locals: { videos: @videos_with_same_channel } %>
</div>
<% if @videos_with_same_channel.size > 3 %>
<div class="show-more-container">
<%= button_tag type: "button",
class: "button",
style: "width: 100%;",
data: { action: "toggle#toggleRecommendedChannelVideos" } do %>
<%= fa_icon "angle-down", data: { 'toggle-target': "recommendedChannelVideosDownButton" } %>
<%= fa_icon "angle-up", class: "isHidden", data: { 'toggle-target': "recommendedChannelVideosUpButton" } %>
<% end %>
</div>
<% end %>
<% end %>
</div>
</div>
</div>
<% end %>
</div>
<div class="under-video-container">
<div class="video-info-primary-container">
<div class="show-video-title">
<h1>
<%= primary_title(@video.display.dancer_names,
@video.title,
@video.display.any_song_attributes,
@video.youtube_id) %>
</h1>
</div>
<div class="show-video-song">
<h2>
<%= link_to_song( @video.display.el_recodo_attributes,
@video.display.external_song_attributes,
@video) %>
</h2>
</div>
<div class="show-video-event">
<%= link_to @video.event.title.titleize,
root_path(event_id: @video.event.id),
{ "data-turbo-frame": "_top" } if @video.event.present? %>
</div>
<div class="show-video-channel">
<%= link_to image_tag(@video.channel.thumbnail_url,
class: "channel-icon"),
root_path(channel: @video.channel.channel_id), { "data-turbo-frame": "_top" } if @video.channel.thumbnail_url.present? %>
<%= link_to truncate(@video.channel.title,
length: 45, omission: ""),
root_path(channel: @video.channel.channel_id),
{ class: "channel-title", "data-turbo-frame": "_top" } %>
</div>
<div class="show-video-metadata">
<%= formatted_metadata(@video) %>
</div>
</div>
<div class="share-container"
data-controller="clipboard"
data-clipboard-success-content="Copied!"
data-clipboard-video-id="<%= @video.youtube_id %>"
data-clipboard-root-url="<%= @root_url %>"
>
<div class="youtube-controls">
<%= render "videos/show/vote" %>
<div>
<%= text_field_tag 'start_time_value', @start_value.present? ? Time.at(@start_value.to_i).utc.strftime("%_M:%S") : "", placeholder: '0:00', class: "input", style: "width: 60px;", data: { action: "keyup->clipboard#changeValue input->youtube#updateStartTime",
"clipboard-target": "startTime",
"youtube-target": "startTime" } %>
</div>
<span class="spacer">-</span>
<div>
<%= text_field_tag 'end_time_value', @end_value.present? ? Time.at(@end_value.to_i).utc.strftime("%_M:%S") : "", placeholder: '0:00', class: "input", style: "width: 60px;", data: { action: "keyup->clipboard#changeValue input->youtube#updateEndTime",
"clipboard-target": "endTime",
"youtube-target": "endTime" } %>
</div>
<span class="spacer">:</span>
<%= select_tag :playback_speed, options_for_select( { ".25x" => "0.25",
".5x" => "0.50",
".75x" => "0.75",
"1x" => "1.00",
"1.25x" => "1.25",
"1.5x" => "1.50",
"1.75x" => "1.75",
"2x" => "2.00" }, sprintf('%.2f', @playback_speed.to_i)),
data: { action: "change->clipboard#changeValue change->youtube#updatePlaybackSpeed",
"clipboard-target": "playbackSpeed",
"youtube-target": "playbackSpeed" } %>
</div>
<div class="copy-to-clipboard">
<%= text_field_tag :url, "tangotube.tv/watch?v#{@video.youtube_id}",
readonly: "readonly",
class: "copy-to-clipboard__field",
data: { "clipboard-target": "source" } %>
<%= button_tag type: "button",
name: "button",
class: "copy-to-clipboard__button button",
data: { "clipboard-target": "button",
action: "click->clipboard#copy"} do %>
<%= fa_icon 'share', text: "Share" %>
<% end %>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus";
import YouTubePlayer from "youtube-player";
export default class extends Controller {
static values = {
videoId: String,
startSeconds: Number,
endSeconds: Number,
playbackSpeed: Number,
};
static targets = ["frame", "playbackSpeed", "startTime", "endTime"];
connect() {
var playerConfig = {
videoId: this.videoIdValue,
playerVars: {
autoplay: 0, // Auto-play the video on load
controls: 1, // Show pause/play buttons in player
modestbranding: 1, // Hide the Youtube Logo
fs: 1, // Hide the full screen button
cc_load_policy: 0, // Hide closed captions
iv_load_policy: 3, // Hide the Video Annotations
start: this.startSecondsValue,
end: this.endSecondsValue,
},
};
const player = YouTubePlayer(this.frameTarget, playerConfig);
player.on("ready", (e) => {
this.element.setAttribute("data-duration", e.target.getDuration());
this.youtube = e.target;
this.element.setAttribute("data-time", this.time);
this.element.setAttribute("data-state", -1);
});
player.setPlaybackRate(this.playbackSpeedValue);
player.on("playbackRateChange", (e) => {
this.playbackSpeedTarget.value = parseFloat(this.player.getPlaybackRate())
.toFixed(2)
.toString();
});
player.on("stateChange", (e) => {
this.element.setAttribute("data-state", e.data);
this.element.setAttribute("data-time", this.time);
this.element.setAttribute("data-playbackRate", this.playbackRateValue);
this.element.setAttribute("data-volume", this.volume);
e.data === 1 ? this.startTimer() : clearInterval(this.timer);
if (e.data === YT.PlayerState.ENDED) {
this.player.seekTo(this.startSecondsValue);
}
});
}
updatePlaybackSpeed() {
this.player.setPlaybackRate(parseFloat(this.playbackSpeedTarget.value));
}
updateStartTime() {
var startTimeArray = this.startTimeTarget.value.split(":");
if (startTimeArray.length == 2) {
var startTime = +startTimeArray[0] * 60 + +startTimeArray[1];
}
if (startTimeArray.length == 1) {
var startTime = startTimeArray[0];
}
this.startSecondsValue = startTime;
this.player.loadVideoById({
videoId: this.videoIdValue,
startSeconds: this.startSecondsValue,
endSeconds: this.endSecondsValue,
});
}
updateEndTime() {
var endTimeArray = this.endTimeTarget.value.split(":");
if (endTimeArray.length == 2) {
var endTime = +endTimeArray[0] * 60 + +endTimeArray[1];
}
if (endTimeArray.length == 1) {
var endTime = endTimeArray[0];
}
this.endSecondsValue = endTime;
this.player.loadVideoById({
videoId: this.videoIdValue,
startSeconds: this.startSecondsValue,
endSeconds: this.endSecondsValue,
});
}
disconnect() {
document.removeEventListener("turbo:before-cache", this.player.destroy);
}
startTimer() {
this.timer = setInterval(() => {
this.element.setAttribute("data-time", this.time);
this.element.dispatchEvent(
new CustomEvent("youtube", {
bubbles: false,
cancelable: false,
detail: { time: this.time },
})
);
}, 1000);
}
playPause(event) {
event.preventDefault();
var playerState = this.element.getAttribute("data-state");
if (playerState == 5 || playerState == 2 || playerState == -1) {
this.play();
} else {
this.pause();
}
}
setTime1(event) {
event.preventDefault();
var currentTime = this.element.getAttribute("data-time");
this.startSecondsValue = currentTime;
this.startTimeTarget.value = currentTime;
this.player.loadVideoById({
videoId: this.videoIdValue,
startSeconds: this.startSecondsValue,
endSeconds: this.endSecondsValue,
});
}
setTime2(event) {
event.preventDefault();
var currentTime = this.element.getAttribute("data-time");
this.endSecondsValue = currentTime;
this.endTimeTarget.value = currentTime;
this.player.loadVideoById({
videoId: this.videoIdValue,
startSeconds: this.startSecondsValue,
endSeconds: this.endSecondsValue,
});
}
toggleMute(event) {
event.preventDefault();
if (this.player.isMuted()) {
this.unMute();
} else {
this.mute();
}
}
reset(event) {
event.preventDefault();
var currentTime = this.time;
this.player.loadVideoById({
videoId: this.videoIdValue,
startSeconds: currentTime,
});
this.endSecondsValue = "";
this.endTimeTarget.value = "";
this.startSecondsValue = "";
this.startTimeTarget.value = "";
this.player.setPlaybackRate(1);
this.playbackSpeedTarget == 1;
}
playFullscreen() {
this.play(); //won't work on mobile
var iframe = this.frameTarget;
var requestFullScreen =
iframe.requestFullScreen ||
iframe.mozRequestFullScreen ||
iframe.webkitRequestFullScreen;
if (requestFullScreen) {
requestFullScreen.bind(iframe)();
}
}
increasePlaybackRate(event) {
event.preventDefault();
this.player.setPlaybackRate(this.playbackRate + 0.25);
}
decreasePlaybackRate(event) {
event.preventDefault();
this.player.setPlaybackRate(this.playbackRate - 0.25);
}
seekForward(event) {
event.preventDefault();
this.seek(this.time + 5);
}
seekBackward(event) {
event.preventDefault();
this.seek(this.time - 5);
}
play = () => this.player.playVideo();
pause = () => this.player.pauseVideo();
stop = () => this.player.stopVideo();
mute = () => this.player.mute();
unMute = () => this.player.unMute();
seek = (seconds) => this.player.seekTo(seconds);
// playbackRate = (rate) => this.player.setPlaybackRate(rate);
get player() {
return this.youtube;
}
get time() {
return Math.round(this.player.getCurrentTime());
}
get playbackRate() {
return this.player.getPlaybackRate();
}
get duration() {
return this.player.getDuration();
}
get state() {
return this.element.getAttribute("data-state");
}
get loaded() {
return this.player.getVideoLoadedFraction();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment