f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=[''],h=0;d>h;h++){g.push("");for(var i=0;d>i;i++)g.push(' | ');g.push("
")}g.push("
"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
\ No newline at end of file
diff --git a/modules-available/locationinfo/frontend/upcoming.js b/modules-available/locationinfo/frontend/upcoming.js
new file mode 100644
index 00000000..af918de4
--- /dev/null
+++ b/modules-available/locationinfo/frontend/upcoming.js
@@ -0,0 +1,248 @@
+/** Next time we need to query the calendar */
+var nextCalendarUpdate = 0;
+/** Next time we need to redraw the list because the active item changed */
+var nextRender = 0;
+var calendars = {};
+var locations = {};
+var DATENOW, NOW;
+var globalTimer;
+
+$(document).ready(function () {
+ if (!globalConfig || globalConfig.constructor !== Object) {
+ fatalError("Embedded panel config is not valid JSON");
+ return;
+ }
+ initializePanel();
+});
+
+function initializePanel() {
+ if (!globalConfig.locations || globalConfig.locations.constructor !== Array) {
+ fatalError("Requested panel doesn't contain locations / not array");
+ return;
+ }
+
+ var locs = 0;
+ globalConfig.locations.forEach(function (x) {
+ // filter out if no numeric id, or id already present
+ if (typeof(x.id) !== 'number' || x.id <= 0 || locations[x.id])
+ return;
+ locs++;
+ locations[x.id] = x;
+ });
+
+ if (locs === 0) {
+ fatalError("List of location ids is empty");
+ return;
+ }
+
+ if (globalConfig.time) {
+ SetUpDate(globalConfig.time);
+ }
+ delete globalConfig.time;
+ delete globalConfig.locations;
+
+ sanitizeAndOverrideConfig(globalConfig);
+ if (!globalTimer) {
+ mainUpdateLoop();
+ if (globalConfig.qrcode) {
+ makeQrCode();
+ }
+ }
+}
+
+/**
+ * gets Additional Parameters from the URL, and from the
+ * downloaded json.
+ * also makes sure parameters are in a given range
+ */
+function sanitizeAndOverrideConfig(config) {
+ sanitizeConfigParam(config, 'calupdate', PARAM_INT, 30, 1, 60, 60 * 1000);
+ sanitizeConfigParam(config, 'qrcode', PARAM_BOOL, false);
+ sanitizeConfigParam(config, 'language', PARAM_STRING, 'en');
+}
+
+function updateNow() {
+ DATENOW = MyDate();
+ NOW = DATENOW.getTime();
+}
+
+/**
+ * queries the Calendar data
+ */
+function queryCalendars() {
+ console.log('Querying current calendar data');
+ if (!PANEL_UUID) return;
+ var url = API + "get=calendar&uuid=" + PANEL_UUID;
+ $.ajax({
+ url: url,
+ dataType: 'json',
+ cache: false,
+ timeout: 30000,
+ success: queryCalendarsCallback,
+ error: function () {
+ // Retry in 5 minutes (300 seconds)
+ updateNow();
+ nextCalendarUpdate = NOW + 300000;
+ }
+ });
+}
+
+var color;
+
+function queryCalendarsCallback(result) {
+ if (result && result.constructor === Array) {
+ var l = result.length;
+ var timeTable = {};
+ color = 'rgb(' + Math.round(Math.random() * 128 + 127) + ', ' + Math.round(Math.random() * 128 + 127) + ', ' + Math.round(Math.random() * 128 + 127) + ')';
+ updateNow();
+ for (var i = 0; i < l; i++) {
+ timeTable[result[i].id] = processSingleCalendar(result[i].calendar);
+ }
+ calendars = mergeCalendars(timeTable);
+ renderUnifiedEvents();
+ }
+}
+
+const SEVEN_DAYS = 7 * 86400 * 1000;
+
+/**
+ * Calendar data will be filtered and sorted.
+ * Expired events will be removed, remaining events sorted by start time, up to 7 days in advance.
+ *
+ * @param {Array} json - The new event data in JSON format to update the calendar.
+ */
+function processSingleCalendar(json) {
+ if (!json || json.constructor !== Array) {
+ console.log("Error: Calendar data was empty or malformed.");
+ return [];
+ }
+ if (json.length === 0) {
+ console.log("Notice: Calendar already empty from server");
+ }
+ json = json.filter(function (el) {
+ if (!el.title || !el.start || !el.end) return false;
+ el.s = new Date(el.start).getTime();
+ el.e = new Date(el.end).getTime();
+ return !(isNaN(el.s) || isNaN(el.e) || Math.abs(el.s - NOW) > SEVEN_DAYS || Math.abs(el.e) < NOW);
+ });
+ if (json.length === 0) {
+ console.log('Notice: Calendar has no current events');
+ }
+ json.sort(function (a, b) { return a.s - b.s; });
+ for (var i = json.length - 1; i > 0; i--) {
+ // if title, start and end are the same, "merge" two events by removing one of them
+ if (json[i].title === json[i-1].title && json[i].start === json[i-1].start && json[i].end === json[i-1].end) {
+ json.splice(i, 1);
+ }
+ }
+ return json;
+}
+
+/**
+ * Takes one or more calendars and merges the individual eventsfrom them, sorted ascending
+ * by start time. The inputcalendars are assumed to be sorted in ascending order already.
+ * If multiple calendars contain an event with matching start/end times and title, it is
+ * assumed to be the same event spread across multiple locations, so the event will be
+ * merged, and the location names concatenated.
+ * @param {Array} calendars - An array of calendars to be merged.
+ * @returns {Array} - Returns a new array containing the merged calendars.
+ */
+function mergeCalendars(calendars) {
+ var k, tt, merged = [];
+ var nextRefresh = null;
+ do {
+ var smallest = false;
+ for (k in calendars) {
+ if (!calendars.hasOwnProperty(k))
+ continue;
+ tt = calendars[k];
+ if (!tt || tt.length === 0 || tt[0].e < NOW)
+ continue;
+ if (smallest === false || tt[0].s < calendars[smallest][0].s) {
+ smallest = k;
+ }
+ }
+ if (smallest) {
+ var v = calendars[smallest].shift();
+ v.location = locations[smallest] ? locations[smallest].name : '???';
+ if (merged.length !== 0) {
+ var cur = merged.pop();
+ if (cur.s === v.s && cur.e === v.e && cur.title === v.title) {
+ v.location += ', ' + cur.location;
+ } else {
+ merged.push(cur);
+ }
+ }
+ merged.push(v);
+ }
+ } while (smallest);
+ return merged;
+}
+
+function pad(number, digits) {
+ var s = "00" + number;
+ return s.substr(s.length - digits);
+}
+
+function formatTime(dateObj) {
+ return pad(dateObj.getHours(), 2) + ":" + pad(dateObj.getMinutes(), 2);
+}
+
+function makeQrCode() {
+ var script = document.createElement('script');
+ script.type = 'text/javascript';
+ script.src = 'modules/locationinfo/frontend/qrcode.min.js';
+
+ console.log("MakeQR");
+ var callback = function() {
+ console.log("Script cb");
+ var $code = $('');
+ $('body').append($code);
+ setTimeout(function() {
+ new QRCode($code[0], {
+ text: window.location.href.replace(/([#&])qrcode=\d+/, '$1').replace(/[]+$/, ''),
+ width: $code.width() * 4,
+ height: $code.height() * 4,
+ correctLevel : QRCode.CorrectLevel.L
+ });
+ }, 1);
+ }
+
+ script.onload = function () {
+ this.onload = null;
+ this.onreadystatechange = null;
+ callback();
+ };
+ script.onreadystatechange = function () {
+ if (this.readyState === 'loaded' || this.readyState === 'complete') {
+ this.onload = null;
+ this.onreadystatechange = null;
+ callback();
+ }
+ };
+
+ document.getElementsByTagName('head')[0].appendChild(script);
+}
+
+//
+
+/**
+ * Main Update loop, this loop runs every 10 seconds
+ */
+function mainUpdateLoop() {
+ updateNow();
+ if (nextCalendarUpdate < NOW) {
+ nextCalendarUpdate = NOW + globalConfig.calupdate;
+ queryCalendars();
+ } else if (nextRender < NOW) {
+ renderUnifiedEvents();
+ } else {
+ queryPanelChange(API, PANEL_UUID, globalConfig);
+ }
+ var nx = Math.min(nextCalendarUpdate, nextRender) - NOW;
+ nx = Math.max(1000, nx);
+ nx = Math.min(15000, nx);
+ $('#time .date-today').text(t('long' + DATENOW.getDay()) + ', ' + DATENOW.toLocaleDateString(LANGUAGE));
+ $('#time .time-today').text(formatTime(DATENOW));
+ globalTimer = setTimeout(mainUpdateLoop, nx);
+}
diff --git a/modules-available/locationinfo/hooks/translation.inc.php b/modules-available/locationinfo/hooks/translation.inc.php
index e83dfd2d..0b58c0e9 100644
--- a/modules-available/locationinfo/hooks/translation.inc.php
+++ b/modules-available/locationinfo/hooks/translation.inc.php
@@ -21,4 +21,22 @@ if (Module::isAvailable('locationinfo')) {
return $return;
};
}
+ $HANDLER['subsections'][] = 'panel-params';
+ $HANDLER['grep_panel-params'] = function($module) {
+ $return = [];
+ foreach (['UPCOMING'] as $panelType) {
+ $params = LocationInfo::getPanelParameters($panelType);
+ $params['string']['panelname'] = [];
+ foreach ($params as $types) {
+ foreach ($types as $key => $data) {
+ $return[$key] = true;
+ $return[$key . '_helptext'] = true;
+ if (isset($data['section'])) {
+ $return['section_' . $data['section']] = true;
+ }
+ }
+ }
+ }
+ return $return;
+ };
}
diff --git a/modules-available/locationinfo/inc/infopanel.inc.php b/modules-available/locationinfo/inc/infopanel.inc.php
index 2557149d..2d8136c0 100644
--- a/modules-available/locationinfo/inc/infopanel.inc.php
+++ b/modules-available/locationinfo/inc/infopanel.inc.php
@@ -23,7 +23,7 @@ class InfoPanel
if ($panel['paneltype'] === 'URL') {
// Shortcut for URL redirect
- $config = json_decode($panel['panelconfig'], true) + $config;
+ $config = json_decode($panel['panelconfig'], true) + $config; // DO NOT REPLACE BY +=
return $panel['paneltype'];
}
@@ -37,7 +37,7 @@ class InfoPanel
$overrides = $json['overrides'];
}
unset($json['overrides']);
- $config = $json + $config;
+ $config = $json + $config; // DO NOT REPLACE BY +=
}
}
if (isset($config['showtitle']) && $config['showtitle']) {
@@ -53,7 +53,7 @@ class InfoPanel
}
}
}
- if ($panel['paneltype'] === 'DEFAULT') {
+ if ($panel['paneltype'] === 'DEFAULT' || $panel['paneltype'] === 'UPCOMING') {
foreach ($lids as $lid) {
$config['locations'][$lid] = array(
'id' => $lid,
@@ -64,7 +64,9 @@ class InfoPanel
$config['locations'][$lid]['config'] = $overrides[$lid];
}
}
- self::appendMachineData($config['locations'], $lids, true, $config['hostname']);
+ }
+ if ($panel['paneltype'] === 'DEFAULT') {
+ self::appendMachineData($config['locations'], $lids, true, $config['hostname'] ?? false);
}
self::appendOpeningTimes($config['locations'], $lids);
@@ -225,8 +227,6 @@ class InfoPanel
return array_map('intval', $allIds);
}
-// ########## ##########
-
/**
* Format the openingtime in the frontend needed format.
* One key per week day, which contains an array of {
diff --git a/modules-available/locationinfo/inc/locationinfo.inc.php b/modules-available/locationinfo/inc/locationinfo.inc.php
index b41f3a33..831f5ad0 100644
--- a/modules-available/locationinfo/inc/locationinfo.inc.php
+++ b/modules-available/locationinfo/inc/locationinfo.inc.php
@@ -83,6 +83,73 @@ class LocationInfo
}
}
+ public static function getPanelParameters(string $type): array
+ {
+ $mods = [];
+ $lowType = strtolower($type);
+ foreach (glob('modules/locationinfo/templates/frontend-' . $lowType . '-*.html') as $file) {
+ if (!preg_match('/frontend-' . $lowType . '-([a-z0-9]+)\.html$/', $file, $matches))
+ continue;
+ $mods[] = $matches[1];
+ }
+ if (!empty($mods)) {
+ array_unshift($mods, '');
+ }
+ if ($type === 'UPCOMING') {
+ return [
+ 'string' => [
+ 'language' => [
+ 'section' => 'general',
+ 'default' => defined('LANG') ? LANG : 'en',
+ 'enum' => Dictionary::getLanguages(),
+ ],
+ 'heading' => ['section' => 'general'],
+ 'subheading' => ['section' => 'general'],
+ 'color_bg' => ['section' => 'styling', 'default' => 'rgb(52, 73, 154)'],
+ 'color_fg' => ['section' => 'styling', 'default' => '#fff'],
+ 'color_section' => ['section' => 'styling', 'default' => 'rgba(255,255,255, .2)'],
+ 'color_time' => ['section' => 'styling', 'default' => 'rgba(255,255,255, .5)'],
+ 'mod' => ['section' => 'general', 'enum' => $mods],
+ ],
+ 'bool' => [
+ 'qrcode' => ['section' => 'general'],
+ 'language-system' => ['section' => 'general'],
+ ]
+ ];
+ }
+ ErrorHandler::traceError("Invalid panel type: '$type'");
+ }
+
+ /**
+ * Takes a configuration for a specific panel type, and creates additional keys for all
+ * known boolean options that are set in the config, after the following pattern
+ * ```
+ * 'somekey' => true
+ * creates:
+ * 'somekey_checked' => 'checked'
+ * ```
+ * so that it can be used in templates more easily.
+ */
+ public static function makeCheckedProperties(string $paneltype, array &$config): void
+ {
+ $params = self::getPanelParameters($paneltype);
+ if (empty($params['bool']))
+ return;
+ Render::mangleFields($config, array_keys($params['bool']));
+ }
+
+ /**
+ * Read panel config from POST parameters for given panel type.
+ * @param string $type Type to process POST parameters for
+ * @return array mangled and sanitized config
+ */
+ public static function panelConfigFromPost(string $type): array
+ {
+ $conf = [];
+ Request::processPostParameters($conf, self::getPanelParameters($type));
+ return $conf;
+ }
+
/**
* Creates and returns a default config for room that didn't save a config yet.
*
@@ -135,7 +202,81 @@ class LocationInfo
'zoom-factor' => 100,
);
}
- return array();
+ if ($type === 'UPCOMING') {
+ $p = self::getPanelParameters($type);
+ $out = [];
+ foreach ($p as $sub) {
+ foreach ($sub as $field => $options) {
+ $out[$field] = $options['default'] ?? $options['min']
+ ?? (isset($options['enum']) ? array_shift($options['enum']) : '');
+ }
+ }
+ return $out;
+ }
+ ErrorHandler::traceError("Invalid panel type: '$type'");
+ }
+
+ public static function getEditTemplateData(string $type, array $current): array
+ {
+ $params = self::getPanelParameters($type);
+ $params['string'] = array_reverse($params['string'], true);
+ $params['string']['panelname'] = [];
+ $params['string'] = array_reverse($params['string'], true);
+ $sections = [];
+ foreach ($params as $varType => $options) {
+ foreach ($options as $key => $properties) {
+ if (isset($properties['enum']) && empty($properties['enum']))
+ continue; // Enum with no values - ignore
+ $section = ($properties['section'] ?? 'general');
+ if (!isset($sections[$section])) {
+ $sections[$section] = [];
+ }
+ $sections[$section][] = [
+ 'title' => Dictionary::translateFile('panel-params', $key, true),
+ 'helptext' => Dictionary::translateFile('panel-params', $key . '_helptext', true),
+ 'html' => self::makeEditField($varType, $key, $properties, $current[$key] ?? ''),
+ ];
+ }
+ }
+ $output = [];
+ foreach ($sections as $sectionName => $sectionData) {
+ $output[] = [
+ 'title' => Dictionary::translateFile('panel-params', 'section_' . $sectionName, true),
+ 'elements' => $sectionData,
+ ];
+ }
+ return $output;
+ }
+
+ private static function makeEditField(string $varType, string $key, array $var, $current): string
+ {
+ if (isset($var['enum'])) {
+ $return = '';
+ } elseif ($varType === 'int') {
+ $return = '
';
+ } elseif ($varType === 'string') {
+ $return = '';
+ } else {
+ ErrorHandler::traceError("Invalid variable type: '$varType'");
+ }
+ return $return;
}
/**
diff --git a/modules-available/locationinfo/install.inc.php b/modules-available/locationinfo/install.inc.php
index 42bc8234..732ea6e9 100644
--- a/modules-available/locationinfo/install.inc.php
+++ b/modules-available/locationinfo/install.inc.php
@@ -26,8 +26,8 @@ $t2 = $res[] = tableCreate('locationinfo_coursebackend', '
$t3 = $res[] = tableCreate('locationinfo_panel', "
`paneluuid` char(36) CHARACTER SET ascii NOT NULL,
`panelname` varchar(30) NOT NULL,
- `locationids` varchar(100) CHARACTER SET ascii NOT NULL,
- `paneltype` enum('DEFAULT','SUMMARY', 'URL') NOT NULL,
+ `locationids` text CHARACTER SET ascii NOT NULL,
+ `paneltype` enum('DEFAULT', 'SUMMARY', 'URL', 'UPCOMING') NOT NULL,
`panelconfig` blob NOT NULL,
`lastchange` int(10) UNSIGNED NOT NULL DEFAULT 0,
PRIMARY KEY (`paneluuid`),
@@ -80,10 +80,8 @@ if ($t1 === UPDATE_DONE) {
if ($t3 === UPDATE_NOOP) {
Database::exec("ALTER TABLE `locationinfo_panel` CHANGE `paneltype`
- `paneltype` ENUM('DEFAULT', 'SUMMARY', 'URL') CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL");
+ `paneltype` ENUM('DEFAULT', 'SUMMARY', 'URL', 'UPCOMING') NOT NULL");
// 2017-12-02 expand locationids column
- Database::exec("ALTER TABLE `locationinfo_panel` CHANGE `locationids`
- `locationids` varchar(100) CHARACTER SET ascii NOT NULL");
}
// 2017-07-26 Add servername key
@@ -105,6 +103,15 @@ if (tableGetConstraints('locationinfo_backendlog', 'serverid',
'locationinfo_coursebackend', 'serverid', 'ON UPDATE CASCADE ON DELETE CASCADE');
}
+// 2025-06-17: Make locationds text column
+if (stripos(tableColumnType('locationinfo_panel', 'locationids'), 'text') === false) {
+ if (Database::exec('ALTER TABLE `locationinfo_panel` MODIFY `locationids` text CHAR SET ascii NOT NULL')) {
+ $res[] = UPDATE_DONE;
+ } else {
+ $res[] = UPDATE_FAILED;
+ }
+}
+
// Create response for browser
if (in_array(UPDATE_RETRY, $res)) {
diff --git a/modules-available/locationinfo/lang/de/panel-params.json b/modules-available/locationinfo/lang/de/panel-params.json
new file mode 100644
index 00000000..a05a3e12
--- /dev/null
+++ b/modules-available/locationinfo/lang/de/panel-params.json
@@ -0,0 +1,26 @@
+{
+ "color_bg": "Hintergrund",
+ "color_bg_helptext": "Hintergrundfarbe des Panels",
+ "color_fg": "Vordergrundfarbe",
+ "color_fg_helptext": "Prim\u00e4re Textfarbe",
+ "color_section": "Sektion",
+ "color_section_helptext": "Textfarbe f\u00fcr \"Aktuell\"- und \"Anstehend\"-\u00dcberschrift",
+ "color_time": "Datum\/Zeit",
+ "color_time_helptext": "Textfarbe f\u00fcr die Datum-\/Zeitanzeige im Vollbild",
+ "heading": "\u00dcberschrift",
+ "heading_helptext": "Prim\u00e4re \u00dcberschrift",
+ "language": "Sprache",
+ "language-system": "Systemsprache bevorzugen",
+ "language-system_helptext": "Sofern unterst\u00fctzt, wird das Panel in der Sprache ausgeliefert, die der Browser des Users anfordert",
+ "language_helptext": "Anzeigesprache des Panels",
+ "mod": "Panelvariante",
+ "mod_helptext": "Alternatives Template f\u00fcr das Panel verwenden",
+ "panelname": "Interner Name",
+ "panelname_helptext": "Name des Panels, wie er in der Administrationsoberfl\u00e4che angezeigt wird",
+ "qrcode": "QRCode zeigen",
+ "qrcode_helptext": "QRCode in der unteren rechten Ecke anzeigen",
+ "section_general": "Allgemein",
+ "section_styling": "Design",
+ "subheading": "Zweite \u00dcberschrift",
+ "subheading_helptext": "Sekund\u00e4re \u00dcberschrift, die unter der prim\u00e4ren angezeigt wird"
+}
\ No newline at end of file
diff --git a/modules-available/locationinfo/lang/de/template-tags.json b/modules-available/locationinfo/lang/de/template-tags.json
index 579b4315..f10a4ae2 100644
--- a/modules-available/locationinfo/lang/de/template-tags.json
+++ b/modules-available/locationinfo/lang/de/template-tags.json
@@ -24,18 +24,20 @@
"lang_createPanel": "Panel anlegen",
"lang_credentials": "Anmeldung",
"lang_currentDay": "Aktueller Tag",
+ "lang_currentEvents": "Aktuell",
"lang_daysToShow": "Tage",
"lang_daysToShowTooltip": "Legt die Anzahl an Tagen im Kalender fest, die angezeigt werden",
- "lang_defaultPanel": "Standard-Panel",
+ "lang_defaultPanel": "Raum\/Kalender",
"lang_deleteConfirmation": "Sind Sie sicher?",
"lang_display": "Anzeige",
"lang_displayName": "Name",
- "lang_displayNameTooltip": "Anzeigename f\u00fcr dieses Panel",
+ "lang_displayNameTooltip": "Name f\u00fcr dieses Panel (in Auflistung)",
"lang_ecoMode": "E-Ink Modus",
"lang_ecoTooltip": "Niedrigere Aktualisierungsrate, Countdown ohne Sekunden",
"lang_editDefaultPanelHints": "Hier k\u00f6nnen Sie ein Panel (z.B. digitales T\u00fcrschild) in Aussehen und Funktionsweise definieren. Um im Kalender \u00d6ffnungszeiten anzeigen zu k\u00f6nnen, m\u00fcssen Sie im Tab \"Raum-\/Ortsbezogene Einstellungen\" f\u00fcr den ausgew\u00e4hlten Raum entsprechend \u00d6ffnungszeiten eintragen. Damit im Kalender Veranstaltungen und andere Termine angezeigt werden k\u00f6nnen, muss ein funktionierendes Backend konfiguriert und den ausgew\u00e4hlten R\u00e4umen zugewiesen worden sein.",
"lang_editPanel": "Panel bearbeiten",
"lang_editSummaryPanelHints": "Hier k\u00f6nnen Sie ein \u00dcbersichts-Panel definieren. Das Panel zeigt eine \u00dcbersicht der in den R\u00e4umen enthalten PCs.",
+ "lang_editUpcomingPanelHints": "Eine Anzeigetafel, die die n\u00e4chsten Veranstaltungen in einem oder mehreren R\u00e4umen anzeigt. Die Veranstaltungen werden chronologisch aufsteigend untereinander angezeigt, mit Start- und Endzeit.",
"lang_editUrlPanelHints": "Hier k\u00f6nnen Sie konfigurieren, welche URL das Panel aufrufen soll. Dies erm\u00f6glicht Ihnen z.B. in Eingangsbereichen aktuelle Meldungen der Hochschule oder sonstige Webseiten anzuzeigen.",
"lang_entryName": "Name",
"lang_error": "Fehler",
@@ -80,6 +82,7 @@
"lang_modeTooltip": "Die Anzeigemodi, welche das Frontend unterst\u00fctzt",
"lang_monday": "Montag",
"lang_nameTooltip": "Legt den Namen des Servers fest",
+ "lang_noCurrentEvents": "Keine aktuellen Veranstaltungen",
"lang_noLocationsWarning": "Bitte w\u00e4hlen Sie mindestens einen Ort aus, der vom Panel angezeigt werden soll.",
"lang_noServer": "",
"lang_openingTime": "\u00d6ffnungszeit",
@@ -134,15 +137,18 @@
"lang_splitloginTooltip": "Erlaube nur Gast-Login oder Gast+Nutzer-Login wenn aktiviert",
"lang_startDay": "Start Tag",
"lang_startDayTooltip": "Der Wochentag an dem der Kalender anf\u00e4ngt",
- "lang_summaryPanel": "\u00dcbersichts-Panel",
+ "lang_summaryPanel": "Gesamt\u00fcbersicht",
"lang_summaryUpdateIntervalTooltip": "Aktualisierungsintervall (Sekunden)",
"lang_sunday": "Sonntag",
"lang_switchTime": "Wechselintervall",
"lang_switchTimeTooltip": "[1-120] Legt die Zeit fest, die vergeht bis ein Wechsel erfolgt (in Sekunden)",
"lang_thuesday": "Dienstag",
"lang_thursday": "Donnerstag",
+ "lang_timeSpanSuffix": "Uhr",
"lang_to": "bis",
"lang_typeTooltip": "Legt fest um welchen Server-Typ es sich handelt",
+ "lang_upcomingEvents": "Anstehend",
+ "lang_upcomingPanel": "Anstehende Veranstaltungen",
"lang_updateRates": "Aktualisierungsintervall",
"lang_url": "URL",
"lang_urlListHelp": "Sie k\u00f6nnen hier Listen von URLs oder Hostnamen angeben, die dann als Whitelist bzw. Blacklist interpretiert werden. Je nach gew\u00e4hltem Browser hat entweder die Whitelist Vorrang, oder die \"genauere\" Regel. Wenn keine Regel zutrifft, wird der Zugriff erlaubt, es sei denn, es gibt den Eintrag \"*\" in der Blacklist. Je nach verwendetem Browser wird \"*\" als Wildcard an verschiedenen Stellen unterst\u00fctzt. Sie k\u00f6nnen Kommentare hinter oder zwischen den Zeilen mittels \"#\" einleiten.",
diff --git a/modules-available/locationinfo/lang/en/panel-params.json b/modules-available/locationinfo/lang/en/panel-params.json
new file mode 100644
index 00000000..0eff714d
--- /dev/null
+++ b/modules-available/locationinfo/lang/en/panel-params.json
@@ -0,0 +1,26 @@
+{
+ "color_bg": "Background",
+ "color_bg_helptext": "Background color of panel",
+ "color_fg": "Foreground",
+ "color_fg_helptext": "Primary text color",
+ "color_section": "Section",
+ "color_section_helptext": "Color for \"current\" and \"upcoming\" headline",
+ "color_time": "Date\/time",
+ "color_time_helptext": "Color for date\/time display in fullscreen display",
+ "heading": "Heading",
+ "heading_helptext": "Primary heading",
+ "language": "Language",
+ "language-system": "Prefer system language",
+ "language-system_helptext": "If supported, the panel will be displayed in the language the user's browser requested",
+ "language_helptext": "Language to display the panel in",
+ "mod": "Panel variant",
+ "mod_helptext": "Use alternate template for panel",
+ "panelname": "Internal name",
+ "panelname_helptext": "Name of panel as displayed in admin interface",
+ "qrcode": "QR code",
+ "qrcode_helptext": "Display QR code in lower right corner",
+ "section_general": "General",
+ "section_styling": "Styling",
+ "subheading": "Subheading",
+ "subheading_helptext": "Secondary heading, displayed below primary heading"
+}
\ No newline at end of file
diff --git a/modules-available/locationinfo/lang/en/template-tags.json b/modules-available/locationinfo/lang/en/template-tags.json
index b2504356..7a25a512 100644
--- a/modules-available/locationinfo/lang/en/template-tags.json
+++ b/modules-available/locationinfo/lang/en/template-tags.json
@@ -24,18 +24,20 @@
"lang_createPanel": "Create panel",
"lang_credentials": "Login",
"lang_currentDay": "Current Day",
+ "lang_currentEvents": "Current",
"lang_daysToShow": "Days",
"lang_daysToShowTooltip": "Defines the amount of days to show in the calendar",
- "lang_defaultPanel": "Default panel",
+ "lang_defaultPanel": "Room\/Calendar",
"lang_deleteConfirmation": "Are you sure?",
"lang_display": "Display",
"lang_displayName": "Name",
- "lang_displayNameTooltip": "Display name for this panel",
+ "lang_displayNameTooltip": "Name for this panel (as shown in listing)",
"lang_ecoMode": "E-Ink mode",
"lang_ecoTooltip": "Lower update rate, countdown doesn't show seconds",
"lang_editDefaultPanelHints": "Here you can define panel properties for e.g. a digital door sign. To show opening times for a room you need to define corresponding times in the settings.\r\nIf you want to show calendar events you have to define a functioning backend first and link it to corresponding rooms.",
"lang_editPanel": "Edit panel",
"lang_editSummaryPanelHints": "Here you can define a summary panel which shows a overview of clients in your locations.",
+ "lang_editUpcomingPanelHints": "Panel that shows active and upcoming events in one or more locations. Events will be shown in ascending order.",
"lang_editUrlPanelHints": "Here you can define which URL is opened by the panel. This enables you to show news about your university or any other website.",
"lang_entryName": "Name",
"lang_error": "Error",
@@ -80,6 +82,7 @@
"lang_modeTooltip": "The display modes the frontend supports",
"lang_monday": "Monday",
"lang_nameTooltip": "Defines the name of the server",
+ "lang_noCurrentEvents": "No current events",
"lang_noLocationsWarning": "Please select at least one location this panel should display.",
"lang_noServer": "",
"lang_openingTime": "Opening time",
@@ -141,8 +144,11 @@
"lang_switchTimeTooltip": "[1-120] Sets the time between switching (in seconds)",
"lang_thuesday": "Thuesday",
"lang_thursday": "Thursday",
+ "lang_timeSpanSuffix": " ",
"lang_to": "to",
"lang_typeTooltip": "Defines on which type of server you want to connect to",
+ "lang_upcomingEvents": "Upcoming",
+ "lang_upcomingPanel": "Upcoming events",
"lang_updateRates": "Update rates",
"lang_url": "URL",
"lang_urlListHelp": "You can specify lists of URLs or hostnames here, which are then interpreted as a whitelist or blacklist. Depending on the selected browser, either the whitelist or the \"more precise\" rule takes precedence. If no rule applies, access will be allowed unless there is an entry \"*\" in the blacklist. Depending on the browser used, \"*\" is supported as a wildcard in different places. You can introduce comments at the end of or between lines using \"#\".",
diff --git a/modules-available/locationinfo/page.inc.php b/modules-available/locationinfo/page.inc.php
index fd9abb18..538f2514 100644
--- a/modules-available/locationinfo/page.inc.php
+++ b/modules-available/locationinfo/page.inc.php
@@ -203,13 +203,19 @@ class Page_LocationInfo extends Page
* and remove any ids that don't exist. The cleaned list will be returned.
* Will show error and redirect to main page if parameter is missing
*
- * @param bool $failIfEmpty Show error and redirect to main page if parameter is missing or list is empty
- * @return array list of locations from parameter
+ * @return int[] list of locations from parameter
*/
private function getLocationIdsFromRequest(): array
{
- $locationids = Request::post('locationids', Request::REQUIRED_EMPTY, 'string');
- $locationids = explode(',', $locationids);
+ $locationids = Request::post('locationids', Request::REQUIRED_EMPTY);
+ if (is_array($locationids)) {
+ // NOOP
+ } elseif (is_numeric($locationids)) {
+ $locationids = [(int)$locationids];
+ } elseif (is_string($locationids)) {
+ $locationids = explode(',', $locationids);
+ }
+ $locationids = array_map('intval', $locationids);
$all = Location::getAllLocationIds();
$locationids = array_filter($locationids, function ($item) use ($all) { return in_array($item, $all); });
if (empty($locationids)) {
@@ -233,13 +239,24 @@ class Page_LocationInfo extends Page
// Check panel type
$paneltype = Request::post('ptype', false, 'string');
- if ($paneltype === 'DEFAULT') {
+//Refactored Code
+ switch ($paneltype) {
+ case 'DEFAULT':
$params = $this->preparePanelConfigDefault();
- } elseif ($paneltype === 'URL') {
+ break;
+ case 'URL':
$params = $this->preparePanelConfigUrl();
- } elseif ($paneltype === 'SUMMARY') {
+ break;
+ case 'SUMMARY':
$params = $this->preparePanelConfigSummary();
- } else {
+ break;
+ case 'UPCOMING':
+ $params = [
+ 'config' => LocationInfo::panelConfigFromPost($paneltype),
+ 'locationids' => self::getLocationIdsFromRequest(),
+ ];
+ break;
+ default:
Message::addError('invalid-panel-type', $paneltype);
Util::redirect('?do=locationinfo', 400);
}
@@ -257,7 +274,7 @@ class Page_LocationInfo extends Page
WHERE paneluuid = :id";
}
$params['id'] = $paneluuid;
- $params['name'] = Request::post('name', '-', 'string');
+ $params['name'] = Request::post('panelname', '-', 'string');
$params['type'] = $paneltype;
$params['now'] = time();
$params['config'] = json_encode($params['config']);
@@ -542,7 +559,8 @@ class Page_LocationInfo extends Page
$locations = Location::getLocations(0, 0, false, true);
// Get hidden state of all locations
- $dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid, loc.openingtime, li.lastcalendarupdate, cb.servertype, cb.servername
+ $dbquery = Database::simpleQuery("SELECT li.locationid, li.serverid, li.serverlocationid,
+ li.lastcalendarupdate, loc.openingtime, cb.servertype, cb.servername
FROM `locationinfo_locationconfig` AS li
LEFT JOIN `locationinfo_coursebackend` AS cb USING (serverid)
LEFT JOIN `location` AS loc USING (locationid)");
@@ -903,28 +921,10 @@ class Page_LocationInfo extends Page
return;
}
$config = false;
- if ($id === 'new-default') {
- // Creating new panel
- $panel = array(
- 'panelname' => '',
- 'locationids' => '',
- 'paneltype' => 'DEFAULT',
- );
- $id = 'new';
- } elseif ($id === 'new-summary') {
- // Creating new panel
- $panel = array(
- 'panelname' => '',
- 'locationids' => '',
- 'paneltype' => 'SUMMARY',
- );
- $id = 'new';
- } elseif ($id === 'new-url') {
- // Creating new panel
- $panel = array(
- 'panelname' => '',
- 'paneltype' => 'URL',
- );
+ if (substr($id, 0, 4) === 'new-') {
+ $panel = [
+ 'paneltype' => substr($id, 4),
+ ];
$id = 'new';
} else {
// Get Config data from db
@@ -951,6 +951,7 @@ class Page_LocationInfo extends Page
} else {
$config += $def;
}
+ $config['panelname'] = $panel['panelname'];
$langs = Dictionary::getLanguages(true);
if (isset($config['language'])) {
@@ -965,7 +966,7 @@ class Page_LocationInfo extends Page
Render::addTemplate('page-config-panel-default', array(
'new' => $id === 'new',
'uuid' => $id,
- 'panelname' => $panel['panelname'],
+ 'panelname' => $panel['panelname'] ?? '',
'languages' => $langs,
'mode' => $config['mode'],
'vertical_checked' => $config['vertical'] ? 'checked' : '',
@@ -981,7 +982,7 @@ class Page_LocationInfo extends Page
'calupdate' => $config['calupdate'],
'roomupdate' => $config['roomupdate'],
'locations' => Location::getLocations(),
- 'locationids' => $panel['locationids'],
+ 'locationids' => $panel['locationids'] ?? '',
'overrides' => json_encode($config['overrides']),
'hostname_checked' => $config['hostname'] ? 'checked' : '',
));
@@ -1006,7 +1007,7 @@ class Page_LocationInfo extends Page
Render::addTemplate('page-config-panel-url', array(
'new' => $id === 'new',
'uuid' => $id,
- 'panelname' => $panel['panelname'],
+ 'panelname' => $panel['panelname'] ?? '',
'url' => $config['url'],
'zoom-factor' => $config['zoom-factor'],
'language' => $config['language'],
@@ -1022,16 +1023,27 @@ class Page_LocationInfo extends Page
'bookmarks' => $bookmarksArray,
'allow-tty_' . $config['allow-tty'] . '_checked' => 'checked',
));
- } else {
+ } elseif ($panel['paneltype'] === 'UPCOMING') {
+ $configData = LocationInfo::getEditTemplateData($panel['paneltype'], $config);
+ LocationInfo::makeCheckedProperties($panel['paneltype'], $config);
+ Render::addTemplate('page-config-panel-upcoming', array(
+ 'new' => $id === 'new',
+ 'uuid' => $id,
+ 'sections' => $configData,
+ 'locationtree' => Render::parse('fragment-locationtree', [
+ 'locations' => Location::getLocations(explode(',', $panel['locationids'] ?? '')),
+ ]),
+ ));
+ } elseif ($panel['paneltype'] === 'SUMMARY') {
Render::addTemplate('page-config-panel-summary', array(
'new' => $id === 'new',
'uuid' => $id,
- 'panelname' => $panel['panelname'],
+ 'panelname' => $panel['panelname'] ?? '',
'languages' => $langs,
'panelupdate' => $config['panelupdate'],
'roomplanner' => $config['roomplanner'],
'locations' => Location::getLocations(),
- 'locationids' => $panel['locationids'],
+ 'locationids' => $panel['locationids'] ?? '',
'eco_checked' => $config['eco'] ? 'checked' : '',
));
}
@@ -1044,6 +1056,7 @@ class Page_LocationInfo extends Page
http_response_code(400);
die('Missing parameter uuid');
}
+ $config = [];
$type = InfoPanel::getConfig($uuid, $config);
if ($type === null) {
http_response_code(404);
@@ -1054,7 +1067,7 @@ class Page_LocationInfo extends Page
Util::redirect($config['url']);
}
- $data = array();
+ $data = [];
preg_match('#^/(.*)/#', $_SERVER['PHP_SELF'], $script);
preg_match('#^/([^?]+)/#', $_SERVER['REQUEST_URI'], $request);
if ($script[1] !== $request[1]) {
@@ -1065,36 +1078,56 @@ class Page_LocationInfo extends Page
$data['api'] = 'api.php?do=locationinfo&';
}
- if ($type === 'DEFAULT') {
- $data += array(
- 'uuid' => $uuid,
- 'config' => json_encode($config),
- 'language' => $config['language'],
- );
+ $lang = $config['language'] ?? 'en';
+ $reqLang = Request::get('forcelang', false, 'string');
+ if ($reqLang !== false && Dictionary::hasLanguage($reqLang)) {
+ $lang = $reqLang;
+ } elseif ($config['language-system'] ?? false) {
+ $langs = preg_split('/[,\s]+/', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
+ foreach ($langs as $check) {
+ $check = substr($check, 0, 2);
+ if (Dictionary::hasLanguage($check)) {
+ $lang = $check;
+ break;
+ }
+ }
+ }
+ unset($config['language']);
+ unset($config['language-system']);
+ if ($type === 'SUMMARY') {
+ $locations = LocationInfo::getLocationsOr404($uuid, false);
+ $config['tree'] = Location::getTree(...$locations);
+ }
+ $data += array(
+ 'uuid' => $uuid,
+ 'config' => json_encode($config),
+ 'language' => $lang,
+ ) + $config;
- die(Render::parse('frontend-default', $data, null, $config['language']));
+ if ($type === 'DEFAULT') {
+ die(Render::parse('frontend-default', $data, null, $lang));
}
if ($type === 'UPCOMING') {
- $data += array(
- 'uuid' => $uuid,
- 'config' => json_encode($config),
- 'language' => $config['language'],
- );
-
- die(Render::parse('frontend-kg2-upcoming', $data, null, $config['language']));
+ $mod = '';
+ if (!empty($config['mod']) && preg_match('#^[a-z0-9]+$#', $config['mod']) && file_exists('modules/locationinfo/templates/frontend-upcoming-' . $config['mod'] . '.html')) {
+ $mod = '-' . $config['mod'];
+ }
+ $bg = $this->parseCssColor($config['color_bg'] ?? '#000');
+ if ($bg !== null) {
+ $data['color_grad1'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', 0)';
+ $data['color_grad2'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', .1)';
+ $data['color_grad3'] = 'rgba(' . $bg[0] . ',' . $bg[1] . ',' . $bg[2] . ', .9)';
+ }
+ $fg = $this->parseCssColor($config['color_fg'] ?? '#fff');
+ if ($fg !== null) {
+ $data['color_half'] = 'rgba(' . $fg[0] . ',' . $fg[1] . ',' . $fg[2] . ', .5)';
+ }
+ die(Render::parse('frontend-upcoming' . $mod, $data, null, $lang));
}
if ($type === 'SUMMARY') {
- $locations = LocationInfo::getLocationsOr404($uuid, false);
- $config['tree'] = Location::getTree(...$locations);
- $data += array(
- 'uuid' => $uuid,
- 'config' => json_encode($config),
- 'language' => $config['language'],
- );
-
- die(Render::parse('frontend-summary', $data, null, $config['language']));
+ die(Render::parse('frontend-summary', $data, null, $lang));
}
http_response_code(500);
@@ -1138,4 +1171,27 @@ class Page_LocationInfo extends Page
Util::redirect('?do=locationinfo');
}
+ /**
+ * Parse a CSS color parameter into an array of color components.
+ * @param string $param The CSS color parameter to parse.
+ * @return ?array An array containing the color components [R, G, B, A] if valid, or null if parsing fails.
+ */
+ private function parseCssColor(string $param): ?array
+ {
+ $param = trim($param);
+ // #fff
+ if (preg_match('/^#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])$/', $param, $out))
+ return [hexdec($out[1]) * 17, hexdec($out[2]) * 17, hexdec($out[3]) * 17, 1];
+ // #ffffff
+ if (preg_match('/^#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})$/', $param, $out))
+ return [hexdec($out[1]), hexdec($out[2]), hexdec($out[3]), 1];
+ // rgb(255,255,255)
+ if (preg_match('/^rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)$/i', $param, $out))
+ return [(int)($out[1]), (int)($out[2]), (int)($out[3]), 1];
+ // rgba(255,255,255,1)
+ if (preg_match('/^rgba\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9.]{1,5})\s*\)$/i', $param, $out))
+ return [(int)$out[1], (int)$out[2], (int)$out[3], (float)$out[4]];
+ return null;
+ }
+
}
diff --git a/modules-available/locationinfo/templates/fragment-locationtree.html b/modules-available/locationinfo/templates/fragment-locationtree.html
new file mode 100644
index 00000000..96cd8e24
--- /dev/null
+++ b/modules-available/locationinfo/templates/fragment-locationtree.html
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+ {{lang_noLocationsWarning}}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/modules-available/locationinfo/templates/frontend-default.html b/modules-available/locationinfo/templates/frontend-default.html
index 04f08590..5cd044d1 100755
--- a/modules-available/locationinfo/templates/frontend-default.html
+++ b/modules-available/locationinfo/templates/frontend-default.html
@@ -364,9 +364,9 @@ optional:
+
-
@@ -393,13 +393,16 @@ optional:
-
+
+