Initial commit
This commit is contained in:
67
app/assets/javascripts/appearance_release_import.js
Normal file
67
app/assets/javascripts/appearance_release_import.js
Normal 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');
|
||||
});
|
||||
});
|
||||
21
app/assets/javascripts/application.js
Normal file
21
app/assets/javascripts/application.js
Normal 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 .
|
||||
13
app/assets/javascripts/cable.js
Normal file
13
app/assets/javascripts/cable.js
Normal 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);
|
||||
0
app/assets/javascripts/channels/.keep
Normal file
0
app/assets/javascripts/channels/.keep
Normal file
45
app/assets/javascripts/channels/broadcasts.coffee
Normal file
45
app/assets/javascripts/channels/broadcasts.coffee
Normal 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
|
||||
|
||||
25
app/assets/javascripts/channels/projects.coffee
Normal file
25
app/assets/javascripts/channels/projects.coffee
Normal 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
|
||||
18
app/assets/javascripts/clipboard.js
Normal file
18
app/assets/javascripts/clipboard.js
Normal 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);
|
||||
});
|
||||
11
app/assets/javascripts/collapse_select.js
Normal file
11
app/assets/javascripts/collapse_select.js
Normal 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");
|
||||
}
|
||||
});
|
||||
64
app/assets/javascripts/collapse_state.js
Normal file
64
app/assets/javascripts/collapse_state.js
Normal 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..");
|
||||
}
|
||||
3
app/assets/javascripts/custom_file_input.js
Normal file
3
app/assets/javascripts/custom_file_input.js
Normal file
@@ -0,0 +1,3 @@
|
||||
$(document).on("turbolinks:load", function() {
|
||||
bsCustomFileInput.init()
|
||||
})
|
||||
55
app/assets/javascripts/digital_signature.js
Normal file
55
app/assets/javascripts/digital_signature.js
Normal 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);
|
||||
});
|
||||
});
|
||||
31
app/assets/javascripts/evaporate.js
Normal file
31
app/assets/javascripts/evaporate.js
Normal 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)
|
||||
}
|
||||
});
|
||||
59
app/assets/javascripts/file_upload_progress.js
Normal file
59
app/assets/javascripts/file_upload_progress.js
Normal 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}`)
|
||||
}
|
||||
}
|
||||
50
app/assets/javascripts/multi_view_broadcasts.js
Normal file
50
app/assets/javascripts/multi_view_broadcasts.js
Normal 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');
|
||||
}
|
||||
}
|
||||
60
app/assets/javascripts/photo_preview.js
Normal file
60
app/assets/javascripts/photo_preview.js
Normal 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");
|
||||
});
|
||||
});
|
||||
24
app/assets/javascripts/play_previous_recordings.js
Normal file
24
app/assets/javascripts/play_previous_recordings.js
Normal 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"> </i>');
|
||||
});
|
||||
|
||||
9
app/assets/javascripts/popover.js
Normal file
9
app/assets/javascripts/popover.js
Normal 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();
|
||||
});
|
||||
81
app/assets/javascripts/select_releasables.js
Normal file
81
app/assets/javascripts/select_releasables.js
Normal 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');
|
||||
});
|
||||
5
app/assets/javascripts/tooltip.js
Normal file
5
app/assets/javascripts/tooltip.js
Normal file
@@ -0,0 +1,5 @@
|
||||
$(document).on("turbolinks:load", function() {
|
||||
$('body').tooltip({
|
||||
selector: '[data-toggle=tooltip]'
|
||||
});
|
||||
});
|
||||
4
app/assets/javascripts/trix.js
Normal file
4
app/assets/javascripts/trix.js
Normal file
@@ -0,0 +1,4 @@
|
||||
// Do not allow file attachments in rich text content
|
||||
addEventListener("trix-file-accept", function(event) {
|
||||
event.preventDefault();
|
||||
})
|
||||
12
app/assets/javascripts/turbolinks-csp.js
Normal file
12
app/assets/javascripts/turbolinks-csp.js
Normal 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);
|
||||
})
|
||||
})
|
||||
135
app/assets/javascripts/video_analysis.js
Normal file
135
app/assets/javascripts/video_analysis.js
Normal 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("")
|
||||
}
|
||||
});
|
||||
97
app/assets/javascripts/video_player.js.erb
Normal file
97
app/assets/javascripts/video_player.js.erb
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user