From: E.Smith <31170571+azlm8t@users.noreply.github.com>
Date: Sat, 18 Nov 2017 17:14:52 +0000 (+0000)
Subject: ui: Add category/genre/new icons and more details (#4594).
X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=a7880a660331589ae28f2860264539701372255d;p=thirdparty%2Ftvheadend.git
ui: Add category/genre/new icons and more details (#4594).
We now display a separate column indicating if a programme is
new. This column also contains icons indicating the programme type
based on the category (from xmltv) or genre (from OTA).
This uses characters rather than icons since most modern fonts
supply glyphs that we can use. We avoid using too-modern glyphs
(such as Unicode-9.0, 2016) and try to use older ones that are
likely to be in Windows 7 or later. Unfortunately there is no
easy way to determine if a particular system contains a glyph,
but most systems revert to another font if necessary.
Also added a number of new details to the dialogs for EPG and DVR
to display previously shown time and cast/credit/keyword details
for DVR upcoming.
Additionally we display "Film (yyyy)" instead of "Film" if we
have the details to help the user differentiate remakes.
Issue: #4594.
---
diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js
index f3e999e19..2bacdb20f 100644
--- a/src/webui/static/app/dvr.js
+++ b/src/webui/static/app/dvr.js
@@ -24,6 +24,12 @@ tvheadend.dvrDetails = function(uuid) {
var autorec_caption = params[12].value;
var timerec_caption = params[13].value;
var image = params[14].value;
+ var copyright_year = params[15].value;
+ var credits = params[16].value;
+ var keyword = params[17].value;
+ var category = params[18].value;
+ var first_aired = params[19].value;
+ var genre = params[20].value;
var content = '';
var but;
@@ -39,8 +45,14 @@ tvheadend.dvrDetails = function(uuid) {
if (duplicate)
content += '
';
+ var icons = tvheadend.getContentTypeIcons(event);
+ if (icons)
+ content += '
' + icons + '
';
content += '
' + event.title;
if (event.subtitle)
content += " : " + event.subtitle;
- if (event.copyrightYear)
- content += " (" + event.copyrightYear + ")";
+ if (event.copyright_year)
+ content += " (" + event.copyright_year + ")";
content += '
';
if (event.episodeOnscreen)
content += '
' + event.episodeOnscreen + '
';
@@ -115,6 +118,8 @@ tvheadend.epgDetails = function(event) {
content += '
' + _('Start Time') + ':' + tvheadend.niceDate(event.start) + '
';
if (event.stop)
content += '
' + _('End Time') + ':' + tvheadend.niceDate(event.stop) + '
';
+ if (event.first_aired)
+ content += '
' + _('First Aired') + ':' + tvheadend.niceDateYearMonth(event.first_aired, event.start) + '
';
if (duration)
content += '
' + _('Duration') + ':' + parseInt(duration / 60) + ' ' + _('min') + '
';
if (chicon) {
@@ -131,58 +136,15 @@ tvheadend.epgDetails = function(event) {
content += '
' + event.description + '
';
if (event.summary || event.description)
content += '
';
-
- // Helper function for common code to sort an array, convert to CSV and
- // return the string to add to the content.
- function sortAndAddArray(arr, title) {
- arr.sort();
- var csv = arr.join(", ");
- if (csv)
- return '
' + title + ':' + csv + '
';
- else
- return '';
- }
-
- if (event.credits) {
- // Our cast (credits) map contains details of actors, writers,
- // etc. so split in to separate categories for displaying.
- var castArr = [];
- var crewArr = [];
- var directorArr = [];
- var writerArr = [];
- var cast = ["actor", "guest", "presenter"];
- // We use arrays here in case more tags in the future map on to
- // director/writer, e.g., SchedulesDirect breaks it down in to
- // writer, writer (adaptation) writer (screenplay), etc. but
- // currently we just have them all as writer.
- var director = ["director"];
- var writer = ["writer"];
-
- for (key in event.credits) {
- var type = event.credits[key];
- if (cast.indexOf(type) != -1)
- castArr.push(key);
- else if (director.indexOf(type) != -1)
- directorArr.push(key);
- else if (writer.indexOf(type) != -1)
- writerArr.push(key);
- else
- crewArr.push(key);
- };
-
- content += sortAndAddArray(castArr, _('Starring'));
- content += sortAndAddArray(directorArr, _('Director'));
- content += sortAndAddArray(writerArr, _('Writer'));
- content += sortAndAddArray(crewArr, _('Crew'));
- }
+ content += tvheadend.getDisplayCredits(event.credits);
if (event.keyword)
- content += sortAndAddArray(event.keyword, _('Keywords'));
+ content += tvheadend.sortAndAddArray(event.keyword, _('Keywords'));
if (event.category)
- content += sortAndAddArray(event.category, _('Categories'));
+ content += tvheadend.sortAndAddArray(event.category, _('Categories'));
if (event.starRating)
- content += '
' + _('Star Rating') + ':' + event.starRating + '
';
+ content += '
' + _('Star Rating') + ':' + event.starRating + '
';
if (event.ageRating)
- content += '
' + _('Age Rating') + ':' + event.ageRating + '
';
+ content += '
' + _('Age Rating') + ':' + event.ageRating + '
';
if (event.genre) {
var genre = [];
Ext.each(event.genre, function(g) {
@@ -488,12 +450,18 @@ tvheadend.epg = function() {
type: 'date',
dateFormat: 'U' /* unix time */
},
+ {
+ name: 'first_aired',
+ type: 'date',
+ dateFormat: 'U' /* unix time */
+ },
{ name: 'starRating' },
{ name: 'credits' },
{ name: 'category' },
{ name: 'keyword' },
{ name: 'ageRating' },
- { name: 'copyrightYear' },
+ { name: 'copyright_year' },
+ { name: 'new' },
{ name: 'genre' },
{ name: 'dvrUuid' },
{ name: 'dvrState' },
@@ -590,6 +558,7 @@ tvheadend.epg = function() {
return "";
}
}),
+ tvheadend.contentTypeAction,
{
width: 250,
id: 'title',
@@ -600,6 +569,7 @@ tvheadend.epg = function() {
var clickable = tvheadend.regexEscape(record.data['title']) !=
epgStore.baseParams.title;
setMetaAttr(meta, record, value && clickable);
+ value = tvheadend.getDisplayTitle(value, record);
return !value ? '' : (clickable ? lookup : '') + value;
},
listeners: { click: { fn: clicked } }
diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css
index 15ed817b3..bcc40cb25 100644
--- a/src/webui/static/app/ext.css
+++ b/src/webui/static/app/ext.css
@@ -667,6 +667,13 @@
margin: 0;
}
+.x-epg-icons {
+ float: left;
+ font-size: 20px;
+ margin-left: 0.5em;
+ letter-spacing: 0.2em;
+}
+
.x-epg-title {
margin: 5px;
padding: 5px;
diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js
index 3c63cbae9..4691c31e1 100644
--- a/src/webui/static/app/tvheadend.js
+++ b/src/webui/static/app/tvheadend.js
@@ -43,6 +43,264 @@ tvheadend.fromCSV = function(s) {
return r;
}
+// We have "major" and "minor" mappings since we want things like
+// "Movie" to be preferred to minor elements such as "Comedy" so we
+// always end up displaying "Movie-Comedy" rather than having "Movie"
+// sometimes hidden in middle of other icons.
+//
+// Although we can insert the characters here, we use the hex
+// values because editors don't always work well with these
+// characters.
+//
+// The comments refer to the official unicode name
+//
+// These categories should _not_ be subject to internationalization
+// since many non-English xmltv providers appear to supply English
+// words for categories, presumably for compatibility with
+// mapping to a genre.
+var catmap_major = {
+ "movie" : "🎞", // Film frames
+ "news" : "📰", // Newspaper
+ "series" : "📺", // Television
+ "sports" : "🏅", // Sports medal
+};
+
+var catmap_minor = {
+ // These are taken from the frequent categories in SD and then
+ // sorted by name. They display reasonably well on a modern
+ // font.
+ "action" : "🏹", // Bow and Arrow
+ "adults only" : "🔞", // No one under eighteen symbol
+ "adventure" : "🏹", // Bow and Arrow
+ "animals" : "🐾", // Paw prints
+ "animated" : "✏️", // Pencil
+ "art" : "🎨", // Artist pallette
+ "auction" : "💸", // Money with wings
+ "auto racing" : "🏎", // Racing car
+ "auto" : "🏎", // Racing car
+ "baseball" : "⚾", // Baseball
+ "basketball" : "f3c0;", // Basketball and hoop
+ "boxing" : "🥊", // Boxing glove
+ "bus./financial" : "📈", // Chart with upwards trend
+ "children" : "👶", // Baby
+ "comedy" : "😀", // Grinning face
+ "computers" : "💻", // Personal computer
+ "community" : "👪", // Family
+ "cooking" : "🔪", // Cooking knife
+ "crime drama" : "👮", // Police officer
+ "dance" : "💃", // Dancer
+ "educational" : "🎓", // Graduation cap
+ "fantasy" : "🦄", // Unicorn face
+ "fashion" : "👠", // High heeled shoe
+ "figure skating" : "⛸", // Ice skate
+ "fishing" : "🎣", // Fishing pole and fish
+ "football" : "🏈", // American Football (not soccer)
+ "game show" : "🎲", // Game die
+ "gymnastics" : "🤸", // Person doing cartwheel
+ "history" : "🏰", // Castle
+ "holiday" : "🛫", // Airplane departure
+ "horror" : "💀", // Skull
+ "horse" : "🐴", // Horse face
+ "house/garden" : "🏡", // House with garden
+ "interview" : "💬", // Speech balloon
+ "law" : "👮", // Police officer
+ "martial arts" : "🥋", // Martial arts uniform
+ "medical" : "🚑", // Ambulance
+ "military" : "🎖", // Military medal
+ "miniseries" : "🔗", // Link symbol
+ "motorcycle" : "🏍", // Racing motorcycle
+ "music" : "🎵", // Musical note
+ "musical" : "🎵", // Musical note
+ "mystery" : "🔍", // Left pointing magnifying glass
+ "nature" : "🐘", // Elephant
+ "paranormal" : "👻", // Ghost
+ "poker" : "🂱", // Playing card ace of hearts
+ "politics" : "🗳", // Ballot box with ballot
+ "pro wrestling" : "🤼", // Wrestlers
+ "reality" : "📸", // Camera with flash
+ "religious" : "🛐", // Place of worship
+ "romance" : "❤️", // Red Heart
+ "romantic comedy" : "❤️", // Red Heart
+ "science fiction" : "👽", // Extraterrestrial alien
+ "science" : "🔬", // Microscope
+ "shopping" : "🛍", // Shopping bags
+ "sitcom": "😀", // Grinning face
+ "skiing" : "⛷", // Skier
+ "soap" : "🝔", // Alchemical symbol for soap
+ "soccer" : "⚽", // Soccer ball
+ "sports talk" : "💬", // Speech balloon
+ "spy": "🕵", // Spy
+ "standup" : "🎤", // Microphone
+ "swimming" : "🏊", // Swimmer
+ "talk" : "💬", // Speech balloon
+ "technology" : "💻", // Personal computer
+ "tennis" : "🎾", // Tennis racquet and ball
+ "theater" : "f3ad;", // Performing arts
+ "travel" : "🛫", // Airplane departure
+ "war" : "🎖", // Military medal
+ "weather" : "⛅", // Sun behind cloud
+ "weightlifting" : "🏋", // Person lifting weights
+ "western" : "🌵", // Cactus
+};
+
+// These are mappings for OTA genres
+var genre_major = {
+ // And genre major-numbers in hex
+ "10" : "📺", // Television: can't distinguish movie / tv
+ "20" : "📰", // Newspaper
+ "30" : "🎲", // Game die
+ "40" : "🏅", // Sports medal
+ "50" : "👶", // Baby
+ "60" : "🎵", // Musical note
+ "70" : "🎭", // Performing arts
+ "80" : "🗳", // Ballot box with ballot
+ "90" : "🎓", // Graduation cap
+ "a0" : "⛺", // Tent
+};
+
+var genre_minor = {
+ "11" : "🕵", // Spy
+ "12" : "🏹", // Bow and Arrow
+ "13" : "👽", // Extraterrestrial alien
+ "14" : "😀", // Grinning face
+ "15" : "🝔", // Alchemical symbol for soap
+ "16" : "❤️", // Red Heart
+ "18" : "🔞", // No one under eighteen symbol
+ "33" : "💬", // Speech balloon
+ "43" : "⚽", // Soccer ball
+ "44" : "🎾", // Tennis racquet and ball
+ "73" : "🛐", // Place of worship
+ "91" : "🐘", // Elephant
+ "a1" : "🛫", // Airplane departure
+ "a5" : "🔪", // Cooking knife
+ "a6" : "🛒", // Shopping trolley
+ "a7" : "🏡", // House with garden
+};
+
+tvheadend.uniqueArray = function(arr) {
+ var unique = [];
+ for ( var i = 0 ; i < arr.length ; ++i ) {
+ if ( unique.indexOf(arr[i]) == -1 )
+ unique.push(arr[i]);
+ }
+ return unique;
+}
+
+
+tvheadend.getContentTypeIcons = function(rec) {
+ var ret_major = [];
+ var ret_minor = [];
+ var cat = rec.category
+ if (cat && cat.length) {
+ cat.sort();
+ for ( var i = 0 ; i < cat.length ; ++i ) {
+ var v = cat[i];
+ v = v.toLowerCase();
+ var l = catmap_major[v];
+ if (l) ret_major.push(l);
+ l = catmap_minor[v];
+ if (l) ret_minor.push(l)
+ }
+ } else {
+ // Genre code
+ var gen = rec.genre;
+ if (gen) {
+ for (var i = 0; i < gen.length; ++i) {
+ var genre = parseInt(gen[i]);
+ if (genre) {
+ // Convert number to hex to make lookup easier to
+ // cross-reference with epg.c
+ var l = genre_major[(genre & 0xf0).toString(16)];
+ if (l) ret_major.push(l);
+ l = genre_minor[genre.toString(16)];
+ if (l) ret_minor.push(l)
+ }
+ }
+ }
+ }
+
+ var ret = "";
+ if (rec.new)
+ ret += "🆕"; // Squared New
+ return ret + tvheadend.uniqueArray(ret_major).join("") + tvheadend.uniqueArray(ret_minor).join("");
+}
+
+tvheadend.displayCategoryIcon = function(value, meta, record, ri, ci, store) {
+ if (value == null)
+ return '';
+ var icons = tvheadend.getContentTypeIcons(record.data);
+ if (icons.length < 1) return '';
+ return icons;
+}
+
+tvheadend.contentTypeAction = {
+ width: 75,
+ id: 'category',
+ header: _("Content Type"),
+ tooltip: _("Content Type"),
+ dataIndex: 'category',
+ renderer: tvheadend.displayCategoryIcon,
+};
+
+tvheadend.getDisplayTitle = function(title, record) {
+ if (!title) return title;
+ var year = record.data['copyright_year'];
+ if (year)
+ title += " (" + year + ")";
+ return title;
+}
+
+// Helper function for common code to sort an array, convert to CSV and
+// return the string to add to the content.
+tvheadend.sortAndAddArray = function (arr, title) {
+ arr.sort();
+ var csv = arr.join(", ");
+ if (csv)
+ return '
' + title + ':' + csv + '
';
+ else
+ return '';
+}
+
+tvheadend.getDisplayCredits = function(credits) {
+ if (!credits)
+ return "";
+ if (credits instanceof Array)
+ return "";
+
+ var content = "";
+ // Our cast (credits) map contains details of actors, writers,
+ // etc. so split in to separate categories for displaying.
+ var castArr = [];
+ var crewArr = [];
+ var directorArr = [];
+ var writerArr = [];
+ var cast = ["actor", "guest", "presenter"];
+ // We use arrays here in case more tags in the future map on to
+ // director/writer, e.g., SchedulesDirect breaks it down in to
+ // writer, writer (adaptation) writer (screenplay), etc. but
+ // currently we just have them all as writer.
+ var director = ["director"];
+ var writer = ["writer"];
+
+ for (var key in credits) {
+ var type = credits[key];
+ if (cast.indexOf(type) != -1)
+ castArr.push(key);
+ else if (director.indexOf(type) != -1)
+ directorArr.push(key);
+ else if (writer.indexOf(type) != -1)
+ writerArr.push(key);
+ else
+ crewArr.push(key);
+ };
+
+ content += tvheadend.sortAndAddArray(castArr, _('Starring'));
+ content += tvheadend.sortAndAddArray(directorArr, _('Director'));
+ content += tvheadend.sortAndAddArray(writerArr, _('Writer'));
+ content += tvheadend.sortAndAddArray(crewArr, _('Crew'));
+ return content;
+}
+
/**
* Change uilevel
*/
@@ -474,6 +732,48 @@ tvheadend.niceDate = function(dt) {
'
' + d.toLocaleTimeString() + '
';
}
+/* Date format when time is not needed, e.g., first_aired time is
+ * often 00:00. Also takes a reference date so if the dt can be made
+ * nicer such as "Previous day" then we will use that instead.
+ */
+tvheadend.niceDateYearMonth = function(dt, refdate) {
+ var d = new Date(dt);
+ // If we have a reference date then we try and make the
+ // date nicer.
+ if (refdate) {
+ var rd = new Date(refdate);
+ if (rd.getYear() == d.getYear() &&
+ rd.getMonth() == d.getMonth() &&
+ rd.getDate() == d.getDate()) {
+ var when;
+ if (rd.getHours() == d.getHours() &&
+ rd.getMinutes() == d.getMinutes()) {
+ when = _("Premiere");
+ } else {
+ when = _("Same day");
+ }
+ return '
' + when + '
';
+ } else {
+ // Determine if it is previous day. We can't just subtract
+ // timestamps since a programme on at 8pm could have
+ // a previous shown timestamp of 00:00 on previous day,
+ // so would be > 86400 seconds ago. So, create temporary
+ // dates with timestamps of 00:00 and compare those.
+ var d0 = new Date(d);
+ var rd0 = new Date(rd);
+ d0.setHours(0);
+ d0.setMinutes(0);
+ rd0.setHours(0);
+ rd0.setMinutes(0);
+ if (Math.abs(d0 - rd0) <= (24 * 60 * 60 * 1000)) {
+ return '
' + _("Previous day") + '
';
+ }
+ }
+ }
+ return '
' + d.toLocaleString(tvheadend.language, {weekday: 'long'}) + '
' +
+ '
' + d.toLocaleDateString() + '
';
+}
+
/*
*
*/