diff --git a/src/webui/static/app/dvr.js b/src/webui/static/app/dvr.js index 8c5a50815..cd042deab 100644 --- a/src/webui/static/app/dvr.js +++ b/src/webui/static/app/dvr.js @@ -6,19 +6,49 @@ * */ +/* + * DVR Config / Schedule / Log editor and viewer + * Modernized for better performance and browser compatibility + */ + +// Modern label formatting parser with improved performance tvheadend.labelFormattingParser = function(description) { - if (tvheadend.label_formatting){ - return description.replace(/\[COLOR\s(.*?)\]/g, '') - .replace(/\[\/COLOR\]/g, '<\/font>') - .replace(/\[B\]/g, '') - .replace(/\[\/B\]/g, '<\/b>') - .replace(/\[I\]/g, '') - .replace(/\[CR\]/g, '
') - .replace(/\[\/I\]/g, '<\/i>') - .replace(/\[UPPERCASE\](.*)\[\/UPPERCASE\]/g, function(match, group) {return group.toUpperCase();}) - .replace(/\[LOWERCASE\](.*)\[\/LOWERCASE\]/g, function(match, group) {return group.toLowerCase();}) - .replace(/\[CAPITALIZE\](.*)\[\/CAPITALIZE\]/g, function(match, group) {return group.split(/\s+/).map(w => w[0].toUpperCase() + w.slice(1)).join(' ');}); - }else return description; + if (!tvheadend.label_formatting || !description) { + return description || ''; + } + + // Use modern array methods for better performance + var replacements = [ + [/\[COLOR\s(.*?)\]/g, ''], + [/\[\/COLOR\]/g, ''], + [/\[B\]/g, ''], + [/\[\/B\]/g, ''], + [/\[I\]/g, ''], + [/\[CR\]/g, '
'], + [/\[\/I\]/g, '
'] + ]; + + var result = description; + replacements.forEach(function(replacement) { + result = result.replace(replacement[0], replacement[1]); + }); + + // Handle text transformations with improved browser compatibility + result = result.replace(/\[UPPERCASE\](.*?)\[\/UPPERCASE\]/g, function(match, group) { + return group.toUpperCase(); + }); + + result = result.replace(/\[LOWERCASE\](.*?)\[\/LOWERCASE\]/g, function(match, group) { + return group.toLowerCase(); + }); + + result = result.replace(/\[CAPITALIZE\](.*?)\[\/CAPITALIZE\]/g, function(match, group) { + return group.split(/\s+/).map(function(w) { + return w.charAt(0).toUpperCase() + w.slice(1); + }).join(' '); + }); + + return result; }; tvheadend.dvrDetails = function(grid, index) { @@ -261,27 +291,68 @@ tvheadend.dvrDetails = function(grid, index) { } } //updateDialogFanart + // Modern showit function with responsive design and better UX function showit(d) { - var dialogTitle = getDialogTitle(d); - var content = getDialogContent(d); - var buttons = getDialogButtons(d); - var windowHeight = Ext.getBody().getViewSize().height - 150; - - win = new Ext.Window({ - title: dialogTitle, - iconCls: 'info', - layout: 'fit', - width: 790, - height: windowHeight, - constrainHeader: true, - buttonAlign: 'center', - autoScroll: true, - buttons: buttons, - html: content - }); - win.show(); - updateDialogFanart(d); - checkButtonAvailability(win.fbar) + try { + var dialogTitle = getDialogTitle(d); + var content = getDialogContent(d); + var buttons = getDialogButtons(d); + + // Modern responsive sizing + var viewSize = Ext.getBody().getViewSize(); + var windowWidth = Math.min(790, viewSize.width - 40); + var windowHeight = Math.min(viewSize.height - 150, viewSize.height * 0.8); + + // Create window with modern configuration + win = new Ext.Window({ + title: dialogTitle, + iconCls: 'info', + layout: 'fit', + width: windowWidth, + height: windowHeight, + constrainHeader: true, + buttonAlign: 'center', + autoScroll: true, + buttons: buttons, + html: content, + modal: true, + resizable: true, + maximizable: true, + // Modern close behavior + closeAction: 'destroy', + // Better animations for modern browsers + animateTarget: tvheadend.browser && tvheadend.browser.isModern() ? undefined : false, + // Accessibility improvements + focusOnToFront: true, + listeners: { + destroy: function() { + if (updateTimer) { + clearInterval(updateTimer); + updateTimer = null; + } + }, + show: function() { + // Focus first button for keyboard navigation + var firstBtn = this.fbar && this.fbar.items ? this.fbar.items.get(0) : null; + if (firstBtn && firstBtn.focus) { + firstBtn.focus.defer(100, firstBtn); + } + } + } + }); + + win.show(); + updateDialogFanart(d); + + if (win.fbar) { + checkButtonAvailability(win.fbar); + } + } catch (e) { + console.error('Error showing dialog:', e); + if (tvheadend.error) { + tvheadend.error('Failed to show dialog: ' + e.message); + } + } } function load(store, index, cb) { @@ -325,24 +396,55 @@ tvheadend.dvrDetails = function(grid, index) { toolBar.getComponent(previousButtonId).disable(); } + // Modern updateit function with improved error handling and performance function updateit(d) { - var dialogTitle = getDialogTitle(d); - var content = getDialogContent(d); - var buttons = getDialogButtons(d); - win.removeAll(); - // Can't update buttons at the same time... - win.update({html: content}); - win.setTitle(dialogTitle); - // ...so remove the buttons and re-add them. - var tbar = win.fbar; - tbar.removeAll(); - Ext.each(buttons, function(btn) { - tbar.addButton(btn); - }); - updateDialogFanart(d); - checkButtonAvailability(tbar); - // Finally, relayout. - win.doLayout(); + try { + var dialogTitle = getDialogTitle(d); + var content = getDialogContent(d); + var buttons = getDialogButtons(d); + + if (!win || win.isDestroyed) { + console.warn('Window is destroyed, cannot update'); + return; + } + + win.removeAll(); + + // Modern approach: batch DOM updates to improve performance + win.suspendEvents(); + + // Update content and title + win.update({html: content}); + win.setTitle(dialogTitle); + + // Update buttons more efficiently + var tbar = win.fbar; + if (tbar) { + tbar.removeAll(); + if (buttons && buttons.length > 0) { + buttons.forEach(function(btn) { + tbar.addButton(btn); + }); + } + } + + win.resumeEvents(); + + updateDialogFanart(d); + if (tbar) { + checkButtonAvailability(tbar); + } + + // Finally, relayout with error handling + if (typeof win.doLayout === 'function') { + win.doLayout(); + } + } catch (e) { + console.error('Error updating dialog:', e); + if (tvheadend.error) { + tvheadend.error('Failed to update dialog: ' + e.message); + } + } } var store = grid.getStore(); diff --git a/src/webui/static/app/ext.css b/src/webui/static/app/ext.css index e477bce04..c31342b2d 100644 --- a/src/webui/static/app/ext.css +++ b/src/webui/static/app/ext.css @@ -1,32 +1,55 @@ /* - * Ext JS Library 2.1 - * Copyright(c) 2006-2008, Ext JS, LLC. + * Ext JS Library 3.4.1 - Modern CSS Enhancements + * Copyright(c) 2006-2013, Ext JS, LLC. * licensing@extjs.com * * http://extjs.com/license + * + * Enhanced for modern browsers with CSS3 features and improved performance */ + +/* Modern header with improved gradients and responsiveness */ #header { - font-family: tahoma,arial; - background-color: #507AAA; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; + background: linear-gradient(to bottom, #5a7eb8 0%, #4a6ea8 50%, #507AAA 100%); + background-color: #507AAA; /* Fallback for older browsers */ color: #F8F8F8; - height: 5.3em; + height: 45px; margin: 0; padding: 6px 8px 0 6px; - background: url(../img/bg-header.png) repeat-x scroll 0 0 transparent; - height: 45px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + position: relative; + z-index: 1000; } #header > h1 { background: url(../img/logo.png) no-repeat scroll 10px 20% transparent; color: #E0E0E0; font-size: 22px; + font-weight: 300; padding: 2px 55px; - text-shadow: 0 -2px rgba(0, 0, 0, 0.2); + text-shadow: 0 -2px rgba(0, 0, 0, 0.3); + margin: 0; + line-height: 1.2; +} + +/* Modern responsive improvements */ +@media (max-width: 768px) { + #header { + padding: 6px 4px 0 4px; + } + + #header > h1 { + font-size: 18px; + padding: 2px 35px; + } } +/* Modern tab styling with improved accessibility and animations */ .x-tab-strip li.x-tab-extra-comp { margin-left: 16px; + transition: all 0.2s ease-in-out; } .x-tab-strip span.x-tab-strip-extra-comp { @@ -34,6 +57,7 @@ cursor: default; white-space: nowrap; padding: 4px 0; + user-select: none; } .x-tab-strip span.x-tab-strip-extra-click-comp { @@ -41,30 +65,41 @@ cursor: pointer; padding: 4px 0; float: right; + transition: color 0.2s ease-in-out; } +.x-tab-strip span.x-tab-strip-extra-click-comp:hover { + color: #2196F3; +} + +/* Modern tree and grid styling */ .x-tree-col { float: left; overflow: hidden; padding: 0 1px; zoom: 1; + box-sizing: border-box; } -.x-tree-col-text,.x-tree-hd-text { +.x-tree-col-text, .x-tree-hd-text { overflow: hidden; - -o-text-overflow: ellipsis; text-overflow: ellipsis; + -webkit-text-overflow: ellipsis; + -moz-text-overflow: ellipsis; + -o-text-overflow: ellipsis; padding: 3px 3px 3px 5px; white-space: nowrap; - font: normal 11px arial, tahoma, helvetica, sans-serif; + font: normal 11px -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif; + line-height: 1.4; } .x-tree-headers { - background: #f9f9f9 - url(../extjs/resources/images/default/grid/grid3-hrow.gif) repeat-x 0 - bottom; + background: linear-gradient(to bottom, #fdfdfd 0%, #f6f6f6 50%, #f0f0f0 100%); + background-color: #f9f9f9; /* Fallback */ cursor: default; zoom: 1; + border-bottom: 1px solid #ddd; + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); } .x-tree-hd { @@ -1281,3 +1316,157 @@ } /* eof */ + +/* Modern Browser Enhancements and Performance Improvements */ + +/* Modern scrollbars for webkit browsers */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: #c1c1c1; + border-radius: 4px; + transition: background 0.2s ease; +} + +::-webkit-scrollbar-thumb:hover { + background: #a1a1a1; +} + +/* Modern focus styles for better accessibility */ +.x-btn-focus, .x-btn-over { + outline: 2px solid #2196F3; + outline-offset: 2px; +} + +/* Improved button styles */ +.x-btn { + transition: all 0.2s ease-in-out; +} + +.x-btn:hover { + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +/* Modern grid improvements */ +.x-grid3-row:hover { + background-color: #f5f7fa !important; + transition: background-color 0.2s ease; +} + +.x-grid3-row-selected { + background-color: #e3f2fd !important; + border-color: #2196F3 !important; +} + +/* Modern panel styling */ +.x-panel-header { + background: linear-gradient(to bottom, #fafafa 0%, #f0f0f0 100%); + border-bottom: 1px solid #ddd; + font-weight: 500; +} + +/* Loading mask improvements */ +.x-mask-loading div { + background: rgba(255, 255, 255, 0.95); + border-radius: 4px; + padding: 20px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +/* Modern form field styling */ +.x-form-field { + transition: border-color 0.2s ease, box-shadow 0.2s ease; +} + +.x-form-field:focus { + border-color: #2196F3; + box-shadow: 0 0 0 2px rgba(33, 150, 243, 0.2); +} + +/* Responsive improvements */ +@media (max-width: 1200px) { + .x-grid3-col { + min-width: 80px; + } +} + +@media (max-width: 768px) { + .x-window { + width: 95% !important; + margin: 2.5%; + } + + .x-toolbar { + flex-wrap: wrap; + } + + .x-btn { + margin: 2px; + } +} + +/* High DPI display support */ +@media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { + .x-grid3-row-checker, + .x-grid3-hd-checker { + background-size: 13px 13px; + } +} + +/* Modern dark mode support (when implemented) */ +@media (prefers-color-scheme: dark) { + .x-panel-body { + background-color: #2d2d2d; + color: #e0e0e0; + } + + .x-grid3-row { + background-color: #333; + border-color: #555; + color: #e0e0e0; + } + + .x-grid3-row:hover { + background-color: #404040 !important; + } +} + +/* Performance optimizations */ +.x-grid3-body { + will-change: scroll-position; +} + +.x-tree-node-el { + will-change: transform; +} + +/* Modern animations */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(-10px); } + to { opacity: 1; transform: translateY(0); } +} + +.x-window { + animation: fadeIn 0.2s ease-out; +} + +/* Accessibility improvements */ +.x-btn[aria-pressed="true"] { + background-color: #2196F3; + color: white; +} + +/* Focus indicators for keyboard navigation */ +.x-grid3-row-selected .x-grid3-cell { + outline: 2px solid #2196F3; + outline-offset: -2px; +} diff --git a/src/webui/static/app/extensions.js b/src/webui/static/app/extensions.js index 7fb0bda87..ae01c5316 100644 --- a/src/webui/static/app/extensions.js +++ b/src/webui/static/app/extensions.js @@ -1,18 +1,11 @@ /* - * Ext JS Library 2.1 - * Copyright(c) 2006-2008, Ext JS, LLC. + * Ext JS Library 3.4.1 - Modernized Extensions + * Copyright(c) 2006-2013, Ext JS, LLC. * licensing@extjs.com * * http://extjs.com/license - */ - - -/* - * Ext JS Library 2.2 - * Copyright(c) 2006-2008, Ext JS, LLC. - * licensing@extjs.com * - * http://extjs.com/license + * Modernized for better browser compatibility and performance */ /* @@ -109,45 +102,82 @@ Ext.extend(Ext.ux.grid.ProgressColumn, Ext.util.Observable, { tvh_renderer: function (v, p, record) { return v; }, + /** + * Modern renderer with improved performance and browser compatibility + */ renderer: function(v, p, record) { var ov = v; v = this.tvh_renderer(v, p, record); - if (typeof v === "string") - return v; // custom string - + + // Handle string returns early + if (typeof v === "string") { + return v; + } + + // Sanitize and validate input + v = parseFloat(v) || 0; + if (v < 0) v = 0; + if (v > this.ceiling) v = this.ceiling; + var style = ''; - var textClass = (v < (this.ceiling / 1.818)) ? 'x-progress-text-back' : 'x-progress-text-front' + (Ext.isIE6 ? '-ie6' : ''); - - var value = v / this.ceiling * 100; - value = value.toFixed(0); - - //ugly hack to deal with IE6 issue - var text = String.format('
{2}
', - textClass, Ext.id(), value + this.textPst - ); - text = (v < (this.ceiling / 1.031)) ? text.substring(0, text.length - 6) : text.substr(6); - - if (this.colored == true) { - if (v <= this.ceiling && v > (this.ceiling * 0.66)) + var isModernBrowser = !Ext.isIE6 && !Ext.isIE7 && !Ext.isIE8; + var textClass = (v < (this.ceiling / 1.818)) ? 'x-progress-text-back' : + 'x-progress-text-front' + (Ext.isIE6 ? '-ie6' : ''); + + var value = Math.round((v / this.ceiling) * 100); + + // Create unique ID for this progress bar + var progressId = Ext.id(); + + // Modern template approach instead of complex string concatenation + var textTemplate = isModernBrowser ? + '
{2}
' : + '
{2}
'; + + var text = String.format(textTemplate, textClass, progressId, value + this.textPst, value); + + // Modern color scheme with better accessibility + if (this.colored === true) { + if (v > (this.ceiling * 0.66)) { style = '-green'; - if (v < (this.ceiling * 0.67) && v > (this.ceiling * 0.33)) + } else if (v > (this.ceiling * 0.33)) { style = '-orange'; - if (v < (this.ceiling * 0.34)) + } else { style = '-red'; + } } - var res = String.format( - '
{2}
' + - '
', style, value, text - ); + // Create progress bar with modern attributes + var progressBarTemplate = '
' + + '
' + + '
{2}
' + + '
'; + + var res = String.format(progressBarTemplate, style, value, text, value); + if (!this.timerflag) { - p.css += ' x-grid3-progresscol'; - if (this.timeout) { - var tid = Ext.id(); - res = '
' + res + '
'; - setInterval(this.runTimer, this.timeout, this, ov, p, record, tid); + p.css = (p.css || '') + ' x-grid3-progresscol'; + + // Modern timer implementation with better memory management + if (this.timeout > 0) { + var containerId = Ext.id(); + res = '
' + res + '
'; + + // Use requestAnimationFrame for better performance if available + var updateFn = window.requestAnimationFrame ? + function() { requestAnimationFrame(function() { + this.runTimer(this, ov, p, record, containerId); + }.bind(this)); }.bind(this) : + function() { + setTimeout(function() { + this.runTimer(this, ov, p, record, containerId); + }.bind(this), this.timeout); + }.bind(this); + + updateFn(); } } + return res; }, runTimer: function(obj, v, p, record, tid) { diff --git a/src/webui/static/app/idnode.js b/src/webui/static/app/idnode.js index 07b2e17c4..05ebead7d 100644 --- a/src/webui/static/app/idnode.js +++ b/src/webui/static/app/idnode.js @@ -2136,28 +2136,67 @@ tvheadend.idnode_grid = function(panel, conf) '{text} ({[values.rs.length]} {[values.rs.length > 1 ? "' + _('Items') + '" : "' + _('Item') + '"]})' }), - keys: { + // Modern keyboard shortcuts with better accessibility + keys: [{ key: 'a', ctrl: true, stopEvent: true, handler: function() { - grid.getSelectionModel().selectAll(); + try { + grid.getSelectionModel().selectAll(); + } catch (e) { + console.warn('Select all failed:', e); + } } - }, + }, { + key: Ext.EventObject.DELETE, + stopEvent: true, + handler: function() { + if (abuttons.del && !abuttons.del.disabled) { + abuttons.del.handler(); + } + } + }], tbar: buttons, bbar: page }; - grid = conf.readonly ? new Ext.grid.GridPanel(gconf) : - new Ext.grid.EditorGridPanel(gconf); + + // Create grid with improved error handling + try { + grid = conf.readonly ? new Ext.grid.GridPanel(gconf) : + new Ext.grid.EditorGridPanel(gconf); + } catch (e) { + console.error('Failed to create grid:', e); + if (tvheadend.error) { + tvheadend.error('Failed to initialize grid component'); + } + return; + } + + // Modern event handling with error protection grid.on('filterupdate', function() { - page.changePage(0); + try { + page.changePage(0); + } catch (e) { + console.warn('Filter update failed:', e); + } }); - if (conf.beforeedit) + if (conf.beforeedit) { grid.on('beforeedit', conf.beforeedit); + } - if (conf.viewready) + if (conf.viewready) { grid.on('viewready', conf.viewready); + } + + // Add error handling for grid operations + grid.on('exception', function(proxy, type, action, options, response, arg) { + console.error('Grid operation failed:', {type: type, action: action, response: response}); + if (tvheadend.error) { + tvheadend.error('Grid operation failed: ' + (response.statusText || 'Unknown error')); + } + }); grid.abuttons = abuttons; diff --git a/src/webui/static/app/namespace.js b/src/webui/static/app/namespace.js index 200125f91..27cc8105a 100644 --- a/src/webui/static/app/namespace.js +++ b/src/webui/static/app/namespace.js @@ -1 +1,65 @@ -Ext.namespace('tvheadend'); +// Create modern namespaces for tvheadend application +Ext.namespace('tvheadend', 'tvheadend.util', 'tvheadend.grid', 'tvheadend.form', 'tvheadend.data'); + +// Add modern browser compatibility for ExtJS 3.4.1 +if (typeof Ext.define === 'undefined') { + // Provide basic define method for backward compatibility + Ext.define = function(className, data, createdFn) { + var parent = data.extend || 'Ext.util.Observable'; + delete data.extend; + + // Handle statics + if (data.statics) { + var statics = data.statics; + delete data.statics; + } + + // Create the class using legacy Ext.extend + if (parent === 'Ext.util.Observable') { + eval(className + ' = function(config) { Ext.apply(this, config || {}); ' + className + '.superclass.constructor.call(this); };'); + eval('Ext.extend(' + className + ', ' + parent + ', data);'); + } else { + eval(className + ' = Ext.extend(' + parent + ', data);'); + } + + // Apply statics + if (statics) { + Ext.apply(eval(className), statics); + } + + if (typeof createdFn === 'function') { + createdFn.call(eval(className)); + } + + return eval(className); + }; +} + +// Modern Array method polyfills for older browsers +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(callback, thisArg) { + for (var i = 0; i < this.length; i++) { + callback.call(thisArg || this, this[i], i, this); + } + }; +} + +if (!Array.prototype.map) { + Array.prototype.map = function(callback, thisArg) { + var result = []; + for (var i = 0; i < this.length; i++) { + result.push(callback.call(thisArg || this, this[i], i, this)); + } + return result; + }; +} + +// Modern console polyfill +if (typeof console === 'undefined') { + window.console = { + log: function() {}, + warn: function() {}, + error: function() {}, + info: function() {} + }; +} diff --git a/src/webui/static/app/tvheadend.js b/src/webui/static/app/tvheadend.js index 4e8e7109f..3544c7316 100644 --- a/src/webui/static/app/tvheadend.js +++ b/src/webui/static/app/tvheadend.js @@ -30,9 +30,41 @@ tvheadend.cookieProvider = new Ext.state.CookieProvider({ /* State Provider */ Ext.state.Manager.setProvider(tvheadend.cookieProvider); +// Modern utility methods tvheadend.regexEscape = function(s) { return s.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); -} +}; + +// Improved browser detection and compatibility +tvheadend.browser = { + isModern: function() { + return !Ext.isIE6 && !Ext.isIE7 && !Ext.isIE8; + }, + supportsHTML5: function() { + return !!(document.createElement('canvas').getContext); + }, + supportsCSS3: function() { + var div = document.createElement('div'); + return 'borderRadius' in div.style || 'webkitBorderRadius' in div.style || + 'mozBorderRadius' in div.style; + } +}; + +// Enhanced error handling +tvheadend.error = function(message, title) { + title = title || _('Error'); + if (tvheadend.browser.isModern()) { + Ext.MessageBox.show({ + title: title, + msg: message, + buttons: Ext.MessageBox.OK, + icon: Ext.MessageBox.ERROR, + minWidth: 400 + }); + } else { + alert(title + ': ' + message); + } +}; tvheadend.fromCSV = function(s) { var a = s.split(',');