Initial commit

This commit is contained in:
Senad Uka
2020-05-31 22:38:19 +02:00
commit 858fafc3c5
1280 changed files with 65918 additions and 0 deletions

View File

@@ -0,0 +1,67 @@
function beforeUnloadHandler(e){
e.preventDefault();
e.returnValue = '';
}
function turbolinksClickHandler(){
return false;
}
let eventListenersAdded = false;
function addEventListenersForUploadProgresBar(){
if (eventListenersAdded) return;
eventListenersAdded = true;
addEventListener("direct-upload:initialize", event => {
const { detail } = event;
const { id, file } = detail;
const progressBar = App.FileUploadProgress.createProgressBar(id);
$('#upload-progress-container').append(progressBar);
});
addEventListener("direct-upload:start", event => {
const { id } = event.detail;
App.FileUploadProgress.showProgressBar(id);
})
addEventListener("direct-upload:progress", event => {
const { id, progress } = event.detail;
if (App.FileUploadProgress._findProgressBar(id)) {
App.FileUploadProgress.updateProgressBar(id, progress);
}
})
addEventListener("direct-upload:error", event => {
event.preventDefault();
const { id, error } = event.detail;
App.FileUploadProgress.showError(id, error);
})
addEventListener("direct-upload:end", event => {
const { id } = event.detail;
App.FileUploadProgress.removeProgressBar(id)
})
}
$(document).on("turbolinks:load", function() {
addEventListenersForUploadProgresBar();
window.removeEventListener('beforeunload', beforeUnloadHandler, true);
$(document).off('turbolinks:click', turbolinksClickHandler);
$("#import_appearance_releases").click(function(e) {
$('#appearance_release_file_upload').click();
});
$('#appearance_release_file_upload').change(function(e) {
$('#import_appearance_releases')
.html('Importing...')
.attr('disabled', 'disabled');
window.addEventListener('beforeunload', beforeUnloadHandler, true);
$(document).on('turbolinks:click', turbolinksClickHandler);
const fileUploadForm = document.getElementById('appearance_releases_import');
Rails.fire(fileUploadForm, 'submit');
});
});

View File

@@ -0,0 +1,21 @@
//= require rails-ujs
//= require jquery3
//= require activestorage
//= require turbolinks
//= require turbolinks-csp
//= require popper
//= require bootstrap-sprockets
//= require bootstrap-datepicker
//= require bs-custom-file-input/dist/bs-custom-file-input
//= require clappr/dist/clappr
//= require clappr-markers-plugin/dist/clappr-markers-plugin
//= require clappr-playback-rate-plugin/dist/clappr-playback-rate-plugin
//= require spark-md5/spark-md5
//= require js-sha256/src/sha256
//= require evaporate/evaporate
//= require exif-js
//= require signature_pad/dist/signature_pad
//= require signature_pad-extensions
//= require smpte-timecode/smpte-timecode
//= require trix/dist/trix
//= require_tree .

View File

@@ -0,0 +1,13 @@
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.
//
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);

View File

View File

@@ -0,0 +1,45 @@
$(document).on "turbolinks:load", ->
# Only connect if a broadcast-token meta tag is present
broadcast_meta = document.querySelector("meta[name=broadcast-token]")
return unless broadcast_meta
broadcastToken = broadcast_meta.getAttribute("content")
App.public = App.cable.subscriptions.create { channel: "BroadcastsChannel", token: broadcastToken },
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
return unless document.querySelector("meta[name=broadcast-token][content='#{broadcastToken}']")
switch data.event
when "broadcast_stream_update" then @refreshBroadcastVideo(data)
when "stream_recording_ready" then @showBroadcastRecordings(data)
when "file_upload_update" then @refreshFilesTab(data)
refreshBroadcastVideo: (data) ->
$("#broadcast_updates").html data.status_content
if data.streamer_status == 'recording' && data.status == 'active'
$("#broadcast_video").html data.video_content
new (Clappr.Player)(
parentId: '#broadcast_video'
source: data.playback_url
width: '100%',
height: '100%',
mute: true,
autoPlay: true,
hlsMinimumDvrSize: 1)
if data.streamer_status == "idle" && data.status == "idle"
$("#broadcast_video").html data.video_content
showBroadcastRecordings: (data) ->
$(".flash-message").html data.flash_content
$("#broadcast_recordings").html data.recordings_content
$("#broadcast_recordings_nav").html data.recordings_nav_content
refreshFilesTab: (data) ->
$("#broadcast_file_list").html data.files_content
$("#broadcast_files_pagination").html data.pagination_content

