Major channel UI changes. Remove commercials. Tools for filler editor. Visible Color and Length signatures for programs and flex.

This commit is contained in:
vexorian 2020-08-09 14:58:59 -04:00
parent 451b4ede14
commit 3184306e86
6 changed files with 282 additions and 89 deletions

View File

@ -9,6 +9,7 @@ module.exports = function ($timeout, $location) {
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.hasFlex = false;
scope.showHelp = false;
scope._frequencyModified = false;
scope._frequencyMessage = "";
@ -183,7 +184,19 @@ module.exports = function ($timeout, $location) {
updateChannelDuration()
}
scope.dateForGuide = (date) => {
return date.toLocaleString();
let t = date.toLocaleTimeString(undefined, {
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
});
if (t.charCodeAt(1) == 58) {
t = "0" + t;
}
return date.toLocaleDateString(undefined,{
year: "numeric",
month: "2-digit",
day: "2-digit"
}) + " " + t;
}
scope.sortByDate = () => {
scope.removeOffline();
@ -254,6 +267,88 @@ module.exports = function ($timeout, $location) {
scope.channel.programs = tmpProgs
updateChannelDuration()
}
scope.describeFallback = () => {
if (scope.channel.offlineMode === 'pic') {
if (
(typeof(scope.channel.offlineSoundtrack) !== 'undefined')
&& (scope.channel.offlineSoundtrack.length > 0)
) {
return "pic+sound";
} else {
return "pic";
}
} else {
return "clip";
}
}
scope.programSquareStyle = (program) => {
let background ="";
if (program.isOffline) {
background = "rgb(255, 255, 255)";
} else {
let r = 0, g = 0, b = 0, r2=0, g2=0,b2=0;
let i = 0;
let angle = 45;
let w = 3;
if (program.type === 'episode') {
let h = Math.abs(scope.getHashCode(program.showTitle, false));
let h2 = Math.abs(scope.getHashCode(program.showTitle, true));
r = h % 256;
g = (h / 256) % 256;
b = (h / (256*256) ) % 256;
i = h % 360;
r2 = (h2 / (256*256) ) % 256;
g2 = (h2 / (256*256) ) % 256;
b2 = (h2 / (256*256) ) % 256;
angle = -90 + h % 180
} else {
r = 10, g = 10, b = 10;
r2 = 245, g2 = 245, b2 = 245;
angle = 45;
w = 6;
}
let rgb1 = "rgb("+ r + "," + g + "," + b +")";
let rgb2 = "rgb("+ r2 + "," + g2 + "," + b2 +")"
background = "repeating-linear-gradient( " + angle + "deg, " + rgb1 + ", " + rgb1 + " " + w + "px, " + rgb2 + " " + w + "px, " + rgb2 + " " + (w*2) + "px)";
}
let ems = Math.pow( Math.min(24*60*60*1000, program.actualDuration), 0.7 );
ems = ems / Math.pow(5*60*1000., 0.7);
ems = Math.max( 0.25 , ems);
let top = Math.max(0.0, (1.75 - ems) / 2.0) ;
if (top == 0.0) {
top = "1px";
} else {
top = top + "em";
}
return {
'width': '0.5em',
'height': ems + 'em',
'margin-right': '0.50em',
'background': background,
'border': '1px solid black',
'margin-top': top,
'margin-bottom': '1px',
};
}
scope.getHashCode = (s, rev) => {
var hash = 0;
if (s.length == 0) return hash;
let inc = 1, st = 0, e = s.length;
if (rev) {
inc = -1, st = e - 1, e = -1;
}
for (var i = st; i != e; i+= inc) {
hash = s.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
scope.nightChannel = (a, b) => {
let o =(new Date()).getTimezoneOffset() * 60 * 1000;
let m = 24*60*60*1000;
@ -650,10 +745,14 @@ module.exports = function ($timeout, $location) {
function updateChannelDuration() {
scope.showRotatedNote = false;
scope.channel.duration = 0
scope.hasFlex = false;
for (let i = 0, l = scope.channel.programs.length; i < l; i++) {
scope.channel.programs[i].start = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
scope.channel.duration += scope.channel.programs[i].duration
scope.channel.programs[i].stop = new Date(scope.channel.startTime.valueOf() + scope.channel.duration)
if (scope.channel.programs[i].isOffline) {
scope.hasFlex = true;
}
}
}
scope.error = {}

View File

@ -10,6 +10,7 @@ module.exports = function ($timeout) {
onDone: "=onDone"
},
link: function (scope, element, attrs) {
scope.showTools = false;
scope.showPlexLibrary = false;
scope.showFallbackPlexLibrary = false;
scope.finished = (prog) => {
@ -32,6 +33,28 @@ module.exports = function ($timeout) {
scope.onDone(JSON.parse(angular.toJson(prog)))
scope.program = null
}
scope.sortFillers = () => {
scope.program.filler.sort( (a,b) => { return a.actualDuration - b.actualDuration } );
}
scope.fillerRemoveAllFiller = () => {
scope.program.filler = [];
}
scope.fillerRemoveDuplicates = () => {
function getKey(p) {
return p.server.uri + "|" + p.server.accessToken + "|" + p.plexFile;
}
let seen = {};
let newFiller = [];
for (let i = 0; i < scope.program.filler.length; i++) {
let p = scope.program.filler[i];
let k = getKey(p);
if ( typeof(seen[k]) === 'undefined') {
seen[k] = true;
newFiller.push(p);
}
}
scope.program.filler = newFiller;
}
scope.importPrograms = (selectedPrograms) => {
for (let i = 0, l = selectedPrograms.length; i < l; i++) {
selectedPrograms[i].commercials = []
@ -55,6 +78,29 @@ module.exports = function ($timeout) {
date.setSeconds( Math.floor(duration / 1000) ); // specify value for SECONDS here
return date.toISOString().substr(11, 8);
}
scope.programSquareStyle = (program, dash) => {
let background = "rgb(255, 255, 255)";
let ems = Math.pow( Math.min(60*60*1000, program.actualDuration), 0.7 );
ems = ems / Math.pow(1*60*1000., 0.7);
ems = Math.max( 0.25 , ems);
let top = Math.max(0.0, (1.75 - ems) / 2.0) ;
if (top == 0.0) {
top = "1px";
}
let solidOrDash = (dash? 'dashed' : 'solid');
return {
'width': '0.5em',
'height': ems + 'em',
'margin-right': '0.50em',
'background': background,
'border': `1px ${solidOrDash} black`,
'margin-top': top,
'margin-bottom': '1px',
};
}
}
};
}

View File

@ -17,8 +17,9 @@
}
.flex-pull-right {
margin-left: auto;
padding-right: 20px
padding-right: 0.2em
}
.list-group-item-video {
background-color: rgb(70, 70, 70);
border-top: 1px solid #daa104;
@ -61,4 +62,34 @@
display: inline-block;
text-align: center;
cursor: pointer;
}
.program-start {
margin-right: 2.5em;
display: inline-block;
vertical-align: top;
/*color: rgb(96,96,96);*/
color: #0c5c68;
font-size: 80%;
font-weight: 400;
font-family: monospace;
}
.program-row {
align-items: start;
}
.programming-counter {
white-space: nowrap;
margin-right: 1em;
font-size: 80%;
}
.programming-counter > span {
font-weight: 300;
}
.programming-counter > b {
font-weight: 400;
}
.btn-programming-tools {
padding: .25rem .5rem;
font-size: .875rem;
line-height: 1.0;
margin-right: 0.5rem;
}

View File

@ -76,24 +76,37 @@
<hr />
<div>
<h6>Programs
<span class="small">Total: {{channel.programs.length}}</span>
<span class="btn fa fa-trash" ng-click="wipeSchedule()"></span>
<span class="badge badge-dark" style="margin-left: 15px;"
ng-show="channel.programs.length !== 0">Commercials</span>
<button class="btn btn-sm btn-secondary" style="margin-left: 10px"
ng-click="showShuffleOptions = !showShuffleOptions"
ng-show="channel.programs.length !== 0">
Tools&nbsp;&nbsp;<span
class="fa {{ showShuffleOptions ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>
</button>
<span class="pull-right">
<span class="text-danger small">{{error.programs}}</span>
<button class="btn btn-sm btn-primary" ng-click="displayPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</span>
</h6>
<h6>Programming</h6>
<div class="flex-container">
<div class='programming-counter'>
<span class="small"><b>Programs:</b> {{channel.programs.length}}</span>
</div>
<div class='programming-counter' ng-show='hasFlex'>
<span class="small"><b>Filler:</b> {{channel.fillerContent.length}}</span>
</div>
<div class='programming-counter' ng-show='hasFlex'>
<span class="small"><b>Fallback:</b> {{describeFallback()}}</span>
</div>
<div class='flex-pull-right' />
<div>
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="showShuffleOptions = !showShuffleOptions"
ng-show="channel.programs.length !== 0">
<span
class="fa {{ showShuffleOptions ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div style='margin-left:0'>
<span class="text-danger small">{{error.programs}}</span>
<button class="btn btn-sm btn-primary" ng-click="displayPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
<div ng-init="blockCount = 1; showShuffleOptions = false" ng-show="showShuffleOptions">
<p class="text-center text-info small">
Tools to modify the schedule.
@ -118,8 +131,6 @@
<h6>Sort Release Dates</h6>
<p>Sorts everything by its release date. This will only work correctly if the release dates in Plex are correct. In case any item does not have a release date specified, it will be moved to the bottom.</p>
<h6>Remove Duplicates</h6>
<p>Removes repeated videos.</p>
<h6>Balance Shows</h6>
<p>Attempts to make the total amount of time each TV show appears in the programming as balanced as possible. This works by adding multiple copies of TV shows that have too little total time and by possibly removing duplicated episodes from TV shows that have too much total time. Note that in many situations it would be impossible to achieve perfect balance because channel duration is not infinite. Movies/Clips are treated as a single TV show. Note that this will most likely result in a larger channel and that having large channels makes some UI operations slower.</p>
@ -130,9 +141,6 @@
<h6>Add Flex</h6>
<p>Adds a &quot;Flex&quot; Time Slot. Can be configured to play a fallback screen and/or random &quot;filler&quot; content (e.g &quot;commercials&quot;, trailers, prerolls, countdowns, music videos, channel bumpers, etc.). Short Flex periods are hidden from the TV guide and are displayed as extensions to the previous program. Long Flex periods appear as the channel name in the TV guide.</p>
<h6>Remove Flex</h6>
<p>Removes any Flex periods from the schedule.</p>
<h6>Pad Times</h6>
<p>Adds Flex breaks after each TV episode or movie to ensure that the program starts at one of the allowed minute marks. For example, you can use this to ensure that all your programs start at either XX:00 times or XX:30 times. Removes any existing Flex periods before adding the new ones.</p>
@ -141,6 +149,17 @@
<h6>Add Breaks</h6>
<p>Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.</p>
<h6>Remove Duplicates</h6>
<p>Removes repeated videos.</p>
<h6>Remove Flex</h6>
<p>Removes any Flex periods from the schedule.</p>
<h6>Remove All</h6>
<p>Wipes out the schedule so that you can start over.</p>
</div>
<div class="row">
@ -173,14 +192,6 @@
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortByDate()">Sort Release Dates</button>
</div>
</div>
<div class="row">
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="removeDuplicates()">Remove Duplicates</button>
</div>
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="removeOffline()">Remove Flex</button>
</div>
</div>
<div class="row">
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="equalizeShows()">Balance Shows</button>
@ -235,9 +246,17 @@
</div>
</div>
<div class="row">
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeDuplicates()">Remove Duplicates</button>
</div>
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="removeOffline()">Remove Flex</button>
</div>
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="wipeSchedule()">Remove All</button>
</div>
</div>
</div>
<div ng-if="channel.programs.length === 0">
<div class="small">Add programs to this channel by selecting media from your Plex library</div>
@ -254,9 +273,8 @@
</div>
<input ng-show="channel.programs.length &gt; 100" type="range" ng-model="minProgramIndex" min="0" max="{{ channel.programs.length - 100 }}" />
<div ng-if="minProgramIndex &gt; 0" class="list-group-item flex-container" >
<div class="small" style="width: 180px; margin-right: 5px;">
<div class="text-success">{{ dateForGuide(channel.startTime) }}</div>
<div class="text-danger">{{ dateForGuide(channel.programs[minProgramIndex-1].stop)}}</div>
<div class="program-start">
{{ dateForGuide(channel.startTime) }}
</div>
<div style="margin-right: 5px; font-weight:ligther; text-align:center">
@ -265,14 +283,14 @@
</div>
<div ng-if="minProgramIndex &lt;= $index &amp;&amp; $index &lt; minProgramIndex+100" ng-repeat="x in channel.programs" ng-click="selectProgram($index)" dnd-list="" dnd-drop="dropFunction(index , $index, item)"
>
<div class="list-group-item flex-container" dnd-draggable="x" dnd-moved="channel.programs.splice($index, 1);" dnd-effect-allowed="move" >
<div class="small" style="width: 180px; margin-right: 5px;">
<div class="text-success">{{ dateForGuide(x.start) }}</div>
<div class="text-danger">{{ dateForGuide(x.stop) }}</div>
</div>
<div style="margin-right: 15px; text-align: center" >
<span class="badge badge-dark">{{x.isOffline? channel.fillerContent.length: x.commercials.length}}</span>
<div class="list-group-item flex-container program-row" dnd-draggable="x" dnd-moved="channel.programs.splice($index, 1);" dnd-effect-allowed="move"
>
<div class="program-start">
{{ dateForGuide(x.start) }}
</div>
<div ng-style="programSquareStyle(x)" />
<div style="margin-right: 5px;" ng-hidden="x.isOffline">
{{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}}
</div>
@ -283,18 +301,17 @@
</div>
</div>
<div ng-if="minProgramIndex &lt; channel.programs.length - 100" class="list-group-item flex-container" >
<div class="small" style="width: 180px; margin-right: 5px;">
<div class="text-success">{{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}</div>
<div class="text-danger">{{ dateForGuide(channel.programs[channel.programs.length-1].stop) }}</div>
<div class="program-start">
{{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
</div>
<div style="margin-right: 5px; font-weight:ligther; text-align:center">
&#8942;
</div>
</div>
<div class="list-group-item flex-container" >
<div class="small" style="width: 180px; margin-right: 5px;">
<div class="text-success">{{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}</div>
<div class="list-group-item flex-container" ng-if="channel.programs.length &gt; 0" >
<div class="program-start">
{{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
</div>
<div style="margin-right: 5px; font-weight:ligther; text-align:center">

View File

@ -60,9 +60,10 @@
<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="small text-success" style="margin-right: 15px; font-family:monospace">
<div class="program-start" >
{{durationString(x.actualDuration)}}
</div>
<div ng-style="programSquareStyle(x, true)" />
<div style="margin-right: 5px;">
<strong>Fallback:</strong> {{x.title}}
</div>
@ -74,7 +75,7 @@
<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 />
<hr style='margin-top:0' />
</div>
<div>
@ -92,24 +93,49 @@
<label class="small" for="overlayDisableIcon" style="margin-bottom: 4px;">&nbsp;Disable overlay icon when playing filler&nbsp;&nbsp;</label>
</div>
<hr />
<div>
<span class="small" ng-show="program.filler.length > 0">
Filler Clips: {{program.filler.length}}<span class="btn fa fa-trash" ng-click="program.filler=[]"></span>
</span>
<button class="btn btn-sm btn-primary pull-right" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
<div class="flex-container">
<div class="programming-counter small" ng-show="program.filler.length > 0">
<span class="small"><b>Filler Clips:</b> {{program.filler.length}}</span>
</div>
<div class='flex-pull-right' />
<div>
<button class="btn btn-sm btn-secondary btn-programming-tools"
ng-click="showTools = !showTools"
ng-show="channel.programs.length !== 0">
<span
class="fa {{ showTools ? 'fa-chevron-down' : 'fa-chevron-right'}}"></span>&nbsp;&nbsp;Tools
</button>
</div>
<div>
<button class="btn btn-sm btn-primary" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</div>
</div>
<div ng-show="showTools">
<div class="row">
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-warning form-control form-control-sm" type="button" ng-click="sortFillers()">Sort Lengths</button>
</div>
<div class="input-group col-md-3" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveDuplicates()">Remove Duplicates</button>
</div>
<div class="input-group col-md-6" style="padding: 5px;">
<button class="btn btn-sm btn-danger form-control form-control-sm" type="button" ng-click="fillerRemoveAllFiller()">Remove All Filler</button>
</div>
</div>
</div>
<div ng-show="program.filler.length === 0">
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import filler content from your Plex server(s).</p>
</div>
<div class="list-group list-group-root" dnd-list="program.filler">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in program.filler" dnd-draggable="x" dnd-moved="program.filler.splice($index, 1)" dnd-effect-allowed="move">
<div class="small text-success" style="margin-right: 15px; font-family:monospace">
<div class="program-start" >
{{durationString(x.actualDuration)}}
</div>
<div ng-style="programSquareStyle(x, false)" />
<div style="margin-right: 5px;">
{{x.title}}
</div>

View File

@ -76,32 +76,6 @@
</div>
</div>
</div>
<div>
<h6 style="margin-top: 10px;">Commercials
<button class="btn btn-sm btn-primary pull-right" ng-click="showPlexLibrary = true">
<span class="fa fa-plus"></span>
</button>
</h6>
<div ng-show="program.commercials.length === 0">
<p class="text-center text-info">Click the <span class="fa fa-plus"></span> to import "commercials" from your Plex server(s).</p>
</div>
<div class="list-group list-group-root" dnd-list="program.commercials">
<div class="list-group-item flex-container" style="cursor: default;" ng-repeat="x in program.commercials" dnd-draggable="x" dnd-moved="program.commercials.splice($index, 1)" dnd-effect-allowed="move">
{{x.title}}
<div class="flex-pull-right">
<span class="small" style="display: inline-block;">
<b>Position</b><br/>
{{x.commercialPosition===0?'START':x.commercialPosition=== 1?'1/4':x.commercialPosition===2?'1/2':x.commercialPosition===3?'3/4':'END'}}
</span>
<span style="padding-top: 10px; display: inline-block;">
<input type="range" min="0" max="4" ng-model="x.commercialPosition"/>
</span>
<span class="btn fa fa-trash" ng-click="program.commercials.splice($index,1)"></span>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-link" ng-click="program = null">Cancel</button>