diff --git a/css/simple.css b/css/simple.css new file mode 100644 index 0000000..77f9541 --- /dev/null +++ b/css/simple.css @@ -0,0 +1,346 @@ +* { +box-sizing:border-box; +} +html { +height:100%; +background:#080b0f; +} +body { +margin:0; +height:100%; +color:#f5f7fa; +background:#080b0f; +font:16px/1.25 Verdana,Arial,Helvetica,sans-serif; +overflow:hidden; +} +button { +font:inherit; +} +.simple-app { +height:100vh; +min-width:320px; +display:flex; +flex-direction:column; +} +.topbar { +height:48px; +display:grid; +grid-template-columns:auto 1fr auto; +align-items:center; +gap:14px; +padding:6px 14px; +background:#121a24; +border-bottom:2px solid #2c4054; +} +.brand { +color:#91d79c; +font-size:20px; +font-weight:bold; +text-decoration:none; +} +.node-title { +overflow:hidden; +text-overflow:ellipsis; +white-space:nowrap; +color:#d7e1ea; +font-size:17px; +} +.node-number { +color:#9db2c4; +font-size:16px; +white-space:nowrap; +} +.simple-main { +flex:1; +min-height:0; +display:grid; +grid-template-rows:auto auto auto 1fr; +gap:8px; +padding:8px; +} +.status-panel, +.warning-panel, +.readonly-panel { +display:grid; +grid-template-columns:1fr auto; +align-items:center; +gap:10px; +border:2px solid #314b61; +padding:8px 10px; +min-height:76px; +background:#101820; +} +.status-label { +font-size:34px; +font-weight:bold; +line-height:1; +letter-spacing:0; +} +.status-detail, +.warning-detail, +.readonly-detail { +margin-top:5px; +color:#d5dde4; +font-size:16px; +overflow:hidden; +text-overflow:ellipsis; +white-space:nowrap; +} +.state-idle { +border-color:#3d4e5e; +} +.state-connected { +border-color:#20a35b; +background:#0e261a; +} +.state-connecting { +border-color:#d2bd4a; +background:#292612; +} +.state-rx { +border-color:#32b876; +background:#102c20; +} +.state-tx { +border-color:#ce4b4b; +background:#331313; +} +.state-warning, +.warning-panel { +border-color:#ffcf3f; +background:#3b2d08; +} +.disconnect-btn, +.disconnect-all-btn, +.scroll-btn { +min-width:132px; +min-height:56px; +border:0; +border-radius:6px; +font-weight:bold; +color:#fff; +background:#3a4957; +} +.disconnect-btn:enabled { +background:#9b3434; +} +.disconnect-all-btn { +min-width:210px; +min-height:64px; +font-size:23px; +background:#c01818; +} +button:disabled { +opacity:0.38; +} +.hidden { +display:none; +} +.warning-title { +font-size:24px; +font-weight:bold; +color:#fff0a3; +} +.readonly-panel { +border-color:#5a7188; +background:#182534; +} +.readonly-title { +font-size:24px; +font-weight:bold; +color:#c9d8e6; +} +.login-btn { +min-width:132px; +min-height:56px; +border:0; +border-radius:6px; +font-weight:bold; +color:#07100a; +background:#91d79c; +} +.channels-shell { +min-height:0; +display:grid; +grid-template-columns:64px 1fr 64px; +gap:8px; +} +.channels-viewport { +min-height:0; +overflow-y:auto; +scrollbar-width:none; +} +.channels-viewport::-webkit-scrollbar { +display:none; +} +.channels-grid { +display:grid; +grid-template-columns:repeat(auto-fit, minmax(180px, 1fr)); +gap:8px; +align-content:start; +padding-bottom:2px; +} +.channel-btn { +position:relative; +display:grid; +grid-template-columns:auto 1fr; +grid-template-rows:auto auto; +gap:3px 8px; +align-items:center; +min-height:104px; +padding:10px; +border:2px solid #2f4559; +border-radius:8px; +background:#162331; +color:#f8fbff; +text-align:left; +overflow:hidden; +} +.channel-btn:enabled:active { +transform:translateY(1px); +} +.channel-btn.active { +border-color:#30c66d; +background:#123d27; +box-shadow:inset 0 0 0 2px rgba(255,255,255,0.12); +} +.channel-btn.connected-extra { +border-color:#ffcf3f; +background:#3b2d08; +} +.channel-index { +grid-row:1 / span 2; +display:flex; +align-items:center; +justify-content:center; +width:34px; +height:34px; +border-radius:50%; +background:#2f4255; +color:#dce8f2; +font-weight:bold; +} +.channel-label { +font-size:18px; +font-weight:bold; +line-height:1.12; +text-align:center; +overflow:hidden; +display:-webkit-box; +-webkit-line-clamp:3; +-webkit-box-orient:vertical; +} +.channel-meta { +grid-column:2; +font-size:14px; +font-weight:normal; +color:#9ee0a8; +white-space:nowrap; +overflow:hidden; +text-overflow:ellipsis; +} +.scroll-btn { +height:100%; +min-width:0; +font-size:22px; +background:#263646; +} +.scroll-btn:enabled { +background:#36516a; +} +.simple-footer { +height:30px; +display:grid; +grid-template-columns:1fr 1fr; +gap:8px; +align-items:center; +padding:3px 8px; +border-top:1px solid #263646; +color:#9eacb7; +font-size:12px; +overflow:hidden; +} +#scanmsg, +#statmsg { +overflow:hidden; +text-overflow:ellipsis; +white-space:nowrap; +} +.empty-channels { +padding:28px; +border:2px dashed #384c5f; +color:#b8c6d1; +text-align:center; +font-size:22px; +} +@media (max-width:700px) { +body { +font-size:14px; +} +.topbar { +grid-template-columns:1fr auto; +height:44px; +} +.brand { +display:none; +} +.node-title { +font-size:15px; +} +.status-label { +font-size:26px; +} +.status-panel, +.warning-panel, +.readonly-panel { +min-height:66px; +} +.disconnect-btn, +.disconnect-all-btn, +.login-btn { +min-width:120px; +min-height:50px; +font-size:15px; +} +.channels-shell { +grid-template-columns:48px 1fr 48px; +} +.channels-grid { +grid-template-columns:repeat(auto-fit, minmax(145px, 1fr)); +} +.channel-btn { +min-height:94px; +} +.channel-label { +font-size:16px; +} +} +@media (max-height:430px) { +.topbar { +height:40px; +} +.simple-main { +gap:6px; +padding:6px; +} +.status-panel, +.warning-panel { +min-height:62px; +padding:6px 8px; +} +.status-label { +font-size:27px; +} +.warning-title { +font-size:20px; +} +.readonly-title { +font-size:20px; +} +.channel-btn { +min-height:86px; +} +.simple-footer { +display:none; +} +} diff --git a/include/common.php b/include/common.php index 5dcb646..f8deadc 100644 --- a/include/common.php +++ b/include/common.php @@ -124,6 +124,11 @@ function checkTitleCfgs() { function getHdrLinks() { global $html, $urlbase, $user; $lnk = []; + if(readOk()) { + $url = "$urlbase/simple/"; + $title = 'Simple'; + $lnk[] = ($url === getScriptName()) ? $title : $html->a($url, null, $title); + } if(isset($user->user_id) && validDbID($user->user_id)) { // Show links to Cfg and User modules if Admin user if(adminUser()) { diff --git a/include/favsUtils.php b/include/favsUtils.php index 72ba7d4..f5f7820 100644 --- a/include/favsUtils.php +++ b/include/favsUtils.php @@ -74,3 +74,114 @@ function findFavsFiles(&$activeFile) { $activeFile = $vfiles[0]; return $vfiles; } + +function getFavsData($favsFile, $node, $astdb, &$msg) { + $favs = []; + $favcmds = []; + $favList = []; + $error = ''; + if(!isset($favsFile) || !$favsFile) { + return ['favs' => $favs, 'favcmds' => $favcmds, 'favList' => $favList, 'error' => $error]; + } + $favsIni = parse_ini_file($favsFile, true); + if($favsIni === false) { + $error = "Error parsing $favsFile. Check file format/permissions or create file with www-data writeable permissions."; + return ['favs' => $favs, 'favcmds' => $favcmds, 'favList' => $favList, 'error' => $error]; + } + $favsCfg = ['label' => [], 'cmd' => []]; + if(isset($favsIni['general'])) + $favsCfg = array_merge($favsCfg, $favsIni['general']); + if(isset($favsIni[$node])) { + foreach($favsIni[$node] as $type => $arr) { + if($type == 'label') { + foreach($arr as $label) + $favsCfg['label'][] = $label; + } elseif($type == 'cmd') { + foreach($arr as $cmd) + $favsCfg['cmd'][] = $cmd; + } + } + } + $favsCfg['label'] = array_map('trim', $favsCfg['label'] ?? []); + $favsCfg['cmd'] = array_map('trim', $favsCfg['cmd'] ?? []); + foreach($favsCfg['cmd'] as $i => $c) { + $label = $favsCfg['label'][$i] ?? ''; + if(!$c) { + unset($favsCfg['cmd'][$i], $favsCfg['label'][$i]); + } else { + if(preg_match('/[0-9]{4,8}/', $c, $m) == 1) + $favs[$i] = (object)['node'=>$m[0], 'label'=>$label, 'cmd'=>$c]; + else + $favcmds[$i] = (object)['label'=>$label, 'cmd'=>$c]; + } + } + $msg[] = _count($favs) . " favorites read from $favsFile"; + $favList = buildFavList($favs, $astdb); + return ['favs' => $favs, 'favcmds' => $favcmds, 'favList' => $favList, 'error' => $error]; +} + +function buildFavList($favs, $astdb) { + $favList = []; + $trimchars = " .,;\n\r\t\v\x00"; + foreach($favs as $n => $f) { + if(array_key_exists($f->node, $astdb)) { + list($x, $call, $desc, $loc) = $astdb[$f->node]; + } else { + if($f->node < 3000000) { + list($x, $call, $desc, $loc) = [$n, '-', '[Not in ASL DB]', '[Check Node Number]']; + } else { + $info = getELInfo($f->node); + if(empty($info)) + list($x, $call, $desc, $loc) = [$n, '-', '[EchoLink Node]', '-']; + else { + if(preg_match('/(.*) (\[.*\])/', $info, $m) != 1) + $m = [1=>'-', 2=>"[EchoLink $f->node]"]; + list($x, $call, $desc, $loc) = [$n, $m[1], $m[2], '-']; + } + } + } + $name = str_replace([$f->node, $call, $desc, $loc, ' ,'], ' ', $f->label); + foreach(['call', 'name', 'desc', 'loc'] as $var) + $$var = trim(str_replace(' ', ' ', $$var), $trimchars); + foreach(['name', 'desc'] as $var) + $$var = str_replace(['mhz', 'MHZ', 'hz'], ['MHz', 'MHz', 'Hz'], $$var); + if(!$name) + $name = $call; + elseif(strpos($name, $call) === false && $call !== '-') + $name = $call . ' ' . $name; + if(empty($desc) && strlen($name) > strlen($call) && strlen($call) > 2) { + if(strpos($name, $call) === 0) { + $desc = trim(substr($name, strlen($call)), $trimchars); + $name = $call; + } + } + if(strpos($name, $call) !== false && strpos($desc, "$call ") !== false) + $desc = trim(str_replace($call, '', $desc), $trimchars); + $favList[] = [$n, $f->node, $name, $desc, $loc, NBSP, NBSP]; + } + return $favList; +} + +function getELInfo($n) { + global $amicfg, $ami; + static $fp; + if( empty($amicfg->host) || empty($amicfg->port) || + empty($amicfg->user) || empty($amicfg->pass) || $fp === false ) { + return; + } + // Login to AMI + if(empty($ami)) { + $ami = new AMI(); + } + if(!isset($fp)) { + $fp = $ami->connect($amicfg->host, $amicfg->port); + if($fp === false) { + return; + } + if($ami->login($fp, $amicfg->user, $amicfg->pass) === false) { + unset($fp); + return; + } + } + return getAstInfo($fp, $n); +} diff --git a/index.php b/index.php index 9209f05..f2740cb 100644 --- a/index.php +++ b/index.php @@ -79,102 +79,10 @@ showNodeCtrlForm(); h2('Favorites'); -// Read in favorites.ini -$favs = []; -$favcmds = []; -if(!isset($favsFile) || !$favsFile) { - msg('No Favorites file found. Favorites files can be uploaded on the Cfgs Tab. ' - . 'A favorites-Sample.ini file can be downloaded from the AllScan github page'); -} else { - $favsIni = parse_ini_file($favsFile, true); - if($favsIni === false) { - p("Error parsing $favsFile. Check file format/permissions or create file with www-data writeable permissions."); - } else { - // Combine [general] stanza with this node's stanza - $favsCfg = $favsIni['general']; - if(isset($favsIni[$node])) { - foreach($favsIni[$node] as $type => $arr) { - if($type == 'label') { - foreach($arr as $label) { - $favsCfg['label'][] = $label; - } - } elseif($type == 'cmd') { - foreach($arr as $cmd) { - $favsCfg['cmd'][] = $cmd; - } - } - } - } - $favsCfg['label'] = array_map('trim', $favsCfg['label']); - $favsCfg['cmd'] = array_map('trim', $favsCfg['cmd']); - foreach($favsCfg['cmd'] as $i => $c) { - if(!$c) { - unset($favsCfg['cmd'][$i], $favsCfg['label'][$i]); - } else { - if(preg_match('/[0-9]{4,8}/', $c, $m) == 1) - $favs[$i] = (object)['node'=>$m[0], 'label'=>$favsCfg['label'][$i], 'cmd'=>$c]; - else - $favcmds[$i] = (object)['label'=>$favsCfg['label'][$i], 'cmd'=>$c]; - } - } - // if(count($favcmds)) - // varDump($favcmds); - $msg[] = _count($favs) . " favorites read from $favsFile"; - } -} -// Combine favs node, label data with astdb data into favList -$favList = []; -$trimchars = " .,;\n\r\t\v\x00"; -foreach($favs as $n => $f) { - if(array_key_exists($f->node, $astdb)) { - list($x, $call, $desc, $loc) = $astdb[$f->node]; - } else { - if($f->node < 3000000) { - list($x, $call, $desc, $loc) = [$n, '-', '[Not in ASL DB]', '[Check Node Number]']; - } else { - $info = getELInfo($f->node); - if(empty($info)) - list($x, $call, $desc, $loc) = [$n, '-', '[EchoLink Node]', '-']; - else { - if(preg_match('/(.*) (\[.*\])/', $info, $m) != 1) - $m = [1=>'-', 2=>"[EchoLink $f->node]"]; - list($x, $call, $desc, $loc) = [$n, $m[1], $m[2], '-']; - } - } - } - // Te keep Favs table compact remove redundant text that exists in multiple fields - $name = str_replace([$f->node, $call, $desc, $loc, ' ,'], ' ', $f->label); - //$msg[] = "$x, $call, $desc, $loc, $f->label"; - //$msg[] = $name; - // Remove unnecessary white space and punctuation - foreach(['call', 'name', 'desc', 'loc'] as $var) - $$var = trim(str_replace(' ', ' ', $$var), $trimchars); - // Fix common misspellings - foreach(['name', 'desc'] as $var) - $$var = str_replace(['mhz', 'MHZ', 'hz'], ['MHz', 'MHz', 'Hz'], $$var); - // Name is derived from favorites file text, but if that contained only redundant information to what - // was in astdb and is thus now blank, use the call sign - if(!$name) - $name = $call; - // Confirm call sign is present in name - elseif(strpos($name, $call) === false && $call !== '-') - $name = $call . ' ' . $name; - // Many nodes in astdb have descriptive text in the name field but then a blank description. In this - // case, to make the table look more consistent and clean move any text from the name (except for the - // call sign) to the description column - if(empty($desc) && strlen($name) > strlen($call) && strlen($call) > 2) { - if(strpos($name, $call) === 0) { - $desc = trim(substr($name, strlen($call)), $trimchars); - $name = $call; - } - } - // Remove redundant call sign data from the description - if(strpos($name, $call) !== false && strpos($desc, "$call ") !== false) { - $desc = trim(str_replace($call, '', $desc), $trimchars); - } - //$msg[] = $name; - $favList[] = [$n, $f->node, $name, $desc, $loc, NBSP, NBSP]; -} +$favsData = getFavsData($favsFile ?? '', $node, $astdb, $msg); +$favs = $favsData['favs']; +$favcmds = $favsData['favcmds']; +$favList = $favsData['favList']; // Sort favList by specified column if fs parm is set $colKey = ['num', 'node', 'name', 'desc', 'loc']; $sortCol = isset($_GET['fs']) && in_array($_GET['fs'], $colKey) ? $_GET['fs'] : 'num'; @@ -185,7 +93,13 @@ } // Output Favorites table if(empty($favList)) { - p('No Favorites have yet been added'); + if($favsData['error']) + p($favsData['error']); + elseif(!isset($favsFile) || !$favsFile) + p('No Favorites file found. Favorites files can be uploaded on the Cfgs Tab. ' + . 'A favorites-Sample.ini file can be downloaded from the AllScan github page'); + else + p('No Favorites have yet been added'); } else { $hdrCols = ['#', 'Node', 'Name', 'Desc', 'Location', 'Rx%', 'LCnt']; if(count($favList) > 1) { @@ -401,30 +315,6 @@ function checkUpdate() { return false; } -function getELInfo($n) { - global $amicfg, $ami; - static $fp; - if( empty($amicfg->host) || empty($amicfg->port) || - empty($amicfg->user) || empty($amicfg->pass) || $fp === false ) { - return; - } - // Login to AMI - if(empty($ami)) { - $ami = new AMI(); - } - if(!isset($fp)) { - $fp = $ami->connect($amicfg->host, $amicfg->port); - if($fp === false) { - return; - } - if($ami->login($fp, $amicfg->user, $amicfg->pass) === false) { - unset($fp); - return; - } - } - return getAstInfo($fp, $n); -} - function showCustomCmdButtons() { global $gCfg, $html; foreach($gCfg[cmdbuttons] as $b) { diff --git a/js/simple.js b/js/simple.js new file mode 100644 index 0000000..9b9581f --- /dev/null +++ b/js/simple.js @@ -0,0 +1,355 @@ +var simple = { + source: null, + channels: [], + channelByNode: {}, + currentNode: '', + connectedNodes: [], + canModify: false, + baseUrl: '', + loginUrl: '', + localNode: '', + statmsg: null, + scanmsg: null, + statusPanel: null, + statusLabel: null, + statusDetail: null, + warningPanel: null, + warningDetail: null, + readonlyPanel: null, + loginBtn: null, + disconnectBtn: null, + disconnectAllBtn: null, + channelsViewport: null, + channelsGrid: null, + scrollUpBtn: null, + scrollDownBtn: null, + retryTmr: null +}; + +function simpleInit() { + var cfg = window.simpleConfig || {}; + simple.channels = cfg.channels || []; + simple.canModify = !!cfg.canModify; + simple.baseUrl = cfg.baseUrl || ''; + simple.loginUrl = cfg.loginUrl || (simple.baseUrl + '/user/'); + simple.localNode = cfg.localNode || ''; + simple.statmsg = document.getElementById('statmsg'); + simple.scanmsg = document.getElementById('scanmsg'); + simple.statusPanel = document.getElementById('statusPanel'); + simple.statusLabel = document.getElementById('statusLabel'); + simple.statusDetail = document.getElementById('statusDetail'); + simple.warningPanel = document.getElementById('warningPanel'); + simple.warningDetail = document.getElementById('warningDetail'); + simple.readonlyPanel = document.getElementById('readonlyPanel'); + simple.loginBtn = document.getElementById('loginBtn'); + simple.disconnectBtn = document.getElementById('disconnectBtn'); + simple.disconnectAllBtn = document.getElementById('disconnectAllBtn'); + simple.channelsViewport = document.getElementById('channelsViewport'); + simple.channelsGrid = document.getElementById('channelsGrid'); + simple.scrollUpBtn = document.getElementById('scrollUpBtn'); + simple.scrollDownBtn = document.getElementById('scrollDownBtn'); + + if(cfg.startupMessages) + statMsg(cfg.startupMessages); + renderChannels(); + bindControls(); + updateReadOnlyPanel(); + setStatus('NO CONNECTION', 'Waiting for node status...', 'idle'); + initEventSource(); +} + +function bindControls() { + simple.disconnectBtn.addEventListener('click', function() { + if(simple.currentNode) + disconnectNode(simple.currentNode); + }); + simple.disconnectAllBtn.addEventListener('click', function() { + disconnectLocalNodes(); + }); + simple.loginBtn.addEventListener('click', function() { + window.location.href = simple.loginUrl; + }); + simple.scrollUpBtn.addEventListener('click', function() { + scrollChannels(-1); + }); + simple.scrollDownBtn.addEventListener('click', function() { + scrollChannels(1); + }); + simple.channelsViewport.addEventListener('scroll', updateScrollButtons); + window.addEventListener('resize', updateScrollButtons); + window.addEventListener('beforeunload', closeEventSource); +} + +function updateReadOnlyPanel() { + if(simple.canModify) + simple.readonlyPanel.classList.add('hidden'); + else + simple.readonlyPanel.classList.remove('hidden'); +} + +function renderChannels() { + simple.channelByNode = {}; + simple.channelsGrid.innerHTML = ''; + if(!simple.channels.length) { + simple.channelsGrid.innerHTML = '