View File

@@ -0,0 +1,25 @@
# TODO: Need to unsubscribe as project changes
$(document).on "turbolinks:load", ->
# Only connect if a project-id meta tag is present
return unless meta = document.querySelector("meta[name=project-id]")
# Get the id of the project to subscribe to
projectId = meta.getAttribute("content")
App.projects = App.cable.subscriptions.create { channel: "ProjectsChannel", id: projectId },
connected: ->
# Called when the subscription is ready for use on the server
disconnected: ->
# Called when the subscription has been terminated by the server
received: (data) ->
switch data.event
when "video_status_update" then @showVideoStatusUpdate(data.content)
when "download_status_update" then @showDownloadStatusUpdate(data.content)
when "conference_recording_ready" then @showDownloadStatusUpdate(data.content)
showVideoStatusUpdate: (content) ->
$("[data-ujs-target='video-analysis-msg']").replaceWith content
showDownloadStatusUpdate: (content) ->
$(".flash-message").html content

View File

@@ -0,0 +1,18 @@
$(document).on("click", "[data-behavior=clipboard]", function(e) {
e.preventDefault();
// Copy the value of the link's `href` attribute
var value = $(this).attr("href");
// Creates a temporary input field to copy from
var copyToClipboard = function(value) {
var $temp = $("<input>");
$("body").append($temp);
$temp.val(value).select();
document.execCommand("copy");
$temp.remove();
}
// Perform the copying
copyToClipboard(value);
});

View File

@@ -0,0 +1,11 @@
$(document).on("change", "[data-toggle=collapse-select]", function(event) {
const select = event.target;
const target = select.dataset.target;
const showValues = JSON.parse(select.dataset.showValues);
if (showValues.indexOf(select.value) > -1) {
$(target).show("fast");
} else {
$(target).hide("fast");
}
});

View File

@@ -0,0 +1,64 @@
if (typeof(Storage) !== "undefined") {
class CollapseState {
static get _KEY() { return "collapseState"; }
set(id, value) {
const state = this._state;
state[id] = value;
window.localStorage.setItem(CollapseState._KEY, JSON.stringify(state));
}
get(id) {
const state = this._state;
return state[id];
}
get _state() {
if (window.localStorage.getItem(CollapseState._KEY) === null) {
window.localStorage.setItem(CollapseState._KEY, JSON.stringify({}));
}
const data = window.localStorage.getItem(CollapseState._KEY);
return JSON.parse(data);
}
}
// EVENT HANDLER - Store state when element is hidden
$(document).on("hidden.bs.collapse", (event) => {
const state = new CollapseState();
state.set(event.target.id, "hide");
});
// EVENT HANDLER - Store state when element is shown
$(document).on("shown.bs.collapse", (event) => {
const state = new CollapseState();
state.set(event.target.id, "show");
});
// Set initial state of elements from storage
$(document).on("turbolinks:load", function() {
const state = new CollapseState();
const collapsibles = document.querySelectorAll("[data-toggle=collapse][data-behavior=stateful]");
collapsibles.forEach( (collapsible) => {
const targetSelector = collapsible.dataset.target.toString().replace("#", "");
const target = document.getElementById(targetSelector);
const value = state.get(target.id);
switch (value) {
case "show":
collapsible.classList.remove("collapsed");
target.classList.add("show");
break;
case "hide":
collapsible.classList.add("collapsed");
target.classList.remove("show");
break;
}
})
});
} else {
console.log("No Web Storage support..");
}

View File

@@ -0,0 +1,3 @@
$(document).on("turbolinks:load", function() {
bsCustomFileInput.init()
})

View File

