diff --git a/web/directives/channel-config.js b/web/directives/channel-config.js index 4e84352..3dc25fe 100644 --- a/web/directives/channel-config.js +++ b/web/directives/channel-config.js @@ -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 = {} diff --git a/web/directives/offline-config.js b/web/directives/offline-config.js index df6b49f..7ba04eb 100644 --- a/web/directives/offline-config.js +++ b/web/directives/offline-config.js @@ -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', + }; + } + } }; } diff --git a/web/public/style.css b/web/public/style.css index deb9333..23d50e8 100644 --- a/web/public/style.css +++ b/web/public/style.css @@ -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; } \ No newline at end of file diff --git a/web/public/templates/channel-config.html b/web/public/templates/channel-config.html index fc66297..916e533 100644 --- a/web/public/templates/channel-config.html +++ b/web/public/templates/channel-config.html @@ -76,24 +76,37 @@
-
Programs - Total: {{channel.programs.length}} - - Commercials - - - {{error.programs}} - - -
+
Programming
+ +
+ +
+ Programs: {{channel.programs.length}} +
+
+ Filler: {{channel.fillerContent.length}} +
+
+ Fallback: {{describeFallback()}} +
+
+
+ +
+ +
+ {{error.programs}} + +
+
+

Tools to modify the schedule. @@ -118,8 +131,6 @@

Sort Release Dates

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.

-
Remove Duplicates
-

Removes repeated videos.

Balance Shows

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.

@@ -130,9 +141,6 @@
Add Flex

Adds a "Flex" Time Slot. Can be configured to play a fallback screen and/or random "filler" content (e.g "commercials", 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.

-
Remove Flex
-

Removes any Flex periods from the schedule.

-
Pad Times

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.

@@ -141,6 +149,17 @@
Add Breaks

Adds Flex breaks between programs, attempting to avoid groups of consecutive programs that exceed the specified number of minutes.

+ +
Remove Duplicates
+

Removes repeated videos.

+ +
Remove Flex
+

Removes any Flex periods from the schedule.

+ +
Remove All
+

Wipes out the schedule so that you can start over.

+ +
@@ -173,14 +192,6 @@
-
-
- -
-
- -
-
@@ -235,9 +246,17 @@
- - - +
+
+ +
+
+ +
+
+ +
+
Add programs to this channel by selecting media from your Plex library
@@ -254,9 +273,8 @@
-
-
{{ dateForGuide(channel.startTime) }}
-
{{ dateForGuide(channel.programs[minProgramIndex-1].stop)}}
+
+ {{ dateForGuide(channel.startTime) }}
@@ -265,14 +283,14 @@
-
-
-
{{ dateForGuide(x.start) }}
-
{{ dateForGuide(x.stop) }}
-
-
- {{x.isOffline? channel.fillerContent.length: x.commercials.length}} +
+ +
+ {{ dateForGuide(x.start) }}
+
+
{{ x.type === 'episode' ? x.showTitle + ' - S' + x.season.toString().padStart(2, '0') + 'E' + x.episode.toString().padStart(2, '0') : x.title}}
@@ -283,18 +301,17 @@
-
-
{{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
-
{{ dateForGuide(channel.programs[channel.programs.length-1].stop) }}
+
+ {{ dateForGuide(channel.programs[minProgramIndex + 100 - 1].stop)}}
-
-
-
{{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
+
+
+ {{ dateForGuide(channel.programs[channel.programs.length-1].stop)}}
diff --git a/web/public/templates/offline-config.html b/web/public/templates/offline-config.html index 8b96a87..dce3945 100644 --- a/web/public/templates/offline-config.html +++ b/web/public/templates/offline-config.html @@ -60,9 +60,10 @@
-
+
{{durationString(x.actualDuration)}}
+
Fallback: {{x.title}}
@@ -74,7 +75,7 @@
-
+
@@ -92,24 +93,49 @@

-
- - Filler Clips: {{program.filler.length}} - - +
+
+ Filler Clips: {{program.filler.length}} +
+
+
+ +
+
+ +
- +
+
+
+ +
+
+ +
+
+ +
+
+
+

Click the to import filler content from your Plex server(s).

-
+
{{durationString(x.actualDuration)}}
+
{{x.title}}
diff --git a/web/public/templates/program-config.html b/web/public/templates/program-config.html index bb5610a..3577ea7 100644 --- a/web/public/templates/program-config.html +++ b/web/public/templates/program-config.html @@ -76,32 +76,6 @@
-
-
Commercials - -
-
-

Click the to import "commercials" from your Plex server(s).

-
-
-
- {{x.title}} -
- - Position
- {{x.commercialPosition===0?'START':x.commercialPosition=== 1?'1/4':x.commercialPosition===2?'1/2':x.commercialPosition===3?'3/4':'END'}} -
- - - - -
- -
-
-