Flex tab in channel editor
This commit is contained in:
parent
398ee0e83f
commit
6cc0cce2d2
@ -102,6 +102,26 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
updateChannelDuration();
|
||||
setTimeout( () => { scope.showRotatedNote = true }, 1, 'funky');
|
||||
}
|
||||
let addMinuteVersionsOfFields = () => {
|
||||
//add the minutes versions of the cooldowns:
|
||||
scope.channel.fillerRepeatCooldownMinutes = scope.channel.fillerRepeatCooldown / 1000 / 60;
|
||||
for (let i = 0; i < scope.channel.fillerCollections.length; i++) {
|
||||
scope.channel.fillerCollections[i].cooldownMinutes = scope.channel.fillerCollections[i].cooldown / 1000 / 60;
|
||||
|
||||
}
|
||||
}
|
||||
addMinuteVersionsOfFields();
|
||||
|
||||
let removeMinuteVersionsOfFields = (channel) => {
|
||||
channel.fillerRepeatCooldown = channel.fillerRepeatCooldownMinutes * 60 * 1000;
|
||||
delete channel.fillerRepeatCooldownMinutes;
|
||||
for (let i = 0; i < channel.fillerCollections.length; i++) {
|
||||
channel.fillerCollections[i].cooldown = channel.fillerCollections[i].cooldownMinutes * 60 * 1000;
|
||||
delete channel.fillerCollections[i].cooldownMinutes;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (scope.isNewChannel) {
|
||||
scope.tab = "basic";
|
||||
} else {
|
||||
@ -163,19 +183,9 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
};
|
||||
}
|
||||
|
||||
scope.updateChannelFromOfflineResult = (program) => {
|
||||
scope.channel.offlineMode = program.channelOfflineMode;
|
||||
scope.channel.offlinePicture = program.channelPicture;
|
||||
scope.channel.offlineSoundtrack = program.channelSound;
|
||||
scope.channel.fillerRepeatCooldown = program.repeatCooldown * 60000;
|
||||
scope.channel.fillerCollections = JSON.parse( angular.toJson(program.filler.map(fixFillerCollection) ) );
|
||||
scope.channel.fallback = JSON.parse( angular.toJson(program.fallback) );
|
||||
scope.channel.disableFillerOverlay = program.disableOverlay;
|
||||
}
|
||||
scope.finishedOfflineEdit = (program) => {
|
||||
let editedProgram = scope.channel.programs[scope.selectedProgram];
|
||||
let duration = program.durationSeconds * 1000;
|
||||
scope.updateChannelFromOfflineResult(program);
|
||||
editedProgram.duration = duration;
|
||||
editedProgram.isOffline = true;
|
||||
scope._selectedOffline = null
|
||||
@ -187,7 +197,6 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
duration: duration,
|
||||
isOffline: true
|
||||
}
|
||||
scope.updateChannelFromOfflineResult(result);
|
||||
scope.channel.programs.splice(scope.minProgramIndex, 0, program);
|
||||
scope._selectedOffline = null
|
||||
scope._addingOffline = null;
|
||||
@ -888,14 +897,7 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
}
|
||||
scope.makeOfflineFromChannel = (duration) => {
|
||||
return {
|
||||
channelOfflineMode: scope.channel.offlineMode,
|
||||
channelPicture: scope.channel.offlinePicture,
|
||||
channelSound: scope.channel.offlineSoundtrack,
|
||||
repeatCooldown : Math.floor(scope.channel.fillerRepeatCooldown / 60000),
|
||||
filler: JSON.parse( angular.toJson(scope.channel.fillerCollections.map(unfixFillerCollection) ) ),
|
||||
fallback: JSON.parse( angular.toJson(scope.channel.fallback) ),
|
||||
durationSeconds: duration,
|
||||
disableOverlay : scope.channel.disableFillerOverlay,
|
||||
}
|
||||
}
|
||||
scope.addOffline = () => {
|
||||
@ -1098,6 +1100,8 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
// validate
|
||||
var now = new Date()
|
||||
scope.error.any = true;
|
||||
|
||||
|
||||
if (typeof channel.number === "undefined" || channel.number === null || channel.number === "") {
|
||||
scope.error.number = "Select a channel number"
|
||||
scope.error.tab = "basic";
|
||||
@ -1125,22 +1129,38 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
} else if (channel.programs.length === 0) {
|
||||
scope.error.programs = "No programs have been selected. Select at least one program."
|
||||
scope.error.tab = "programming";
|
||||
} else if (
|
||||
channel.offlineMode != 'pic'
|
||||
&& (channel.fallback.length == 0)
|
||||
) {
|
||||
scope.error.fallback = 'Either add a fallback clip or change the fallback mode to Picture.';
|
||||
scope.error.tab = "flex";
|
||||
} else {
|
||||
scope.error.any = false;
|
||||
for (let i = 0; i < scope.channel.programs.length; i++) {
|
||||
delete scope.channel.programs[i].$index;
|
||||
}
|
||||
try {
|
||||
removeMinuteVersionsOfFields(channel);
|
||||
let s = angular.toJson(channel);
|
||||
addMinuteVersionsOfFields();
|
||||
if (s.length > 50*1000*1000) {
|
||||
scope.error.any = true;
|
||||
scope.error.programs = "Channel is too large, can't save.";
|
||||
scope.error.tab = "programming";
|
||||
} else {
|
||||
await scope.onDone(JSON.parse(s))
|
||||
let cloned = JSON.parse(s);
|
||||
//clean up some stuff that's only used by the UI:
|
||||
cloned.fillerCollections = cloned.fillerCollections.filter( (f) => { return f.id != 'none'; } );
|
||||
cloned.fillerCollections.forEach( (c) => {
|
||||
delete c.percentage;
|
||||
delete c.options;
|
||||
} );
|
||||
await scope.onDone(cloned)
|
||||
s = null;
|
||||
}
|
||||
} catch(err) {
|
||||
addMinuteVersionsOfFields();
|
||||
$timeout();
|
||||
console.error(err);
|
||||
scope.error.any = true;
|
||||
@ -1339,6 +1359,28 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
|
||||
}
|
||||
|
||||
scope.openFallbackLibrary = () => {
|
||||
scope.showFallbackPlexLibrary = true
|
||||
}
|
||||
|
||||
scope.importFallback = (selectedPrograms) => {
|
||||
for (let i = 0, l = selectedPrograms.length; i < l && i < 1; i++) {
|
||||
selectedPrograms[i].commercials = []
|
||||
}
|
||||
scope.channel.fallback = [];
|
||||
if (selectedPrograms.length > 0) {
|
||||
scope.channel.fallback = [ selectedPrograms[0] ];
|
||||
}
|
||||
scope.showFallbackPlexLibrary = false;
|
||||
}
|
||||
|
||||
scope.fillerOptions = scope.channel.fillerCollections.map( (f) => {
|
||||
return {
|
||||
id: f.id,
|
||||
name: `(${f.id})`,
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
scope.nightStartHours = [ { id: -1, description: "Start" } ];
|
||||
scope.nightEndHours = [ { id: -1, description: "End" } ];
|
||||
@ -1354,6 +1396,116 @@ module.exports = function ($timeout, $location, dizquetv) {
|
||||
}
|
||||
scope.rerunStartHours = scope.nightStartHours;
|
||||
scope.paddingMod = 30;
|
||||
|
||||
let fillerOptionsFor = (index) => {
|
||||
let used = {};
|
||||
let added = {};
|
||||
for (let i = 0; i < scope.channel.fillerCollections.length; i++) {
|
||||
if (scope.channel.fillerCollections[i].id != 'none' && i != index) {
|
||||
used[ scope.channel.fillerCollections[i].id ] = true;
|
||||
}
|
||||
}
|
||||
let options = [];
|
||||
for (let i = 0; i < scope.fillerOptions.length; i++) {
|
||||
if ( used[scope.fillerOptions[i].id] !== true) {
|
||||
added[scope.fillerOptions[i].id] = true;
|
||||
options.push( scope.fillerOptions[i] );
|
||||
}
|
||||
}
|
||||
if (scope.channel.fillerCollections[index].id == 'none') {
|
||||
added['none'] = true;
|
||||
options.push( {
|
||||
id: 'none',
|
||||
name: 'Add a filler list...',
|
||||
} );
|
||||
}
|
||||
if ( added[scope.channel.fillerCollections[index].id] !== true ) {
|
||||
options.push( {
|
||||
id: scope.channel.fillerCollections[index].id,
|
||||
name: `[${f.id}]`,
|
||||
} );
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
scope.refreshFillerStuff = () => {
|
||||
if (typeof(scope.channel.fillerCollections) === 'undefined') {
|
||||
return;
|
||||
}
|
||||
addAddFiller();
|
||||
updatePercentages();
|
||||
refreshIndividualOptions();
|
||||
}
|
||||
|
||||
let updatePercentages = () => {
|
||||
let w = 0;
|
||||
for (let i = 0; i < scope.channel.fillerCollections.length; i++) {
|
||||
if (scope.channel.fillerCollections[i].id !== 'none') {
|
||||
w += scope.channel.fillerCollections[i].weight;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < scope.channel.fillerCollections.length; i++) {
|
||||
if (scope.channel.fillerCollections[i].id !== 'none') {
|
||||
scope.channel.fillerCollections[i].percentage = (scope.channel.fillerCollections[i].weight * 100 / w).toFixed(2) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
let addAddFiller = () => {
|
||||
if ( (scope.channel.fillerCollections.length == 0) || (scope.channel.fillerCollections[scope.channel.fillerCollections.length-1].id !== 'none') ) {
|
||||
scope.channel.fillerCollections.push ( {
|
||||
'id': 'none',
|
||||
'weight': 300,
|
||||
'cooldown': 0,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let refreshIndividualOptions = () => {
|
||||
for (let i = 0; i < scope.channel.fillerCollections.length; i++) {
|
||||
scope.channel.fillerCollections[i].options = fillerOptionsFor(i);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshFillerOptions = async() => {
|
||||
|
||||
try {
|
||||
let r = await dizquetv.getAllFillersInfo();
|
||||
scope.fillerOptions = r.map( (f) => {
|
||||
return {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
};
|
||||
} );
|
||||
scope.refreshFillerStuff();
|
||||
scope.$apply();
|
||||
} catch(err) {
|
||||
console.error("Unable to get filler info", err);
|
||||
}
|
||||
};
|
||||
scope.refreshFillerStuff();
|
||||
refreshFillerOptions();
|
||||
|
||||
scope.showList = () => {
|
||||
return ! scope.showFallbackPlexLibrary;
|
||||
}
|
||||
|
||||
|
||||
scope.deleteFillerList =(index) => {
|
||||
scope.channel.fillerCollections.splice(index, 1);
|
||||
scope.refreshFillerStuff();
|
||||
}
|
||||
|
||||
|
||||
|
||||
scope.durationString = (duration) => {
|
||||
var date = new Date(0);
|
||||
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
|
||||
return date.toISOString().substr(11, 8);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
const dizquetv = require("../services/dizquetv");
|
||||
|
||||
module.exports = function ($timeout, dizquetv) {
|
||||
return {
|
||||
restrict: 'E',
|
||||
@ -12,7 +10,6 @@ module.exports = function ($timeout, dizquetv) {
|
||||
onDone: "=onDone"
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
scope.fillerOptions = [];
|
||||
let updateNext = true;
|
||||
scope.$watch('program', () => {
|
||||
try {
|
||||
@ -23,126 +20,14 @@ module.exports = function ($timeout, dizquetv) {
|
||||
return;
|
||||
}
|
||||
updateNext = false;
|
||||
let filler = scope.program.filler;
|
||||
if (typeof(filler) === 'undefined') {
|
||||
filler = [];
|
||||
}
|
||||
scope.program.filler = filler;
|
||||
scope.showFallbackPlexLibrary = false;
|
||||
scope.fillerOptions = filler.map( (f) => {
|
||||
return {
|
||||
id: f.id,
|
||||
name: `(${f.id})`,
|
||||
}
|
||||
});
|
||||
|
||||
$timeout( () => {
|
||||
refreshFillerOptions();
|
||||
}, 0);
|
||||
} catch(err) {
|
||||
console.error("$watch error", err);
|
||||
scope.error = null;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
})
|
||||
|
||||
let fillerOptionsFor = (index) => {
|
||||
let used = {};
|
||||
let added = {};
|
||||
for (let i = 0; i < scope.program.filler.length; i++) {
|
||||
if (scope.program.filler[i].id != 'none' && i != index) {
|
||||
used[ scope.program.filler[i].id ] = true;
|
||||
}
|
||||
}
|
||||
let options = [];
|
||||
for (let i = 0; i < scope.fillerOptions.length; i++) {
|
||||
if ( used[scope.fillerOptions[i].id] !== true) {
|
||||
added[scope.fillerOptions[i].id] = true;
|
||||
options.push( scope.fillerOptions[i] );
|
||||
}
|
||||
}
|
||||
if (scope.program.filler[index].id == 'none') {
|
||||
added['none'] = true;
|
||||
options.push( {
|
||||
id: 'none',
|
||||
name: 'Add a filler list...',
|
||||
} );
|
||||
}
|
||||
if ( added[scope.program.filler[index].id] !== true ) {
|
||||
options.push( {
|
||||
id: scope.program.filler[index].id,
|
||||
name: `[${f.id}]`,
|
||||
} );
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
scope.refreshFillerStuff = () => {
|
||||
if (typeof(scope.program) === 'undefined') {
|
||||
return;
|
||||
}
|
||||
addAddFiller();
|
||||
updatePercentages();
|
||||
refreshIndividualOptions();
|
||||
}
|
||||
|
||||
let updatePercentages = () => {
|
||||
let w = 0;
|
||||
for (let i = 0; i < scope.program.filler.length; i++) {
|
||||
if (scope.program.filler[i].id !== 'none') {
|
||||
w += scope.program.filler[i].weight;
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < scope.program.filler.length; i++) {
|
||||
if (scope.program.filler[i].id !== 'none') {
|
||||
scope.program.filler[i].percentage = (scope.program.filler[i].weight * 100 / w).toFixed(2) + "%";
|
||||
}
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
let addAddFiller = () => {
|
||||
if ( (scope.program.filler.length == 0) || (scope.program.filler[scope.program.filler.length-1].id !== 'none') ) {
|
||||
scope.program.filler.push ( {
|
||||
'id': 'none',
|
||||
'weight': 300,
|
||||
'cooldown': 0,
|
||||
} );
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let refreshIndividualOptions = () => {
|
||||
for (let i = 0; i < scope.program.filler.length; i++) {
|
||||
scope.program.filler[i].options = fillerOptionsFor(i);
|
||||
}
|
||||
}
|
||||
|
||||
let refreshFillerOptions = async() => {
|
||||
|
||||
try {
|
||||
let r = await dizquetv.getAllFillersInfo();
|
||||
scope.fillerOptions = r.map( (f) => {
|
||||
return {
|
||||
id: f.id,
|
||||
name: f.name,
|
||||
};
|
||||
} );
|
||||
scope.refreshFillerStuff();
|
||||
scope.$apply();
|
||||
} catch(err) {
|
||||
console.error("Unable to get filler info", err);
|
||||
}
|
||||
};
|
||||
scope.refreshFillerStuff();
|
||||
refreshFillerOptions();
|
||||
|
||||
scope.finished = (prog) => {
|
||||
if (
|
||||
prog.channelOfflineMode != 'pic'
|
||||
&& (prog.fallback.length == 0)
|
||||
) {
|
||||
scope.error = { fallback: 'Either add a fallback clip or change the fallback mode to Picture.' }
|
||||
}
|
||||
scope.error = null;
|
||||
if (isNaN(prog.durationSeconds) || prog.durationSeconds < 0 ) {
|
||||
scope.error = { duration: 'Duration must be a positive integer' }
|
||||
}
|
||||
@ -152,37 +37,9 @@ module.exports = function ($timeout, dizquetv) {
|
||||
}, 30000)
|
||||
return
|
||||
}
|
||||
prog.filler = prog.filler.filter( (f) => { return f.id != 'none'; } );
|
||||
scope.onDone(JSON.parse(angular.toJson(prog)))
|
||||
scope.program = null
|
||||
}
|
||||
scope.showList = () => {
|
||||
return ! scope.showFallbackPlexLibrary;
|
||||
}
|
||||
scope.importFallback = (selectedPrograms) => {
|
||||
for (let i = 0, l = selectedPrograms.length; i < l && i < 1; i++) {
|
||||
selectedPrograms[i].commercials = []
|
||||
}
|
||||
scope.program.fallback = [];
|
||||
if (selectedPrograms.length > 0) {
|
||||
scope.program.fallback = [ selectedPrograms[0] ];
|
||||
}
|
||||
scope.showFallbackPlexLibrary = false;
|
||||
}
|
||||
|
||||
|
||||
scope.deleteFillerList =(index) => {
|
||||
scope.program.filler.splice(index, 1);
|
||||
scope.refreshFillerStuff();
|
||||
}
|
||||
|
||||
|
||||
|
||||
scope.durationString = (duration) => {
|
||||
var date = new Date(0);
|
||||
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
|
||||
return date.toISOString().substr(11, 8);
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
@ -285,7 +285,7 @@ div.programming-panes div.programming-pane {
|
||||
div.programming-programs div.list-group-item {
|
||||
height: 1.5rem;
|
||||
}
|
||||
.channel-editor-modal {
|
||||
.channel-editor-modal-big {
|
||||
width:1200px;
|
||||
min-width: 98%;
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<div>
|
||||
<div class="modal" tabindex="-1" role="dialog" style="display: block; background-color: rgba(0, 0, 0, .5);">
|
||||
<div class="modal-dialog modal-xl channel-editor-modal" role="document">
|
||||
<div ng-class="{'channel-editor-modal-big':showShuffleOptions, 'modal-dialog-scrollable': (tab !== 'programming') }" class="modal-dialog modal-xl" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<ul class="nav nav-tabs">
|
||||
@ -24,6 +24,16 @@
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<span class="nav-link btn btn-link {{ tab === 'flex' ? 'active' : ''}}" ng-click="tab = 'flex'">
|
||||
<span ng-if="error.tab==='flex'" class='text-danger'>
|
||||
<i class='fas fa-exclamation-triangle' ></i> Flex
|
||||
</span>
|
||||
<span ng-if="error.tab!=='flex'">
|
||||
Flex
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<h5 class="modal-title">
|
||||
{{ getTitle() }}
|
||||
@ -145,11 +155,6 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="(tab == 'programming') && showShuffleOptions">
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body programming-panes" ng-if="tab == 'programming'">
|
||||
<div class='row'>
|
||||
@ -457,6 +462,143 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-body" ng-if="tab == 'flex'">
|
||||
<div class='text-center text-info' ng-show='!hasFlex'>
|
||||
<i class='fas fa-info-circle' ></i> Use programming tools like "Add Padding", "Add Breaks" or "Add Flex" to add "Flex" time slots to the channel's programming.
|
||||
</div>
|
||||
<div class='row'>
|
||||
<div class="col-md-12">
|
||||
<label for="offlineMode" class="small">Fallback Mode:</label>
|
||||
<div class="input-group mb-1">
|
||||
<select class="form-control form-control-sm" id="offlineMode" ng-model="channel.offlineMode">
|
||||
<option value="pic">Picture</option>
|
||||
<option value="clip">Clip from Library</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="channel.offlineMode == 'clip'" >
|
||||
<p class="text-center text-info">Pick a video clip that will be used for fallback when there's no appropriate
|
||||
filler available for the time duration. It's recommended to use countdown or looping videos for this. <span class="text-danger">{{error.fallback}}</span></p>
|
||||
</div>
|
||||
|
||||
<div class='row' ng-show="channel.offlineMode == 'pic'" >
|
||||
<div class="col-md-3">
|
||||
<img ng-src="{{channel.offlinePicture}}" alt="Fallback preview" style="max-height: 120px;"/>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div>
|
||||
<label for="offlinePicture" class="small">
|
||||
Picture: <span class="text-danger pull-right">{{error.picture}}</span></label>
|
||||
<input name="offlinePicture" id="offlinePicture" class="form-control form-control-sm" type="url" ng-model="channel.offlinePicture" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="offlineSound" class="small">Sound Track:<span class="text-danger pull-right">{{error.sound}}</span></label>
|
||||
<input name="offlineSound" id="offlineSound" class="form-control form-control-sm" type="url" ng-model="channel.offlineSoundtrack" placeholder="URL to a sound track that will loop during the offline screen, leave empty for silence." />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class='row' ng-show="channel.offlineMode == 'pic'" >
|
||||
<div class='col-md-12'><p class="text-center text-info">This picture is used in case there are no filler clips available with a shorter length than the Flex time duration. Requires ffmpeg transcoding.</p></div>
|
||||
</div>
|
||||
|
||||
<div ng-show="channel.offlineMode == 'clip'">
|
||||
<div class="list-group list-group-root" dnd-list="channel.fallback">
|
||||
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in channel.fallback" dnd-draggable="x" dnd-moved="channel.fallback.splice($index, 1)" dnd-effect-allowed="move">
|
||||
<div class="program-start" >
|
||||
{{durationString(x.duration)}}
|
||||
</div>
|
||||
<div ng-style="programSquareStyle(x, true)" />
|
||||
<div style="margin-right: 5px;">
|
||||
<strong>Fallback:</strong> {{x.title}}
|
||||
</div>
|
||||
<div class="flex-pull-right">
|
||||
<button class="btn btn-sm btn-link" ng-click="channel.fallback.splice($index,1)">
|
||||
<i class="text-danger fa fa-trash-alt" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="channel.fallback.length === 0">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="openFallbackLibrary()">Pick fallback</button>
|
||||
</div>
|
||||
<hr style='margin-top:0' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 style="margin-top: 10px;">Filler</h5>
|
||||
<div>
|
||||
<label>Minimum time before replaying a filler (Minutes): </label>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="channel.fillerRepeatCooldownMinutes" ng-pattern="/^([1-9][0-9]*)$/" min='0' max='1440' />
|
||||
|
||||
<span class="text-danger pull-right">{{error.blockRepeats}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<input id="overlayDiableIcon" type="checkbox" ng-model="channel.disableFillerOverlay">
|
||||
<label class="small" for="overlayDisableIcon" style="margin-bottom: 4px;"> Disable overlay icon when playing filler </label>
|
||||
</div>
|
||||
<hr />
|
||||
<h6>Filler Lists</h6>
|
||||
<div id='fillerContainer'>
|
||||
<br />
|
||||
|
||||
<div class="form-row" ng-repeat = "x in channel.fillerCollections" track-by = "$index">
|
||||
<div class='form-group col-md-5'>
|
||||
<label ng-if="$index==0" for="fillerselect{{$index}}">List</label>
|
||||
<select
|
||||
id="fillerselect{{$index}}" class="custom-select form-control"
|
||||
ng-model="x.id" ng-options="o.id as o.name for o in x.options"
|
||||
ng-change="refreshFillerStuff()"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
<div class='form-group col-md-2' ng-if="x.id !== 'none' " >
|
||||
<label ng-if="$index==0" for="cooldown{{$index}}">Cooldown (minutes)</label>
|
||||
<input class='form-control' id="cooldown{{$index}}" type='number' ng-model='x.cooldownMinutes' ng-pattern="/^([0-9][0-9]*)$/"
|
||||
min='0' max='1440'
|
||||
data-toggle="tooltip" data-placement="bottom" title="The channel won't pick a video from this list if it played something from this list less than this amount of minutes ago."
|
||||
> </input>
|
||||
</div>
|
||||
<div class='form-group col-md-2' ng-if="x.id === 'none' " >
|
||||
</div>
|
||||
<div class='form-group col-md-3' ng-if="x.id !== 'none' && channel.fillerCollections.length > 2 " >
|
||||
<label ng-if="$index==0" for="fillerrange{{$index}}">Weight</label>
|
||||
<input class='form-control-range custom-range' id="fillerrange{{$index}}" type='range' ng-model='x.weight' min=1 max=600
|
||||
data-toggle="tooltip" data-placement="bottom" title="Lists with more weight will be picked more frequently."
|
||||
ng-change="refreshFillerStuff()"
|
||||
>
|
||||
</input>
|
||||
</div>
|
||||
<div class='form-group col-md-4' ng-if="x.id === 'none' || channel.fillerCollections.length <= 2 " >
|
||||
</div>
|
||||
<div class='form-group col-md-1' ng-if="x.id !== 'none' && channel.fillerCollections.length > 2" >
|
||||
<label ng-if="$index==0" for="fillerp{{$index}}">%</label>
|
||||
<input class='form-control flex-filler-percent' id="fillerp{{$index}}" type='text' ng-model='x.percentage'
|
||||
data-toggle="tooltip" data-placement="bottom" title="This is the overall probability this list might be picked, assuming all lists are available." readonly
|
||||
>
|
||||
</input>
|
||||
</div>
|
||||
<div class='form-group col-md-1' ng-if="x.id !== 'none'" >
|
||||
<label ng-if="$index==0" for="delete{{$index}}">-</label>
|
||||
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteFillerList($index)' >
|
||||
<i class='text-danger fa fa-trash-alt'></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr></hr >
|
||||
<p class="text-center text-info">Videos from the filler list will be randomly picked to play unless there are cooldown restrictions to place or if no videos are short enough for the remaining Flex time. Use the Filler tab at the main page to create Filler Lists. If no videos are available, the fallback will be used instead.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<span class="pull-right text-danger" ng-show="error.any"> <i class='fa fa-exclamation-triangle'></i> There were errors. Please review the form.</span>
|
||||
<span class="pull-right text-info" ng-show='! hasPrograms() && (tab != "programming")'> <i class='fas fa-info-circle'></i> Use the "Programming" tab to add programs to the channel.</span>
|
||||
@ -478,5 +620,6 @@
|
||||
<remove-shows program-titles="_removablePrograms" on-done="removeShows" deleted="_deletedProgramNames"></remove-shows>
|
||||
<flex-config offline-title="Add Flex Time" program="_addingOffline" on-done="finishedAddingOffline"></flex-config>
|
||||
<plex-library limit="libraryLimit" height="300" visible="displayPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
|
||||
<channel-redirect visible="_displayRedirect" on-done="finishRedirect" form-title="_redirectTitle" program="_selectedRedirect" ></channel-redirect>
|
||||
</div>
|
||||
|
||||
@ -12,141 +12,6 @@
|
||||
<label>Duration (Seconds): <span class="text-danger pull-right">{{error.duration}}</span></label>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="program.durationSeconds" ng-pattern="/^([1-9][0-9]*)$/"/>
|
||||
|
||||
<h6 style="margin-top: 10px;">Flex Settings:</h6>
|
||||
|
||||
<div class='row'>
|
||||
<div class="col-md-12">
|
||||
<label for="offlineMode" class="small">Fallback Mode:</label>
|
||||
<div class="input-group mb-1">
|
||||
<select class="form-control form-control-sm" id="offlineMode" ng-model="program.channelOfflineMode">
|
||||
<option value="pic">Picture</option>
|
||||
<option value="clip">Clip from Library</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div ng-show="program.channelOfflineMode == 'clip'" >
|
||||
<p class="text-center text-info">Pick a video clip that will be used for fallback when there's no appropriate
|
||||
filler available for the time duration. It's recommended to use countdown or looping videos for this. <span class="text-danger">{{error.fallback}}</span></p>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div class='row' ng-show="program.channelOfflineMode == 'pic'" >
|
||||
<div class="col-md-3">
|
||||
<img ng-src="{{program.channelPicture}}" alt="Fallback preview" style="max-height: 120px;"/>
|
||||
</div>
|
||||
<div class="col-md-9">
|
||||
<div>
|
||||
<label for="offlinePicture" class="small">
|
||||
Picture: <span class="text-danger pull-right">{{error.picture}}</span></label>
|
||||
<input name="offlinePicture" id="offlinePicture" class="form-control form-control-sm" type="url" ng-model="program.channelPicture" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="offlineSound" class="small">Sound Track:<span class="text-danger pull-right">{{error.sound}}</span></label>
|
||||
<input name="offlineSound" id="offlineSound" class="form-control form-control-sm" type="url" ng-model="program.channelSound" placeholder="URL to a sound track that will loop during the offline screen, leave empty for silence." />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class='row' ng-show="program.channelOfflineMode == 'pic'" >
|
||||
<div class='col-md-12'><p class="text-center text-info">This picture is used in case there are no filler clips available with a shorter length than the Flex time duration. Requires ffmpeg transcoding.</p></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div ng-show="program.channelOfflineMode == 'clip'">
|
||||
<div class="list-group list-group-root" dnd-list="program.fallback">
|
||||
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in program.fallback" dnd-draggable="x" dnd-moved="program.fallback.splice($index, 1)" dnd-effect-allowed="move">
|
||||
<div class="program-start" >
|
||||
{{durationString(x.duration)}}
|
||||
</div>
|
||||
<div ng-style="programSquareStyle(x, true)" />
|
||||
<div style="margin-right: 5px;">
|
||||
<strong>Fallback:</strong> {{x.title}}
|
||||
</div>
|
||||
<div class="flex-pull-right">
|
||||
<button class="btn btn-sm btn-link" ng-click="program.fallback.splice($index,1)">
|
||||
<i class="text-danger fa fa-trash-alt" ></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div ng-show="program.fallback.length === 0">
|
||||
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="showFallbackPlexLibrary = true">Pick fallback</button>
|
||||
</div>
|
||||
<hr style='margin-top:0' />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h5 style="margin-top: 10px;">Filler</h5>
|
||||
<div>
|
||||
<label>Minimum time before replaying a filler (Minutes): </label>
|
||||
<input type="number" class="form-control form-control-sm" ng-model="program.repeatCooldown" ng-pattern="/^([1-9][0-9]*)$/" min='0' max='1440' />
|
||||
|
||||
<span class="text-danger pull-right">{{error.blockRepeats}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<input id="overlayDiableIcon" type="checkbox" ng-model="program.disableOverlay">
|
||||
<label class="small" for="overlayDisableIcon" style="margin-bottom: 4px;"> Disable overlay icon when playing filler </label>
|
||||
</div>
|
||||
<hr />
|
||||
<h6>Filler Lists</h6>
|
||||
<div id='fillerContainer'>
|
||||
<br />
|
||||
|
||||
<div class="form-row" ng-repeat = "x in program.filler" track-by = "$index">
|
||||
<div class='form-group col-md-5'>
|
||||
<label ng-if="$index==0" for="fillerselect{{$index}}">List</label>
|
||||
<select
|
||||
id="fillerselect{{$index}}" class="custom-select form-control"
|
||||
ng-model="x.id" ng-options="o.id as o.name for o in x.options"
|
||||
ng-change="refreshFillerStuff()"
|
||||
>
|
||||
</select>
|
||||
</div>
|
||||
<div class='form-group col-md-2' ng-if="x.id !== 'none' " >
|
||||
<label ng-if="$index==0" for="cooldown{{$index}}">Cooldown (minutes)</label>
|
||||
<input class='form-control' id="cooldown{{$index}}" type='number' ng-model='x.cooldown' ng-pattern="/^([0-9][0-9]*)$/"
|
||||
min='0' max='1440'
|
||||
data-toggle="tooltip" data-placement="bottom" title="The channel won't pick a video from this list if it played something from this list less than this amount of minutes ago."
|
||||
> </input>
|
||||
</div>
|
||||
<div class='form-group col-md-2' ng-if="x.id === 'none' " >
|
||||
</div>
|
||||
<div class='form-group col-md-3' ng-if="x.id !== 'none' && program.filler.length > 2 " >
|
||||
<label ng-if="$index==0" for="fillerrange{{$index}}">Weight</label>
|
||||
<input class='form-control-range custom-range' id="fillerrange{{$index}}" type='range' ng-model='x.weight' min=1 max=600
|
||||
data-toggle="tooltip" data-placement="bottom" title="Lists with more weight will be picked more frequently."
|
||||
ng-change="refreshFillerStuff()"
|
||||
>
|
||||
</input>
|
||||
</div>
|
||||
<div class='form-group col-md-4' ng-if="x.id === 'none' || program.filler.length <= 2 " >
|
||||
</div>
|
||||
<div class='form-group col-md-1' ng-if="x.id !== 'none' && program.filler.length > 2" >
|
||||
<label ng-if="$index==0" for="fillerp{{$index}}">%</label>
|
||||
<input class='form-control flex-filler-percent' id="fillerp{{$index}}" type='text' ng-model='x.percentage'
|
||||
data-toggle="tooltip" data-placement="bottom" title="This is the overall probability this list might be picked, assuming all lists are available." readonly
|
||||
>
|
||||
</input>
|
||||
</div>
|
||||
<div class='form-group col-md-1' ng-if="x.id !== 'none'" >
|
||||
<label ng-if="$index==0" for="delete{{$index}}">-</label>
|
||||
<button id='delete{{$index}}' class='btn btn-link form-control' ng-click='deleteFillerList($index)' >
|
||||
<i class='text-danger fa fa-trash-alt'></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<hr></hr >
|
||||
<p class="text-center text-info">Videos from the filler list will be randomly picked to play unless there are cooldown restrictions to place or if no videos are short enough for the remaining Flex time. Use the Filler tab at the top to define filler lists. If no videos are available, the fallback will be used instead.</p>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
@ -158,6 +23,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<plex-library limit=1000000000 height="300" visible="showPlexLibrary" on-finish="importPrograms"></plex-library>
|
||||
<plex-library height="300" limit=1 visible="showFallbackPlexLibrary" on-finish="importFallback"></plex-library>
|
||||
|
||||
</div>
|
||||
Loading…
x
Reference in New Issue
Block a user