@@ -0,0 +1,55 @@
App.DigitalSignature = {
init: function(canvas) {
var signatureInput = $(canvas).data("signature-input");
var $clearButton = $("[data-behavior=clear-digital-signature]")
var opts = {
onEnd: function() {
// Save the signature to an input field
if ($(signatureInput).length) {
$(signatureInput).val(signaturePad.crop().toDataURL());
}
}
};
// Initialize using SignaturePad plugin
var signaturePad = new SignaturePad(canvas, opts);
// Handler for when the browser window is resized
// Stores the current signature and restores it after the resizing
function resizeCanvas() {
var cachedSignature = null;
if (!signaturePad.isEmpty()) {
var cachedSignature = signaturePad.toDataURL();
}
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
signaturePad.clear(); // otherwise isEmpty() might return incorrect value
if (cachedSignature !== null) {
signaturePad.fromDataURL(cachedSignature);
}
}
// Attach resize handler and trigger it once for an initial sizing
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
// Attach clear button handler
$clearButton.on("click", function(e) {
e.preventDefault();
signaturePad.clear();
$(signatureInput).val("");
});
},
};
$(document).on("turbolinks:load", function() {
$("[data-behavior=digital-signature]").each(function(index, element) {
App.DigitalSignature.init(element);
});
});

View File

@@ -0,0 +1,31 @@
$(document).on("click", "#video_submit_button", (event) => {
const input = $("#video_file");
if (input[0].files.length > 0) {
event.preventDefault();
const bucket = input.data("aws-bucket");
const awsAccessKeyId = input.data("aws-access-key-id");
const signerUrl = input.data("signer-url");
const uploader = Evaporate.create({
logging: false,
signerUrl: signerUrl,
aws_key: awsAccessKeyId,
bucket: bucket,
cloudfront: false,
computeContentMd5: true,
cryptoMd5Method: (d) => btoa(SparkMD5.ArrayBuffer.hash(d, true)),
cryptoHexEncodedHash256: sha256,
});
const submitButton = $(event.target);
submitButton.prop('disabled', true);
const allFilesCompleted = () => {
input.value = '';
$("#video_form").submit();
}
uploadFile(input[0].files[0], uploader, input, allFilesCompleted)
}
});

View File

@@ -0,0 +1,59 @@
App.FileUploadProgress = {
// Creates a new progress bar element
createProgressBar: id => {
// <div class="progress">
const progress = document.createElement("div")
progress.setAttribute("id", `progress_bar_${id}`)
progress.setAttribute("hidden", "hidden")
progress.classList.add("mt-2")
progress.classList.add("progress")
// <div class="progress-bar">
const progressBar = document.createElement("div")
progressBar.setAttribute("role", "progressbar")
progressBar.setAttribute("aria-valuenow", "0")
progressBar.setAttribute("aria-valuemin", "0")
progressBar.setAttribute("aria-valuemax", "100")
progressBar.classList.add("progress-bar")
progress.appendChild(progressBar)
return progress
},
// Unhides an existing progress bar
showProgressBar: id => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.removeAttribute("hidden")
progressBar.firstChild.setAttribute("style", "width: 0%")
},
// Updates the progress of an existing progress bar
updateProgressBar: (id, progress) => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.firstChild.setAttribute("style", `width: ${progress}%`)
progressBar.firstChild.setAttribute("aria-valuenow", progress)
},
// Shows an error message
showError: (id, error) => {
// <div class="text-danger>...</div>
const errorMsg = document.createElement("div")
errorMsg.classList.add("text-danger")
errorMsg.appendChild( document.createTextNode(error) )
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.parentNode.replaceChild(errorMsg, progressBar)
},
// Removes a progress bar element from the document
removeProgressBar: id => {
const progressBar = App.FileUploadProgress._findProgressBar(id)
progressBar.parentNode.removeChild(progressBar)
},
// Finds a progress bar element by the given ID
_findProgressBar: id => {
return document.getElementById(`progress_bar_${id}`)
}
}

View File

