diff --git a/dashboard-ui/bower_components/iron-meta/.bower.json b/dashboard-ui/bower_components/iron-meta/.bower.json
index 8119ebcf41..9e650790be 100644
--- a/dashboard-ui/bower_components/iron-meta/.bower.json
+++ b/dashboard-ui/bower_components/iron-meta/.bower.json
@@ -25,14 +25,14 @@
"web-component-tester": "*",
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.7.0"
},
- "homepage": "https://github.com/polymerelements/iron-meta",
+ "homepage": "https://github.com/PolymerElements/iron-meta",
"_release": "1.0.3",
"_resolution": {
"type": "version",
"tag": "v1.0.3",
"commit": "91529259262b0d8f33fed44bc3fd47aedf35cb04"
},
- "_source": "git://github.com/polymerelements/iron-meta.git",
+ "_source": "git://github.com/PolymerElements/iron-meta.git",
"_target": "^1.0.0",
- "_originalSource": "polymerelements/iron-meta"
+ "_originalSource": "PolymerElements/iron-meta"
}
\ No newline at end of file
diff --git a/dashboard-ui/cordova/chromecast.js b/dashboard-ui/cordova/chromecast.js
index 0c7807bcfc..d1f14013d3 100644
--- a/dashboard-ui/cordova/chromecast.js
+++ b/dashboard-ui/cordova/chromecast.js
@@ -282,7 +282,8 @@
"SetAudioStreamIndex",
"SetSubtitleStreamIndex",
"DisplayContent",
- "SetRepeatMode"
+ "SetRepeatMode",
+ "EndSession"
];
return target;
@@ -664,6 +665,17 @@
}
};
+ self.endSession = function () {
+
+ if (currentDevice) {
+ currentDevice.disconnect();
+ }
+
+ cleanupSession();
+ currentDevice = null;
+ currentDeviceId = null;
+ };
+
$(MediaController).on('playerchange', function (e, newPlayer, newTarget) {
if (newTarget.id != currentDeviceId) {
@@ -675,7 +687,6 @@
cleanupSession();
currentDevice = null;
currentDeviceId = null;
- self.lastPlayerData = {};
}
}
});
diff --git a/dashboard-ui/css/librarybrowser.css b/dashboard-ui/css/librarybrowser.css
index 74daf517b8..e8933e399c 100644
--- a/dashboard-ui/css/librarybrowser.css
+++ b/dashboard-ui/css/librarybrowser.css
@@ -421,6 +421,10 @@ span.itemCommunityRating:not(:empty) + .userDataIcons {
margin-top: 0 !important;
}
+.backdropPage .noBackdrop {
+ background-color: transparent;
+}
+
.itemBackdropContent {
position: absolute;
bottom: 0;
diff --git a/dashboard-ui/scripts/chromecast.js b/dashboard-ui/scripts/chromecast.js
index 1cca91a594..4f4e688400 100644
--- a/dashboard-ui/scripts/chromecast.js
+++ b/dashboard-ui/scripts/chromecast.js
@@ -705,7 +705,8 @@
"SetAudioStreamIndex",
"SetSubtitleStreamIndex",
"DisplayContent",
- "SetRepeatMode"]
+ "SetRepeatMode",
+ "EndSession"]
};
};
@@ -778,6 +779,11 @@
});
};
+ self.endSession = function () {
+
+ castPlayer.stopApp();
+ };
+
self.volumeUp = function () {
castPlayer.sendMessage({
diff --git a/dashboard-ui/scripts/itemdetailpage.js b/dashboard-ui/scripts/itemdetailpage.js
index 17736d04f5..dc3bb59e1a 100644
--- a/dashboard-ui/scripts/itemdetailpage.js
+++ b/dashboard-ui/scripts/itemdetailpage.js
@@ -68,10 +68,13 @@
renderDetails(page, item, context);
var hasBackdrop = LibraryBrowser.renderDetailPageBackdrop(page, item);
- //$('#itemBackdrop', page).addClass('noBackdrop').css('background-image', 'none');
- //Backdrops.setBackdrops(page, [item]);
+
// For these types, make the backdrop a little smaller so that the items are more quickly accessible
- if (item.Type == "Season" || item.Type == "MusicArtist" || item.Type == "MusicAlbum" || item.Type == "Series" || item.Type == "Playlist" || item.Type == "BoxSet") {
+ if (item.Type == 'MusicArtist' || item.Type == "MusicAlbum" || item.Type == "Playlist" || item.Type == "BoxSet" || item.Type == "Audio") {
+ $('#itemBackdrop', page).addClass('noBackdrop').css('background-image', 'none');
+ Backdrops.setBackdrops(page, [item]);
+ }
+ else if (item.Type == "Season" || item.Type == "Series") {
page.querySelector('#itemBackdrop').classList.add('smallBackdrop');
} else {
page.querySelector('#itemBackdrop').classList.remove('smallBackdrop');
@@ -411,7 +414,7 @@
var topOverview = page.querySelector('.topOverview');
var bottomOverview = page.querySelector('.bottomOverview');
- var seasonOnBottom = screen.availHeight < 600 || screen.availWidth < 600;
+ var seasonOnBottom = screen.availHeight < 800 || screen.availWidth < 600;
if (item.Type == 'MusicAlbum' || item.Type == 'MusicArtist' || (item.Type == 'Season' && seasonOnBottom) || (item.Type == 'Series' && seasonOnBottom)) {
LibraryBrowser.renderOverview([bottomOverview], item);
@@ -647,14 +650,20 @@
return;
}
+ var shape = item.Type == "MusicAlbum" || item.Type == "MusicArtist" ? "detailPageSquare" : "detailPagePortrait";
var screenWidth = $(window).width();
+ var screenHeight = $(window).height();
var options = {
userId: Dashboard.getCurrentUserId(),
- limit: screenWidth > 800 ? 5 : 4,
+ limit: screenWidth > 800 && shape == "detailPagePortrait" ? 5 : 4,
fields: "PrimaryImageAspectRatio,UserData,SyncInfo"
};
+ if (screenWidth >= 800 && screenHeight >= 1000) {
+ options.limit *= 2;
+ }
+
ApiClient.getSimilarItems(item.Id, options).done(function (result) {
if (!result.Items.length) {
@@ -669,7 +678,7 @@
var html = LibraryBrowser.getPosterViewHtml({
items: result.Items,
- shape: item.Type == "MusicAlbum" || item.Type == "MusicArtist" ? "detailPageSquare" : "detailPagePortrait",
+ shape: shape,
showParentTitle: item.Type == "MusicAlbum",
centerText: true,
showTitle: item.Type == "MusicAlbum" || item.Type == "Game" || item.Type == "MusicArtist",
diff --git a/dashboard-ui/scripts/mediacontroller.js b/dashboard-ui/scripts/mediacontroller.js
index 11fd603142..2f93b66c2a 100644
--- a/dashboard-ui/scripts/mediacontroller.js
+++ b/dashboard-ui/scripts/mediacontroller.js
@@ -64,119 +64,110 @@
});
}
- function getTargetsHtml(targets) {
+ function showPlayerSelection() {
var playerInfo = MediaController.getPlayerInfo();
- var html = '';
- html += '
';
+ if (t.appName && t.appName != t.name) {
+ name += " - " + t.appName;
+ }
- return html;
+ return {
+ name: name,
+ id: t.id,
+ ironIcon: 'tablet-android'
+ };
+
+ });
+
+ require(['actionsheet'], function () {
+
+ Dashboard.hideModalLoadingMsg();
+
+ ActionSheetElement.show({
+ title: Globalize.translate('HeaderSelectPlayer'),
+ items: menuItems,
+ callback: function (id) {
+
+ var target = targets.filter(function (t) {
+ return t.id == id;
+ })[0];
+
+ MediaController.trySetActivePlayer(target.playerName, target);
+
+ mirrorIfEnabled();
+ }
+ });
+ });
+ });
}
- function showPlayerSelection() {
+ function showActivePlayerMenu(playerInfo) {
- var promise = MediaController.getTargets();
+ var id = 'dlg' + new Date().getTime();
+ var html = '';
- var html = '';
+ var style = "";
- html += '
';
+ html += '
';
- html += '
';
- html += '' + Globalize.translate('ButtonRemoteControl') + '
';
+ html += '';
+ html += (playerInfo.deviceName || playerInfo.name);
+ html += '
';
+
+ html += '';
+
+ if (playerInfo.supportedCommands.indexOf('DisplayContent') != -1) {
+
+ html += '
';
+ var checkedHtml = MediaController.enableDisplayMirroring() ? ' checked' : '';
+ html += '
' + Globalize.translate('OptionEnableDisplayMirroring') + '';
+ html += '
';
+ }
html += '
';
+ html += '';
+
+ html += '';
+
$(document.body).append(html);
- require(['jqmicons']);
+ setTimeout(function () {
- var elem = $('#playerSelectionPanel').panel({}).trigger('create').panel("open").on("panelclose", function () {
+ var dlg = document.getElementById(id);
- $(this).off("panelclose").remove();
- });
+ $('.chkMirror', dlg).on('change', onMirrorChange);
- promise.done(function (targets) {
-
- $('.players', elem).html(getTargetsHtml(targets)).trigger('create');
-
- $('.chkEnableMirrorMode', elem).on('change', function () {
- MediaController.enableDisplayMirroring(this.checked);
+ dlg.open();
+ // Has to be assigned a z-index after the call to .open()
+ $(dlg).on('iron-overlay-closed', function () {
+ $(this).remove();
});
- $('.radioSelectPlayerTarget', elem).off('change').on('change', function () {
+ }, 100);
+ }
- var supportsMirror = this.getAttribute('data-mirror') == 'true';
-
- if (supportsMirror) {
- $('.fldMirrorMode', elem).show();
- } else {
- $('.fldMirrorMode', elem).hide();
- }
-
- var playerName = this.getAttribute('data-playername');
- var targetId = this.getAttribute('data-targetid');
- var targetName = this.getAttribute('data-targetname');
- var deviceName = this.getAttribute('data-deviceName');
- var playableMediaTypes = this.getAttribute('data-mediatypes').split(',');
- var supportedCommands = this.getAttribute('data-commands').split(',');
-
- MediaController.trySetActivePlayer(playerName, {
- id: targetId,
- name: targetName,
- playableMediaTypes: playableMediaTypes,
- supportedCommands: supportedCommands,
- deviceName: deviceName
-
- });
-
- mirrorIfEnabled();
-
- });
-
- if ($('.radioSelectPlayerTarget:checked', elem)[0].getAttribute('data-mirror') == 'true') {
- $('.fldMirrorMode', elem).show();
- } else {
- $('.fldMirrorMode', elem).hide();
- }
- });
+ function onMirrorChange() {
+ MediaController.enableDisplayMirroring(this.checked);
}
function bindKeys(controller) {
@@ -369,6 +360,36 @@
}
};
+ self.disconnectFromPlayer = function () {
+
+ var playerInfo = self.getPlayerInfo();
+
+ if (playerInfo.supportedCommands.indexOf('EndSession') != -1) {
+
+ var options = {
+ callback: function (result) {
+
+ if (result == 0) {
+ MediaController.getCurrentPlayer().endSession();
+ }
+
+ if (result != 2) {
+ self.setDefaultPlayerActive();
+ }
+ },
+ message: Globalize.translate('ConfirmEndPlayerSession'),
+ title: Globalize.translate('HeaderDisconnectFromPlayer'),
+ buttons: [Globalize.translate('ButtonYes'), Globalize.translate('ButtonNo'), Globalize.translate('ButtonCancel')]
+ };
+
+ Dashboard.dialog(options);
+
+ } else {
+
+ self.setDefaultPlayerActive();
+ }
+ };
+
self.getPlayers = function () {
return players;
};
diff --git a/dashboard-ui/scripts/site.js b/dashboard-ui/scripts/site.js
index ae989c75d1..2fcf6d1510 100644
--- a/dashboard-ui/scripts/site.js
+++ b/dashboard-ui/scripts/site.js
@@ -20,7 +20,6 @@ $.support.cors = true;
$(document).one('click', WebNotifications.requestPermission);
var Dashboard = {
-
jQueryMobileInit: function () {
// Page
@@ -96,7 +95,7 @@ var Dashboard = {
var url = getWindowUrl().toLowerCase();
return url.indexOf('mediabrowser.tv') != -1 ||
- url.indexOf('emby.media') != -1;
+ url.indexOf('emby.media') != -1;
},
isRunningInCordova: function () {
@@ -244,8 +243,7 @@ var Dashboard = {
if (document.createStyleSheet) {
document.createStyleSheet(url);
- }
- else {
+ } else {
var link = document.createElement('link');
link.setAttribute('rel', 'stylesheet');
link.setAttribute('type', 'text/css');
@@ -599,6 +597,75 @@ var Dashboard = {
}
},
+ dialog: function (options) {
+
+ var title = options.title;
+ var message = options.message;
+ var buttons = options.buttons;
+ var callback = options.callback;
+
+ // Cordova
+ if (navigator.notification && navigator.notification.confirm && message.indexOf('<') == -1) {
+
+ navigator.notification.confirm(message, function (index) {
+
+ callback(index);
+
+ }, title, buttons.join(','));
+
+ } else {
+ Dashboard.dialogInternal(message, title, buttons, callback);
+ }
+ },
+
+ dialogInternal: function (message, title, buttons, callback) {
+
+ var id = 'paperdlg' + new Date().getTime();
+
+ var html = '
';
+ html += '' + title + '
';
+ html += '' + message + '
';
+ html += '';
+ html += '';
+
+ $(document.body).append(html);
+
+ // This timeout is obviously messy but it's unclear how to determine when the webcomponent is ready for use
+ // element onload never fires
+ setTimeout(function () {
+
+ var dlg = document.getElementById(id);
+
+ $('.dialogButton', dlg).on('click', function () {
+
+ if (callback) {
+ callback(parseInt(this.getAttribute('data-index')));
+ }
+
+ });
+
+ // Has to be assigned a z-index after the call to .open()
+ $(dlg).on('iron-overlay-closed', function (e) {
+
+ this.parentNode.removeChild(this);
+ });
+
+ dlg.open();
+
+ }, 300);
+ },
+
confirm: function (message, title, callback) {
// Cordova
@@ -619,46 +686,43 @@ var Dashboard = {
confirmInternal: function (message, title, showCancel, callback) {
- require(['paperbuttonstyle'], function () {
+ var id = 'paperdlg' + new Date().getTime();
- var id = 'paperdlg' + new Date().getTime();
+ var html = '
';
+ html += '' + title + '
';
+ html += '' + message + '
';
+ html += '';
- html += '';
+ $(document.body).append(html);
- $(document.body).append(html);
+ // This timeout is obviously messy but it's unclear how to determine when the webcomponent is ready for use
+ // element onload never fires
+ setTimeout(function () {
- // This timeout is obviously messy but it's unclear how to determine when the webcomponent is ready for use
- // element onload never fires
- setTimeout(function () {
+ var dlg = document.getElementById(id);
- var dlg = document.getElementById(id);
+ // Has to be assigned a z-index after the call to .open()
+ $(dlg).on('iron-overlay-closed', function (e) {
+ var confirmed = this.closingReason.confirmed;
+ this.parentNode.removeChild(this);
- // Has to be assigned a z-index after the call to .open()
- $(dlg).on('iron-overlay-closed', function (e) {
- var confirmed = this.closingReason.confirmed;
- this.parentNode.removeChild(this);
+ if (callback) {
+ callback(confirmed);
+ }
+ });
- if (callback) {
- callback(confirmed);
- }
- });
+ dlg.open();
- dlg.open();
-
- }, 300);
- });
+ }, 300);
},
refreshSystemInfoFromServer: function () {
diff --git a/dashboard-ui/strings/javascript/en-US.json b/dashboard-ui/strings/javascript/en-US.json
index 98884da779..184aa55fd6 100644
--- a/dashboard-ui/strings/javascript/en-US.json
+++ b/dashboard-ui/strings/javascript/en-US.json
@@ -860,5 +860,10 @@
"OptionBudget": "Budget",
"ForAdditionalLiveTvOptions": "For additional Live TV providers, click on the External Services tab to see the available options.",
"ButtonGuide": "Guide",
- "ButtonRecordedTv": "Recorded TV"
+ "ButtonRecordedTv": "Recorded TV",
+ "HeaderDisconnectFromPlayer": "Disconnect from Player",
+ "ConfirmEndPlayerSession": "Would you like to shutdown the app on the remote device?",
+ "ButtonDisconnect": "Disconnect",
+ "ButtonYes": "Yes",
+ "ButtonNo": "No"
}
\ No newline at end of file
diff --git a/dashboard-ui/strings/javascript/javascript.json b/dashboard-ui/strings/javascript/javascript.json
index cb642d3982..3da851aea3 100644
--- a/dashboard-ui/strings/javascript/javascript.json
+++ b/dashboard-ui/strings/javascript/javascript.json
@@ -421,7 +421,7 @@
"ButtonScenes": "Scenes",
"ButtonQuality": "Quality",
"HeaderNotifications": "Notifications",
- "HeaderSelectPlayer": "Select Player:",
+ "HeaderSelectPlayer": "Select Player",
"ButtonSelect": "Select",
"ButtonNew": "New",
"MessageInternetExplorerWebm": "For best results with Internet Explorer please install the WebM playback plugin.",
@@ -860,6 +860,7 @@
"OptionDatePlayed": "Date Played",
"OptionDateAdded": "Date Added",
"OptionPlayCount": "Play Count",
+ "ButtonDisconnect": "Disconnect",
"ButtonSort": "Sort",
"ButtonMenu": "Menu",
"ButtonFilter": "Filter",
@@ -870,5 +871,9 @@
"OptionBudget": "Budget",
"ForAdditionalLiveTvOptions": "For additional Live TV providers, click on the External Services tab to see the available options.",
"ButtonGuide": "Guide",
- "ButtonRecordedTv": "Recorded TV"
+ "ButtonRecordedTv": "Recorded TV",
+ "HeaderDisconnectFromPlayer": "Disconnect from Player",
+ "ConfirmEndPlayerSession": "Would you like to shutdown the app on the remote device?",
+ "ButtonYes": "Yes",
+ "ButtonNo": "No"
}
diff --git a/dashboard-ui/thirdparty/jquerymobile-1.4.5/jquery.mobile.custom.js b/dashboard-ui/thirdparty/jquerymobile-1.4.5/jquery.mobile.custom.js
index c6bc456f16..2579d48adf 100644
--- a/dashboard-ui/thirdparty/jquerymobile-1.4.5/jquery.mobile.custom.js
+++ b/dashboard-ui/thirdparty/jquerymobile-1.4.5/jquery.mobile.custom.js
@@ -307,96 +307,6 @@
timeout_id = setTimeout( poll, $.fn[ str_hashchange ].delay );
};
- // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
- // vvvvvvvvvvvvvvvvvvv REMOVE IF NOT SUPPORTING IE6/7/8 vvvvvvvvvvvvvvvvvvv
- // vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
- window.attachEvent && !window.addEventListener && !supports_onhashchange && (function(){
- // Not only do IE6/7 need the "magical" Iframe treatment, but so does IE8
- // when running in "IE7 compatibility" mode.
-
- var iframe,
- iframe_src;
-
- // When the event is bound and polling starts in IE 6/7, create a hidden
- // Iframe for history handling.
- self.start = function(){
- if ( !iframe ) {
- iframe_src = $.fn[ str_hashchange ].src;
- iframe_src = iframe_src && iframe_src + get_fragment();
-
- // Create hidden Iframe. Attempt to make Iframe as hidden as possible
- // by using techniques from http://www.paciellogroup.com/blog/?p=604.
- iframe = $('
').hide()
-
- // When Iframe has completely loaded, initialize the history and
- // start polling.
- .one( 'load', function(){
- iframe_src || history_set( get_fragment() );
- poll();
- })
-
- // Load Iframe src if specified, otherwise nothing.
- .attr( 'src', iframe_src || 'javascript:0' )
-
- // Append Iframe after the end of the body to prevent unnecessary
- // initial page scrolling (yes, this works).
- .insertAfter( 'body' )[0].contentWindow;
-
- // Whenever `document.title` changes, update the Iframe's title to
- // prettify the back/next history menu entries. Since IE sometimes
- // errors with "Unspecified error" the very first time this is set
- // (yes, very useful) wrap this with a try/catch block.
- doc.onpropertychange = function(){
- try {
- if ( event.propertyName === 'title' ) {
- iframe.document.title = doc.title;
- }
- } catch(e) {}
- };
-
- }
- };
-
- // Override the "stop" method since an IE6/7 Iframe was created. Even
- // if there are no longer any bound event handlers, the polling loop
- // is still necessary for back/next to work at all!
- self.stop = fn_retval;
-
- // Get history by looking at the hidden Iframe's location.hash.
- history_get = function() {
- return get_fragment( iframe.location.href );
- };
-
- // Set a new history item by opening and then closing the Iframe
- // document, *then* setting its location.hash. If document.domain has
- // been set, update that as well.
- history_set = function( hash, history_hash ) {
- var iframe_doc = iframe.document,
- domain = $.fn[ str_hashchange ].domain;
-
- if ( hash !== history_hash ) {
- // Update Iframe with any initial `document.title` that might be set.
- iframe_doc.title = doc.title;
-
- // Opening the Iframe's document after it has been closed is what
- // actually adds a history entry.
- iframe_doc.open();
-
- // Set document.domain for the Iframe document as well, if necessary.
- domain && iframe_doc.write( '\x3cscript>document.domain="' + domain + '"\x3c/script>' );
-
- iframe_doc.close();
-
- // Update the Iframe's hash, for great justice.
- iframe.location.hash = hash;
- }
- };
-
- })();
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
- // ^^^^^^^^^^^^^^^^^^^ REMOVE IF NOT SUPPORTING IE6/7/8 ^^^^^^^^^^^^^^^^^^^
- // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
return self;
})();
@@ -747,79 +657,6 @@ $.extend( $.expr[ ":" ], {
}
});
-// support: jQuery <1.8
-if ( !$( "
" ).outerWidth( 1 ).jquery ) {
- $.each( [ "Width", "Height" ], function( i, name ) {
- var side = name === "Width" ? [ "Left", "Right" ] : [ "Top", "Bottom" ],
- type = name.toLowerCase(),
- orig = {
- innerWidth: $.fn.innerWidth,
- innerHeight: $.fn.innerHeight,
- outerWidth: $.fn.outerWidth,
- outerHeight: $.fn.outerHeight
- };
-
- function reduce( elem, size, border, margin ) {
- $.each( side, function() {
- size -= parseFloat( $.css( elem, "padding" + this ) ) || 0;
- if ( border ) {
- size -= parseFloat( $.css( elem, "border" + this + "Width" ) ) || 0;
- }
- if ( margin ) {
- size -= parseFloat( $.css( elem, "margin" + this ) ) || 0;
- }
- });
- return size;
- }
-
- $.fn[ "inner" + name ] = function( size ) {
- if ( size === undefined ) {
- return orig[ "inner" + name ].call( this );
- }
-
- return this.each(function() {
- $( this ).css( type, reduce( this, size ) + "px" );
- });
- };
-
- $.fn[ "outer" + name] = function( size, margin ) {
- if ( typeof size !== "number" ) {
- return orig[ "outer" + name ].call( this, size );
- }
-
- return this.each(function() {
- $( this).css( type, reduce( this, size, true, margin ) + "px" );
- });
- };
- });
-}
-
-// support: jQuery <1.8
-if ( !$.fn.addBack ) {
- $.fn.addBack = function( selector ) {
- return this.add( selector == null ?
- this.prevObject : this.prevObject.filter( selector )
- );
- };
-}
-
-// support: jQuery 1.6.1, 1.6.2 (http://bugs.jquery.com/ticket/9413)
-if ( $( "" ).data( "a-b", "a" ).removeData( "a-b" ).data( "a-b" ) ) {
- $.fn.removeData = (function( removeData ) {
- return function( key ) {
- if ( arguments.length ) {
- return removeData.call( this, $.camelCase( key ) );
- } else {
- return removeData.call( this );
- }
- };
- })( $.fn.removeData );
-}
-
-
-
-
-
// deprecated
$.ui.ie = !!/msie [\w.]+/.exec( navigator.userAgent.toLowerCase() );