From 8d5096ca1a79acca639be95df5aad35d008f55db Mon Sep 17 00:00:00 2001 From: "Benjamin E. Seidenberg" Date: Thu, 30 Jul 2026 12:31:01 -0700 Subject: [PATCH] Add channel-style touch interface Add a new /simple/ interface focused on quick, touch-friendly favorite node control. The interface is designed to behave more like a channelized radio than the full AllScan control page: favorites are shown as large channel buttons, and the normal workflow is to select one channel/node at a time. When a channel is selected, the Simple interface uses the existing connect API with autodisconnect enabled so the local node is first disconnected from other nodes before connecting to the selected favorite. If additional connections are still present, the page shows a warning state and offers a disconnect-all control instead of treating that as the normal single-channel state. To avoid duplicating the main page's favorites parsing, move the shared favorites loading and favorite-list building logic from index.php into include/favsUtils.php. The main page now calls that helper and keeps its existing favorites table rendering, sorting, and empty/error states. The Simple interface includes: - live node status via the existing server-sent events endpoint - connect/disconnect controls using the existing astapi/connect.php API - read-only handling when the current user cannot modify connections - dedicated CSS and JS for the simplified touch layout Add a Simple header link for users with read access. Co-authored-by: OpenAI Codex --- css/simple.css | 346 ++++++++++++++++++++++++++++++++++++++++ include/common.php | 5 + include/favsUtils.php | 111 +++++++++++++ index.php | 132 ++-------------- js/simple.js | 355 ++++++++++++++++++++++++++++++++++++++++++ simple/index.php | 149 ++++++++++++++++++ 6 files changed, 977 insertions(+), 121 deletions(-) create mode 100644 css/simple.css create mode 100644 js/simple.js create mode 100644 simple/index.php 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 = '
No channels configured
'; + return; + } + simple.channels.forEach(function(channel, i) { + var btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'channel-btn'; + btn.dataset.node = channel.node; + btn.disabled = !simple.canModify; + btn.innerHTML = '' + (i + 1) + '' + + '' + escapeHtml(channelLabel(channel)) + '' + + '' + escapeHtml(channelMeta(channel)) + ''; + btn.addEventListener('click', function() { + connectChannel(channel.node); + }); + simple.channelByNode[channel.node] = channel; + simple.channelsGrid.appendChild(btn); + }); + updateScrollButtons(); +} + +function initEventSource() { + if(typeof(EventSource) === 'undefined') { + statMsg('ERROR: Browser does not support server-sent events.'); + return; + } + closeEventSource(); + var url = simple.baseUrl + '/astapi/server.php?nodes=' + encodeURIComponent(simple.localNode); + simple.source = new EventSource(url); + simple.source.addEventListener('nodes', handleNodesEvent, false); + simple.source.addEventListener('connection', handleConnectionEvent, false); + simple.source.addEventListener('errMsg', handleErrMsgEvent, false); + simple.source.onerror = function() { + statMsg('Event Source error. Retrying...'); + if(simple.retryTmr !== null) + clearTimeout(simple.retryTmr); + simple.retryTmr = setTimeout(initEventSource, 15000); + }; +} + +function closeEventSource() { + if(simple.retryTmr !== null) { + clearTimeout(simple.retryTmr); + simple.retryTmr = null; + } + if(simple.source) { + simple.source.close(); + simple.source = null; + } +} + +function handleConnectionEvent(event) { + var data = JSON.parse(event.data); + if(data.status) + statMsg(data.status); +} + +function handleErrMsgEvent(event) { + var data = JSON.parse(event.data); + statMsg('ERROR: ' + data.status); +} + +function handleNodesEvent(event) { + var tabledata = JSON.parse(event.data); + var nodeData = tabledata[simple.localNode]; + if(!nodeData) + return; + var directNodes = []; + var keyed = false; + var txKeyed = false; + var connecting = false; + + nodeData.remote_nodes.forEach(function(row) { + if(row.cos_keyed == 1) + keyed = true; + if(row.tx_keyed == 1) + txKeyed = true; + if(row.info === 'NO CONNECTION') + return; + if(row.node == 1) + return; + if(row.node) + directNodes.push(String(row.node)); + if(row.mode === 'C') + connecting = true; + }); + + directNodes = unique(directNodes); + simple.connectedNodes = directNodes; + updateState(directNodes, keyed, txKeyed, connecting); +} + +function updateState(directNodes, keyed, txKeyed, connecting) { + var activeNode = ''; + directNodes.some(function(node) { + if(simple.channelByNode[node]) { + activeNode = node; + return true; + } + return false; + }); + simple.currentNode = activeNode; + updateChannelButtons(activeNode); + + var extras = directNodes.filter(function(node) { + return node !== activeNode; + }); + var hasExternalOnly = !activeNode && directNodes.length > 0; + var hasExtras = extras.length > 0 || hasExternalOnly; + var label = 'NO CONNECTION'; + var detail = 'Tap a channel to connect.'; + var state = 'idle'; + + if(hasExtras) { + label = 'EXTRA CONNECTIONS'; + detail = activeNode ? channelTitle(activeNode) + ' plus ' + extras.join(', ') : 'Connected to ' + directNodes.join(', '); + state = 'warning'; + } else if(activeNode) { + label = txKeyed ? 'TX ACTIVE' : (keyed ? 'RX ACTIVE' : (connecting ? 'CONNECTING' : 'CONNECTED')); + detail = channelTitle(activeNode); + state = txKeyed ? 'tx' : (keyed ? 'rx' : (connecting ? 'connecting' : 'connected')); + } + + setStatus(label, detail, state); + updateWarning(hasExtras, activeNode, extras, directNodes); + simple.disconnectBtn.disabled = !simple.canModify || !activeNode || hasExtras; +} + +function setStatus(label, detail, state) { + simple.statusLabel.textContent = label; + simple.statusDetail.textContent = detail; + simple.statusPanel.className = 'status-panel state-' + state; + document.title = label + ' - AllScan Simple'; +} + +function updateWarning(show, activeNode, extras, directNodes) { + if(!show) { + simple.warningPanel.classList.add('hidden'); + return; + } + var nodes = activeNode ? extras : directNodes; + simple.warningDetail.textContent = nodes.length + ? 'Additional connected node(s): ' + nodes.join(', ') + : 'Additional nodes are connected.'; + simple.warningPanel.classList.remove('hidden'); + simple.disconnectAllBtn.disabled = !simple.canModify; +} + +function updateChannelButtons(activeNode) { + var buttons = simple.channelsGrid.querySelectorAll('.channel-btn'); + buttons.forEach(function(btn) { + var node = btn.dataset.node; + btn.classList.toggle('active', node === activeNode); + btn.classList.toggle('connected-extra', simple.connectedNodes.indexOf(node) >= 0 && node !== activeNode); + }); +} + +function connectChannel(node) { + if(!simple.canModify) + return; + statMsg('Connecting to channel ' + node + '...'); + sendControl('remotenode=' + encodeURIComponent(node) + + '&perm=false&button=connect&localnode=' + encodeURIComponent(simple.localNode) + + '&autodisc=true'); +} + +function disconnectNode(node) { + if(!simple.canModify) + return; + var label = node === '0' ? 'Disconnecting all nodes...' : 'Disconnecting ' + node + '...'; + statMsg(label); + sendControl('remotenode=' + encodeURIComponent(node) + + '&perm=false&button=disconnect&localnode=' + encodeURIComponent(simple.localNode)); +} + +function disconnectLocalNodes() { + if(!simple.canModify || !simple.connectedNodes.length) + return; + statMsg('Disconnecting locally connected node(s): ' + simple.connectedNodes.join(', ') + '...'); + sendControl('remotenode=0&perm=false&button=disconnect&localnode=' + encodeURIComponent(simple.localNode)); +} + +function sendControl(parms) { + var xhr = new XMLHttpRequest(); + xhr.onreadystatechange = function() { + if(xhr.readyState !== 4) + return; + if(xhr.status === 200) + statMsg(xhr.responseText); + else + statMsg('Control request failed: HTTP ' + xhr.status); + }; + xhr.open('POST', simple.baseUrl + '/astapi/connect.php', true); + xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); + xhr.send(parms); +} + +function scrollChannels(direction) { + var amount = Math.max(120, Math.floor(simple.channelsViewport.clientHeight * 0.85)); + simple.channelsViewport.scrollBy({top: direction * amount, behavior: 'smooth'}); +} + +function updateScrollButtons() { + var vp = simple.channelsViewport; + var canScroll = vp.scrollHeight > vp.clientHeight + 2; + simple.scrollUpBtn.disabled = !canScroll || vp.scrollTop <= 1; + simple.scrollDownBtn.disabled = !canScroll || vp.scrollTop + vp.clientHeight >= vp.scrollHeight - 1; +} + +function statMsg(msg) { + if(!simple.statmsg || !msg) + return; + simple.statmsg.innerHTML = msg; +} + +function channelTitle(node) { + var channel = simple.channelByNode[node]; + if(!channel) + return 'Node ' + node; + return channelLabel(channel) + ' (' + node + ')'; +} + +function channelLabel(channel) { + var label = channel.label || channel.name || channel.node || ''; + if(channel.node) { + var nodePat = new RegExp('(^|[\\s,;:-]+)' + escapeRegExp(channel.node) + '([\\s,;:-]*$|(?=[\\s,;:-]+))', 'g'); + label = label.replace(nodePat, function(match, prefix) { + return prefix && prefix.trim() ? prefix : ''; + }); + } + label = label.replace(/\s{2,}/g, ' ').replace(/[\s,;:-]+$/g, '').trim(); + return label || channel.name || channel.node || ''; +} + +function channelMeta(channel) { + var parts = []; + if(channel.node) + parts.push(channel.node); + if(channel.call) + parts.push(channel.call); + return parts.join(' - '); +} + +function unique(items) { + var out = []; + items.forEach(function(item) { + item = String(item); + if(item && out.indexOf(item) < 0) + out.push(item); + }); + return out; +} + +function escapeHtml(txt) { + return String(txt).replace(/[&<>"']/g, function(c) { + return {'&':'&', '<':'<', '>':'>', '"':'"', "'":'''}[c]; + }); +} + +function escapeRegExp(txt) { + return String(txt).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/simple/index.php b/simple/index.php new file mode 100644 index 0000000..9877a5c --- /dev/null +++ b/simple/index.php @@ -0,0 +1,149 @@ +validate(); +if(!readOk()) + redirect('user/'); + +if(!getAmiCfg($msg)) + simplePageError(implode(BR, $msg)); +$node = $amicfg->node; +$astdb = readAstDb($msg); +if($astdb === false) + simplePageError(implode(BR, $msg)); + +$favsFile = ''; +$favsFiles = findFavsFiles($favsFile); +$parms = getRequestParms(); +if(!empty($favsFiles) && isset($parms['favsfile']) && in_array($parms['favsfile'], $favsFiles)) + $favsFile = $parms['favsfile']; + +$favsData = getFavsData($favsFile, $node, $astdb, $msg); +$channels = []; +foreach($favsData['favList'] as $f) { + $rawLabel = isset($favsData['favs'][$f[0]]) ? $favsData['favs'][$f[0]]->label : $f[2]; + $callSign = ''; + if(array_key_exists($f[1], $astdb)) { + $callSign = $astdb[$f[1]][1]; + } elseif($f[2] && $f[2] !== '-') { + $nameParts = preg_split('/\s+/', $f[2]); + $callSign = $nameParts[0]; + } + $channels[] = [ + 'num' => $f[0], + 'node' => (string)$f[1], + 'call' => $callSign, + 'label' => $rawLabel, + 'name' => $f[2], + 'desc' => $f[3], + 'loc' => $f[4] + ]; +} + +checkTitleCfgs(); +$canModify = modifyOk(); +$pageTitle = 'AllScan Simple'; +$jsonFlags = JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT; +$channelJson = json_encode($channels, $jsonFlags); +$titleJson = json_encode($title2, $jsonFlags); +$msgJson = json_encode(implode(BR, $msg), $jsonFlags); + +echo $html->htmlOpen($pageTitle) + . "" . NL + . "" . NL + . '' . NL + . "" . NL + . '' . NL + . '' . NL; +?> +
+
+ AllScan +
+
Node
+
+ +
+
+
+
NO CONNECTION
+
Waiting for node status...
+
+ +
+ + + + + +
+ +
+
+
+ +
+
+ +
+
+
+
+
+ + +\n\n"; +exit(); + +function simplePageError($errMsg) { + global $html, $urlbase; + $errMsg = htmlspecial(str_replace(BR, ' ', $errMsg)); + echo $html->htmlOpen('AllScan Simple Error') + . "" . NL + . '' . NL + . "\n\n" + . '
' + . '
' + . '
ERROR
' + . '
' . $errMsg . '
' + . '
' + . "\n\n\n"; + exit(); +}