@@ -0,0 +1,50 @@
$(document).on("click", "[data-behavior=select_broadcast]", function() {
var _this = this;
var checkbox = $(this).children("input:checkbox");
selectBroadcast(_this, checkbox);
});
$(document).on("click", "[data-behavior=select_broadcast] input[type='checkbox']", function(e) {
e.stopPropagation();
var _this = this;
var checkbox = $(this);
selectBroadcast(_this, checkbox);
});
function selectBroadcast(clicked_element, checkbox) {
if (clicked_element.hasChildNodes()) {
if(checkbox.prop("checked")) {
checkbox.prop('checked', false);
} else {
checkbox.prop('checked', true);
}
}
var checked = checkbox.prop("checked");
var project_id = JSON.parse($('#multi_view_broadcasts').parent().attr('data-project-id'));
var broadcast_ids = JSON.parse($('#multi_view_broadcasts').parent().attr('data-broadcast-ids'));
var selected_broadcast_id = checkbox.val();
if (checked && !broadcast_ids.includes(selected_broadcast_id)) {
broadcast_ids.push(selected_broadcast_id);
} else if(!checked && broadcast_ids.includes(selected_broadcast_id)) {
broadcast_ids.splice( $.inArray(selected_broadcast_id, broadcast_ids), 1 );
}
$('#multi_view_broadcasts').parent().attr('data-broadcast-ids', JSON.stringify(broadcast_ids));
if (broadcast_ids.length >= 2) {
multi_view_ids = $.param({multi_view_ids: broadcast_ids});
broadcast_url = "/en/projects/" + project_id + "/broadcasts/" + broadcast_ids[0] + "?" + multi_view_ids
$("#multi_view_broadcasts").attr("href", broadcast_url);
$("#multi_view_broadcasts").attr("target", "_blank");
$("#multi_view_broadcasts").removeClass('disabled');
} else if (broadcast_ids.length < 2) {
$("#multi_view_broadcasts").attr("href", "javascript:void(0);");
$("#multi_view_broadcasts").addClass('disabled');
}
}

View File

@@ -0,0 +1,60 @@
App.PhotoPreview = {
init: function(element) {
var fileInput = $(element).data("file-input");
// Listen for changes on the file input field
$(fileInput).on("change", function(e) {
var input = e.target;
if (input.files && input.files[0]) {
var reader = new FileReader();
// Load a preview of the image and append to the container
reader.onload = function (e) {
App.PhotoPreview.set(element, e.target.result);
}
reader.readAsDataURL(input.files[0]);
}
});
},
set: function(element, imgSrc) {
var img = $("<img>").attr('src', imgSrc).addClass("img-thumbnail photo-preview");
$(element).html(img);
App.PhotoPreview.autoOrient(img.get(0));
},
autoOrient: function(image) {
// Get the EXIF data from the image
EXIF.getData(image, function() {
var orientation = EXIF.getTag(this, "Orientation");
// Rotate the image to ensure it appears right side up
switch (orientation) {
case 6:
$(image).css('transform', 'rotate(90deg)');
break;
case 8:
$(image).css('transform', 'rotate(270deg)');
break;
case 3:
$(image).css('transform', 'rotate(180deg)');
break;
}
});
}
}
$(document).on("turbolinks:load", function() {
$("[data-behavior=person-photo-preview]").each(function(index, element) {
App.PhotoPreview.init(element);
});
$("[data-behavior=guardian-photo-preview]").each(function(index, element) {
App.PhotoPreview.init(element);
});
$("[data-behavior=take-person-photo]").click(function(e) {
$("[data-ujs-target=person-photo-input]").trigger("click");
});
$("[data-behavior=take-guardian-photo]").click(function(e) {
$("[data-ujs-target=guardian-photo-input]").trigger("click");
});
});

View File

@@ -0,0 +1,24 @@
$(document).on("click", "[data-behavior=play_recording]", function() {
if ($(this).hasClass('active')){
return false;
}
var playback_url = $(this).attr("data-playback-url")
$("#broadcast_video").empty();
new Clappr.Player({
parentId: '#broadcast_video',
source: playback_url,
width: '100%',
height: '100%',
autoPlay: true
});
$(".dropdown-menu").children().removeClass('active');
$(".dropdown-menu").children().children('i').remove();
$(this).siblings().removeClass('active');
$(this).siblings().children("i").remove();
$(this).addClass('active');
$(this).prepend('<i class="fa fa-check">&nbsp;</i>');
});

View File

@@ -0,0 +1,9 @@
// $(document).on("turbolinks:load", function() {
// $('body').popover({
// selector: '[data-toggle=popover]'
// });
// });
$(document).on("turbolinks:load", function() {
$('[data-toggle=popover]').popover();
});

View File

@@ -0,0 +1,81 @@
// Makes releasables selectable and taggable.
$(document).on("click", "[data-behavior=all-selectable]", function() {
var checkbox = $(this).children("input:checkbox");
var checked = checkbox.prop("checked");
if (checked) {
checkbox.prop("checked", false);
} else {
checkbox.prop("checked", true);
}
var checkedNewValue = checkbox.prop("checked");
if(checkedNewValue) {
$("[data-behavior=select]").children('input:checkbox').each(function() {
if (!this.checked) {
$(this).parent().trigger('click')
}
});
} else {
$("[data-behavior=select]").children('input:checkbox').each(function() {
if (this.checked) {
$(this).parent().trigger('click')
}
});
}
});
$(document).on("click", "[data-behavior=select]", function() {
var checkbox = $(this).children("input:checkbox");
var checked = checkbox.prop("checked");
var releasable_ids = JSON.parse($('#tag_all').parent().attr('data-releasable-ids'));
var selected_releasable_id = $(this).children('input').val();
if (checked) {
checkbox.prop("checked", false);
} else {
checkbox.prop("checked", true);
}
var checkedNewValue = checkbox.prop("checked");
if (checkedNewValue && !releasable_ids.includes(selected_releasable_id)) {
releasable_ids.push(selected_releasable_id);
} else if(!checkedNewValue && releasable_ids.includes(selected_releasable_id)) {
releasable_ids.splice( $.inArray(selected_releasable_id, releasable_ids), 1 );
}
if (releasable_ids.length >= 1) {
$("#tag_all").prop('disabled', false);
} else if (releasable_ids.length < 1) {
$("#tag_all").prop('disabled', true);
}
$('#tag_all').parent().attr('data-releasable-ids', JSON.stringify(releasable_ids));
});
$(document).on("click", "#tag_all", function(event) {
event.preventDefault();
var releasable_ids = JSON.parse($(this).parent().attr('data-releasable-ids'));
if (releasable_ids.length >= 1) {
var input_ids = $('<input>').attr({ type: 'hidden', name: 'releasable_ids', value: JSON.stringify(releasable_ids) });
var input_name = $('<input>').attr({ type: 'hidden', name: 'releasable_name', value: $("tbody").attr("id") });
$(this).parent().append(input_ids);
$(this).parent().append(input_name);
Rails.fire($(this).parent()[0], 'submit');
}
});
$(document).on("click", "[data-behavior=select] input:checkbox", function() {
$(this).parent().trigger('click');
});
$(document).on("click", "[data-behavior=all-selectable] input:checkbox", function() {
$(this).parent().trigger('click');
});

View File

@@ -0,0 +1,5 @@
$(document).on("turbolinks:load", function() {
$('body').tooltip({
selector: '[data-toggle=tooltip]'
});
});

View File

@@ -0,0 +1,4 @@
// Do not allow file attachments in rich text content
addEventListener("trix-file-accept", function(event) {
event.preventDefault();
})

View File

@@ -0,0 +1,12 @@
// Include CSP nonce for every Turbolinks request (see: content_security_policy.rb)
document.addEventListener("turbolinks:request-start", function(event) {
var xhr = event.data.xhr;
xhr.setRequestHeader("X-Turbolinks-Nonce", $("meta[name='csp-nonce']").prop('content'));
});
// Ensure all <script> tags on Turbolinks cached pages include a nonce
document.addEventListener("turbolinks:before-cache", function() {
$('script[nonce]').each(function(index, element) {
$(element).attr('nonce', element.nonce);
})
})

View File

@@ -0,0 +1,135 @@
// TODO: Do we need to detach events when the page is changed?
App.VideoAnalysis = {
initPlayerWithAnalysis: function(videoElement, bookmarkData) {
// Initialize the video player
var player = App.VideoPlayer.init(videoElement);
// Attach custom keyboard shortcuts
var oldBindKeyEvents = player.core.mediaControl.bindKeyEvents.bind(player.core.mediaControl);
var oldUnbindKeyEvents = player.core.mediaControl.unbindKeyEvents.bind(player.core.mediaControl);
player.core.mediaControl.bindKeyEvents = function() {
oldBindKeyEvents();
player.core.mediaControl.bindKeyAndShow('d', function() {
var markersPlugin = player.getPlugin("markers-plugin");
markersPlugin.getAll().forEach(function(marker, index) {
if ( Math.abs(marker.getTime() - player.getCurrentTime()) < 2 ) {
markersPlugin.removeMarker(marker);
}
});
});
}
player.core.mediaControl.unbindKeyEvents = function() {
console.log("Unbinding key events");
oldUnbindKeyEvents();
if (player.core.mediaControl.kibo) {
player.core.mediaControl.kibo.off('d')
}
}
function ClassyMarker(time, tooltipText, cssClass) {
this._cssClass = cssClass || null;
ClapprMarkersPlugin.StandardMarker.call(this, time, tooltipText);
}
ClassyMarker.prototype = Object.create(ClapprMarkersPlugin.StandardMarker.prototype);
ClassyMarker.prototype.constructor = ClassyMarker;
ClassyMarker.prototype._buildMarkerEl = function() {
var $marker = Clappr.$('<div />').addClass('standard-marker' + ' ' + this._cssClass);
$marker.append(Clappr.$('<div />').addClass('standard-marker-inner'));
return $marker;
}
function RemovableMarker(time, tooltipText, cssClass, url) {
this._url = url || null;
ClassyMarker.call(this, time, tooltipText, cssClass);
}
RemovableMarker.prototype = Object.create(ClassyMarker.prototype);
RemovableMarker.prototype.constructor = RemovableMarker;
RemovableMarker.prototype.onDestroy = function() {
$.ajax({ url: this.source._url, type: "DELETE", headers: { 'X-CSRF-Token': $('meta[name="csrf-token"]').attr('content') } });
};
// Add existing bookmarks to the timeline and set up handlers for adding new bookmarks
var addBookmark = function(bookmark) {
var markers = player.getPlugin("markers-plugin");
// TODO: Move the remove URL into a serializer?
var marker = new RemovableMarker(Number(bookmark.time_elapsed), bookmark.notes + " (Press 'D' to delete)", "bookmark-marker", "/bookmarks/" + bookmark.id);
markers.addMarker(marker);
}
bookmarkData.forEach(function(bookmark, index) {
addBookmark(bookmark)
});
$(document).on("addBookmark-" + $(videoElement).data("video-id"), function(event, bookmark) {
addBookmark(bookmark);
});
// TODO: Where to put this??
player.on("timeupdate", function(event) {
$("[data-ujs-target=bookmark-form] input[name='bookmark[time_elapsed]']").val(event.current);
$("[data-ujs-target=unreleased-appearance-form] input[name='unreleased_appearance[time_elapsed]']").val(event.current);
$("[data-ujs-target=video-release-confirmation-form] input[name='video_release_confirmation[time_elapsed]']").val(event.current);
$("[data-ujs-target=audio-confirmation-form] input[name='audio_confirmation[time_elapsed]']").val(event.current);
$("[data-ujs-target=graphics-element-form] input[name='graphics_element[time_elapsed]']").val(event.current);
$("[data-ujs-target=edl-event-form] input[name='edl_event[time_elapsed]']").val(event.current);
});
// // Add event handler to show recommendations as the video plays
player.on("timeupdate", function(event) {
$("#suggested_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
$("#graphics_elements_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
$("#audio_matches [data-time]").each(function(index, item) {
var time = $(item).data("time");
if (event.current > time) {
$(item).show();
} else {
$(item).hide();
}
});
});
$(document).on("click", "#seek_button", () => {
try {
const timecode = new Timecode($("#seek_input").val(), 30);
player.seek(timecode.frameCount / timecode.frameRate);
} catch(e) {
$("#seek_input").val("")
}
});
$(document).on('click',"[data-behavior~=seekable-timecode]", function(e){
e.preventDefault();
const timeCodeText = e.target.innerText;
let extractedTime = timeCodeText;
const startOfTimeCode = 8; // MOV TC: 00:00....
if (timeCodeText.startsWith('MOV')){
extractedTime = timeCodeText.slice(startOfTimeCode);
}
const timeCode = new Timecode(extractedTime, 30);
player.seek(timeCode.frameCount / timeCode.frameRate);
});
}
}
$(document).on("click", "#go_button", function () {
const timecode = $("#go_to").val()
const $edlEvent = $(`[data-timecode-in='${timecode}']`).first()
const edlEventContainer = $("#edl_events_list").parent();
if ($edlEvent.length > 0) {
edlEventContainer.scrollTop($edlEvent.offset().top - edlEventContainer.offset().top + edlEventContainer.scrollTop())
} else {
$("#go_to").val("")
}
});

View File

@@ -0,0 +1,97 @@
App.VideoPlayer = {
init: function(videoElement) {
// The source path for the video
var videoUrl = $(videoElement).attr("src");
// Replace the standard <video> element with the Clappr player
$(videoElement).replaceWith($("<div>", { id: "player" }));
var player = new Clappr.Player({
<%= "baseUrl: 'http://cdn.clappr.io/latest'," if Rails.env.test? %>
source: videoUrl, parentId: "#player", width: "100%",
focusElement: NotTypingSelectiveListenerElement(document), // Allows keyboard shortcuts to work even if the player itself isn't in focus
plugins: {
core: [ClapprMarkersPlugin, PlaybackRatePlugin]
},
markersPlugin: { markers: [] }
});
return player;
}
}
$(document).on("turbolinks:load", function() {
$("[data-behavior=video-player]").each(function(index, element) {
App.VideoPlayer.init(element);
});
});
// An implementation of SelectiveListenerElement which only listens when text areas are not in focus
var NotTypingSelectiveListenerElement = function(element) {
var inputInFocus = function() { return $('input:focus, textarea:focus, video:focus').length > 0; }
var noInputInFocus = function() { return !inputInFocus(); }
return new SelectiveListenerElement(element, noInputInFocus);
}
// A helper class that maintains a collection of handlers for a given event and callback
var HandlerCollection = function() {
this.handlers = [];
// Stores a handler for a given event and callback
this.store = function(eventName, originalCallback, handler) {
var item = {
eventName: eventName,
callback: originalCallback,
handler: handler,
}
this.handlers.push(item);
}
// Finds a handler for a given event and callback
this.find = function(eventName, callback) {
var handler = null;
for (var i = 0; i < this.handlers.length; i++) {
var item = this.handlers[i];
if (item.eventName == eventName && item.callback == callback) {
handler = item.handler;
}
}
return handler;
}
}
// A helper class to allow a DOM element to be wrapped and selectively ignore events that are triggered on it
var SelectiveListenerElement = function(element, shouldListenFn) {
// The element we are proxying
this.element = element;
// A function that determines if we should run any event callbacks or not
this.shouldRunCallbacks = shouldListenFn;
// Stores the functions to be called when an event listener needs to be removed
this.removeHandlers = new HandlerCollection();
this.addEventListener = function(eventName, callback, useCapture) {
// Create a new callback function that conditionally calls the original
var conditionalCallback = function() {
if (this.shouldRunCallbacks()) {
callback();
}
}.bind(this);
// Store the function to be called when removing the event listener
this.removeHandlers.store(eventName, callback, function() {
this.element.removeEventListener(eventName, conditionalCallback, useCapture);
}.bind(this));
// Attach an event listener to the same event with the conditional callback function
this.element.addEventListener(eventName, conditionalCallback, useCapture);
}
this.removeEventListener = function(eventName, callback, useCapture) {
var removeHandler = this.removeHandlers.find(eventName, callback);
if (removeHandler != null) {
removeHandler();
}
}
}