Merge remote-tracking branch 'upstream/master' into Intel-processor-core-types

This commit is contained in:
SimonFair
2025-05-06 19:28:15 +01:00
137 changed files with 6302 additions and 3153 deletions

View File

@@ -21,8 +21,8 @@ Nchan="docker_load:stop"
<?
require_once "$docroot/plugins/dynamix.docker.manager/include/DockerClient.php";
$width = in_array($theme,['white','black']) ? -58: -44;
$top = in_array($theme,['white','black']) ? 40 : 20;
$width = $themeHelper->isTopNavTheme() ? -58: -44; // $themeHelper set in DefaultPageLayout.php
$top = $themeHelper->isTopNavTheme() ? 40 : 20;
$busy = "<i class='fa fa-spin fa-circle-o-notch'></i> "._('Please wait')."... "._('starting up containers');
$cpus = cpu_list();
?>
@@ -82,7 +82,7 @@ function LockButton() {
$('#resetsort').removeClass('nohand').addClass('hand');
$('i.mover').show();
$('#docker_list .sortable').css({'cursor':'move'});
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton').find('a').prop('title',"_(Lock sortable items)_");
$('div.nav-item.LockButton').find('b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
<?endif;?>
@@ -99,7 +99,7 @@ function LockButton() {
$('#resetsort').removeClass('hand').addClass('nohand');
$('i.mover').hide();
$('#docker_list .sortable').css({'cursor':'default'});
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton').find('a').prop('title',"_(Unlock sortable items)_");
$('div.nav-item.LockButton').find('b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
<?endif;?>

View File

@@ -123,7 +123,7 @@ function hide_eth($network) {
return in_array($network,$mgmt_port) && lan_port('wlan0',true)==1;
}
$bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c';
$bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c'; // $themeHelper set in DefaultPageLayout.php
//Check if docker.cfg does exist
$no_dockercfg = !is_file('/boot/config/docker.cfg');
@@ -146,11 +146,9 @@ _(Enable Docker)_:
<?=mk_option(_var($dockercfg,'DOCKER_ENABLED'), 'no', _('No'))?>
<?=mk_option(_var($dockercfg,'DOCKER_ENABLED'), 'yes', _('Yes'))?>
</select>
<?if (_var($var,'fsState')!="Started"):?>
<?if (_var($var,'fsState')!="Started"):?>
<span id="arraystopped"><i class="fa fa-warning icon warning"></i> <?=(_var($dockercfg,'DOCKER_ENABLED')=='yes') ? '_(Docker will be available after Array is Started)_' : '_(Apply to activate Docker after Array is Started)_'?></span>
<?elseif (!is_dir(dirname(_var($dockercfg,'DOCKER_IMAGE_FILE'))) || !is_dir(_var($dockercfg,'DOCKER_APP_CONFIG_PATH'))):?>
<span class="basic"><i class="fa fa-warning icon warning"></i> _(One or more paths do not exist)_ (<a href="#" onclick="$('.advancedview').switchButton('option','checked',true); return false">_(view)_</a>)</span>
<?endif;?>
<?endif;?>
:docker_enable_help:
@@ -192,9 +190,10 @@ _(Docker vDisk size)_:
_(Docker vDisk location)_:
: <input type="text" id="DOCKER_IMAGE_FILE1" name="DOCKER_IMAGE_FILE1" autocomplete="off" spellcheck="false" value="<?=htmlspecialchars(_var($dockercfg,'DOCKER_IMAGE_FILE'))?>" placeholder="_(e.g.)_ /mnt/user/system/docker.img" data-pickcloseonfile="true" data-pickfilter="img" data-pickroot="/mnt" data-pickfolders="true" disabled required pattern="^[^\\]*(docker-xfs\.img|docker\.img)$">
<span class="deleteLabel"><label><input type="checkbox" class="deleteCheckbox"> _(Delete vDisk file)_</label></span>
<?if ($var['fsState'] != "Started"):?><span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir(dirname(htmlspecialchars(_var($dockercfg,'DOCKER_IMAGE_FILE'))))):?><span class="nonexist"><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span>
<?endif;?><span id="IMAGE_ERROR1" class="errortext"></span>
<?if ($var['fsState'] != "Started"):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
<span id="IMAGE_ERROR1" class="errortext"></span>
:docker_vdisk_location_help:
@@ -203,9 +202,10 @@ _(Docker vDisk location)_:
_(Docker directory)_:
: <input type="text" id="DOCKER_IMAGE_FILE2" name="DOCKER_IMAGE_FILE2" autocomplete="off" spellcheck="false" value="<?=htmlspecialchars(_var($dockercfg,'DOCKER_IMAGE_FILE'))?>" placeholder="_(e.g.)_ /mnt/user/system/docker" data-pickcloseonfile="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="/mnt" data-pickfolders="true" disabled required pattern="^[^\\]*/$">
<span class="deleteLabel"><label><input type="checkbox" class="deleteCheckbox"> _(Delete directory)_</label></span>
<?if ($var['fsState'] != "Started"):?><span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir(dirname(_var($dockercfg,'DOCKER_IMAGE_FILE')))):?><span class="nonexist"><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span>
<?endif;?><span id="IMAGE_ERROR2" class="errortext"></span>
<?if ($var['fsState'] != "Started"):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
<span id="IMAGE_ERROR2" class="errortext"></span>
:docker_vdisk_directory_help:
@@ -217,11 +217,9 @@ _(Docker storage driver)_:
<?=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'overlay2', _('overlay2'))?>
<?=mk_option(_var($dockercfg,'DOCKER_BACKINGFS'), 'native', _('native'))?>
</select>
<?if ($var['fsState'] != "Started"):?>
<?if ($var['fsState'] != "Started"):?>
<span id="WARNING_BACKINGFS" style="display:none;"><i class="fa fa-warning icon warning"></i>_(Only modify if this is a new installation since this can lead to unwanted behaviour!)_</span>
<?elseif (is_dir(_var($dockercfg,'DOCKER_IMAGE_FILE'))):?>
<span id="WARNING_BACKINGFS" style="display:none;"><i class="fa fa-warning icon warning"></i>_(Switching the Storage Driver requires to delete and rebuild the Docker directory manually!)_</span>
<?endif;?>
<?endif;?>
:docker_storage_driver_help:
@@ -229,11 +227,9 @@ _(Docker storage driver)_:
_(Default appdata storage location)_:
: <input type="text" id="DOCKER_APP_CONFIG_PATH" name="DOCKER_APP_CONFIG_PATH" autocomplete="off" spellcheck="false" value="<?=htmlspecialchars(_var($dockercfg,'DOCKER_APP_CONFIG_PATH'))?>" placeholder="_(e.g.)_ /mnt/user/appdata/" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="/mnt" data-pickfolders="true" pattern="^[^\\]*/$">
<?if ($var['fsState'] != "Started"):?>
<?if ($var['fsState'] != "Started"):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir(_var($dockercfg,'DOCKER_APP_CONFIG_PATH'))):?>
<span class="nonexist"><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span>
<?endif;?>
<?endif;?>
:docker_appdata_location_help:

View File

@@ -318,7 +318,8 @@ $authoringMode = $dockercfg['DOCKER_AUTHORING_MODE'] == "yes" ? true : false;
$authoring = $authoringMode ? 'advanced' : 'noshow';
$disableEdit = $authoringMode ? 'false' : 'true';
$showAdditionalInfo = '';
$bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c';
$bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c'; // $themeHelper set in DefaultPageLayout.php
# Search for existing TAILSCALE_ entries in the Docker template
$TS_existing_vars = false;

View File

@@ -32,7 +32,7 @@ var browserName = (function(agent){
})(window.navigator.userAgent.toLowerCase());
$(function(){
<?if ($themes2):?>
<?if ($themeHelper->isSidebarTheme()):?>
$('.nav-item.gui_search').hover(function(){gui_search();},function(e){closeSearchBox(e);});
<?endif;?>
$.post('/plugins/dynamix.gui.search/include/exec.php',function(data) {
@@ -51,7 +51,7 @@ function setupGUIsearch() {
window.addEventListener('keydown',function(e){
if (!e.shiftKey && !e.altKey && (navigator.appVersion.indexOf('Mac')==-1 ? e.ctrlKey : e.metaKey) && e.keyCode==75) {
e.preventDefault();
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
if (guiSearchBoxSpan()) closeSearchBox(e); else gui_search();
<?endif;?>
}
@@ -63,7 +63,7 @@ function setupGUIsearch() {
}
function gui_search() {
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
languageVisible = $('.nav-item.LanguageButton').is(':visible');
$('.nav-tile.right').prepend("<?=$guiSearchBoxSpan?>").css('overflow','visible');
$('.nav-item.util,.nav-user.show').hide();
@@ -86,7 +86,7 @@ function gui_search() {
function closeSearchBox(e) {
e.stopPropagation();
$('#guiSearchBoxSpan').remove();
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('.nav-tile.right').css({'overflow-x':'auto','overflow-y':'hidden'});
$('.nav-item.util,.nav-user.show').show();
if (!languageVisible) $('.nav-item.LanguageButton').hide();

View File

@@ -1,6 +1,6 @@
{
".nuxt/nuxt-custom-elements/entries/unraid-components.client.mjs": {
"file": "_nuxt/unraid-components.client-LDESjq-u.js",
"file": "_nuxt/unraid-components.client-Chq1f4tv.js",
"name": "unraid-components.client",
"src": ".nuxt/nuxt-custom-elements/entries/unraid-components.client.mjs",
"isEntry": true,
@@ -8,5 +8,5 @@
"_nuxt/unraid-components-BvEqdEb_.css"
]
},
"ts": 1744139284
"ts": 1746122627
}

View File

@@ -15,36 +15,50 @@ Tag="download"
*/
?>
<script>
const my = {};
const my = {};
function installPlugin(file) {
if (file == null) {
$('#plugin_file').val('');
openPlugin(my.cmd,my.title,my.plg);
return;
}
file = file.trim();
if (!file) return;
$.get('/plugins/dynamix.plugin.manager/include/ShowPlugins.php',{cmd:'alert'},function(data) {
my.cmd = 'plugin install '+file;
my.title = "_(Install Plugin)_";
my.plg = file.replace(/^.*(\\|\/|\:)/,'').replace('.plg','')+':install';
if (data==0) {
// no alert message - proceed with install
setTimeout(installPlugin);
} else {
// show alert message and ask for confirmation
openAlert("showchanges <?=$alerts?>","<?=_('Alert Message')?>",'installPlugin');
function installPlugin(file) {
if (file == null) {
$('#plugin_file').val('');
openPlugin(my.cmd, my.title, my.plg);
return;
}
file = file.trim();
if (!file) return;
$.get('/plugins/dynamix.plugin.manager/include/ShowPlugins.php', {
cmd: 'alert'
}, function(data) {
const forceInstall = $('#force_install').prop('checked');
my.cmd = 'plugin install ' + file + (forceInstall ? ' force' : '');
my.title = "_(Install Plugin)_";
my.plg = file.replace(/^.*(\\|\/|\:)/, '').replace('.plg', '') + ':install';
if (data == 0) {
// no alert message - proceed with install
setTimeout(installPlugin);
} else {
// show alert message and ask for confirmation
openAlert("showchanges <?= $alerts ?>", "<?= _('Alert Message') ?>", 'installPlugin');
}
});
}
$(function() {
$('form[name="plugin_install"]').on('submit', function(e) {
e.preventDefault();
installPlugin($('#plugin_file').val());
});
});
}
</script>
**_(Enter URL of remote plugin file or local plugin file)_**
<form name="plugin_install" method="POST" target="progressFrame">
<input type="text" name="file" id="plugin_file" maxlength="1024" value="" style="width:33%">
<input type="button" value="_(Install)_" onclick="installPlugin(this.form.file.value)">
<input type="text" name="file" id="plugin_file" maxlength="1024" value="" style="width:33%">
<label style="margin-right: 10px;" title="_(Allows you to install the same or older version of a plugin)_">
<input type="checkbox" id="force_install" name="force_install">
_(Force Install)_
</label>
<input type="submit" value="_(Install)_">
</form>
:plugin_install_help:

View File

@@ -22,12 +22,13 @@ $docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
$is_intel_cpu = is_intel_cpu();
$core_types = $is_intel_cpu ? get_intel_core_types() : [];
$themeName = $themeHelper->getThemeName();
$cpus = cpu_list();
$hover = in_array($theme,['white','azure']) ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)';
$bgcolor = in_array($theme,['white','azure']) ? '#f2f2f2' : '#1c1c1c';
$fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
$incolor = $theme!='gray' ? $bgcolor : '#121510';
$hover = $themeHelper->isLightTheme() ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.1)';
$bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c';
$fgcolor = $themeHelper->isLightTheme() ? '#1c1c1c' : '#f2f2f2';
$incolor = $themeName !== 'gray' ? $bgcolor : '#121510';
function showCPUs($uuid) {
global $cpus,$is_intel_cpu,$core_types;
@@ -406,7 +407,7 @@ function LockButton() {
$('#resetsort').removeClass('nohand').addClass('hand');
$('i.mover').show();
$('#kvm_list .sortable').css({'cursor':'move'});
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton a').prop('title',"_(Lock sortable items)_");
$('div.nav-item.LockButton b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
<?endif;?>
@@ -429,7 +430,7 @@ function LockButton() {
$('#resetsort').removeClass('hand').addClass('nohand');
$('i.mover').hide();
$('#kvm_list .sortable').css({'cursor':'default'});
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton a').prop('title',"_(Unlock sortable items)_");
$('div.nav-item.LockButton b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
<?endif;?>

View File

@@ -4,8 +4,8 @@ Icon="icon-virtualization"
Tag="columns"
---
<?PHP
/* Copyright 2005-2023, Lime Technology
* Copyright 2012-2023, Bergware International.
/* Copyright 2005-2025, Lime Technology
* Copyright 2012-2025, Bergware International.
* Copyright 2015-2021, Derek Macias, Eric Schultz, Jon Panozzo.
*
* This program is free software; you can redistribute it and/or
@@ -92,7 +92,7 @@ if (is_file('/boot/syslinux/syslinux.cfg')) {
$arrValidNetworks = getValidNetworks();
$pcie_acs_override = detect($bootcfg, $bootenv, 'pcie_acs_override');
$vfio_allow_unsafe = detect($bootcfg, $bootenv, 'allow_unsafe_interrupts');
$bgcolor = strstr('white,azure',$display['theme']) ? '#f2f2f2' : '#1c1c1c';
$bgcolor = $themeHelper->isLightTheme() ? '#f2f2f2' : '#1c1c1c'; // $themeHelper set in DefaultPageLayout.php
$started = $var['fsState']=='Started';
$libvirt_up = $libvirt_running=='yes';
$libvirt_log = file_exists("/var/log/libvirt/libvirtd.log");
@@ -112,13 +112,11 @@ _(Enable VMs)_:
<?= mk_option($libvirt_service, 'disable', _('No'))?>
<?= mk_option($libvirt_service, 'enable', _('Yes'))?>
</select>
<?if ($hardware):?>
<?if (!$started):?>
<?if ($hardware):?>
<?if (!$started):?>
<span id="arraystopped"><i class="fa fa-warning icon warning"></i> <?=($libvirt_service=='enable')?'_(VMs will be available after Array is Started)_':'_(Apply to activate VMs after Array is Started)_'?></span>
<?elseif (!is_dir(dirname($domain_cfg['IMAGE_FILE'])) || !is_dir($domain_cfg['DOMAINDIR']) || !is_dir($domain_cfg['MEDIADIR'])):?>
<span class="basic" style="display:inline"><i class="fa fa-warning icon warning"></i> _(One or more paths do not exist)_ (<a href="#" onclick="$('.advancedview').switchButton('option','checked',true); return false">_(view)_</a>)</span>
<?endif;?>
<?endif;?>
<?endif;?>
<?endif;?>
:vms_enable_help:
@@ -154,25 +152,29 @@ _(Libvirt vdisk size)_:
_(Libvirt storage location)_:
: <input type="text" id="IMAGE_FILE" name="IMAGE_FILE" autocomplete="off" spellcheck="false" value="<?=htmlspecialchars($domain_cfg['IMAGE_FILE']);?>" placeholder="e.g. /mnt/user/system/libvirt/libvirt.img" data-pickcloseonfile="true" data-pickfilter="img" data-pickroot="/mnt" data-pickfolders="true" required pattern="^[^\\]*libvirt\.img$">
<?if (file_exists($domain_cfg['IMAGE_FILE'])):?><span id="deletePanel"><label><input type="checkbox" id="deleteCheckbox" /> _(Delete Image File)_</label></span><?endif;?>
<?if (!$started):?><span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir(dirname($domain_cfg['IMAGE_FILE']))):?><span><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span>
<?endif;?><span id="IMAGE_ERROR" class="errortext"></span>
<?if (file_exists($domain_cfg['IMAGE_FILE'])):?>
<span id="deletePanel"><label><input type="checkbox" id="deleteCheckbox" /> _(Delete Image File)_</label></span><?endif;?>
<?if (!$started):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
<span id="IMAGE_ERROR" class="errortext"></span>
:vms_libvirt_location_help:
<?endif;?>
_(Default VM storage path)_:
: <input type="text" id="domaindir" name="DOMAINDIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="/mnt" value="<?=htmlspecialchars($domain_cfg['DOMAINDIR'])?>" placeholder="_(Click to Select)_" pattern="^[^\\]*/$">
<?if (!$started):?><span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir($domain_cfg['DOMAINDIR'])):?><span><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span><?endif;?>
<?if (!$started):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
:vms_libvirt_storage_help:
_(Default ISO storage path)_:
: <input type="text" id="mediadir" name="MEDIADIR" autocomplete="off" spellcheck="false" data-pickfolders="true" data-pickfilter="HIDE_FILES_FILTER" data-pickroot="<?=is_dir('/mnt/user')?'/mnt/user':'/mnt'?>" value="<?=htmlspecialchars($domain_cfg['MEDIADIR'])?>" placeholder="_(Click to Select)_" pattern="^[^\\]*/$">
<?if (!$started):?><span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?elseif (!is_dir($domain_cfg['MEDIADIR'])):?><span><i class="fa fa-warning icon warning"></i> _(Path does not exist)_</span><?endif;?>
<?if (!$started):?>
<span><i class="fa fa-warning icon warning"></i> _(Modify with caution: unable to validate path until Array is Started)_</span>
<?endif;?>
:vms_libvirt_iso_storage_help:

View File

@@ -39,6 +39,7 @@ $i = 0;
$kvm = ['var kvm=[];'];
$show = explode(',',unscript(_var($_GET,'show')));
$path = _var($domain_cfg,'MEDIADIR');
$pci_device_changes = comparePCIData();
foreach ($vms as $vm) {
$res = $lv->get_domain_by_name($vm);
@@ -52,6 +53,11 @@ foreach ($vms as $vm) {
$image = substr($icon,-4)=='.png' ? "<img src='$icon' class='img'>" : (substr($icon,0,5)=='icon-' ? "<i class='$icon img'></i>" : "<i class='fa fa-$icon img'></i>");
$arrConfig = domain_to_config($uuid);
$snapshots = getvmsnapshots($vm) ;
$vmpciids = $lv->domain_get_vm_pciids($vm);
$pcierror = false;
foreach($vmpciids as $pciid => $pcidetail) {
if (isset($pci_device_changes["0000:".$pciid])) $pcierror = true;
}
$cdroms = $lv->get_cdrom_stats($res,true,true) ;
if ($state == 'running') {
$mem = $dom['memory']/1024;
@@ -117,7 +123,7 @@ foreach ($vms as $vm) {
unset($dom);
if (!isset($domain_cfg["CONSOLE"])) $vmrcconsole = "web" ; else $vmrcconsole = $domain_cfg["CONSOLE"] ;
if (!isset($domain_cfg["RDPOPT"])) $vmrcconsole .= ";no" ; else $vmrcconsole .= ";".$domain_cfg["RDPOPT"] ;
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s', '%s')\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)));
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s', '%s', %s)\"", addslashes($vm),addslashes($uuid),addslashes($template),$state,addslashes($vmrcurl),strtoupper($vmrcprotocol),addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)),$pcierror);
$kvm[] = "kvm.push({id:'$uuid',state:'$state'});";
switch ($state) {
case 'running':
@@ -196,7 +202,10 @@ foreach ($vms as $vm) {
$title = _('Select ISO image');
$cdstr = $cdromcount." / 2<a class='hand' title='$title' href='#' onclick='$changemedia'><i class='fa fa-dot-circle-o'></i></a>";
echo "<tr parent-id='$i' class='sortable'><td class='vm-name' style='width:220px;padding:8px'><i class='fa fa-arrows-v mover orange-text'></i>";
echo "<span class='outer'><span id='vm-$uuid' $menu class='hand'>$image</span><span class='inner'><a href='#' onclick='return toggle_id(\"name-$i\")' title='click for more VM info'>$vm</a><br><i class='fa fa-$shape $status $color'></i><span class='state'>"._($status)." </span></span></span></td>";
echo "<span class='outer'><span id='vm-$uuid' $menu class='hand'>$image</span>";
echo "<span class='inner'><a href='#' onclick='return toggle_id(\"name-$i\")' title='click for more VM info'>$vm</a>";
if ($pcierror) echo "<i class=\"fa fa-warning fa-fw orange-text\" title=\""._('PCI Changed')."\n"._('Start disabled')."\"></i>";
echo "<br><i class='fa fa-$shape $status $color'></i><span class='state'>"._($status)." </span></span></span></td>";
echo "<td>$desc</td>";
echo "<td><a class='vcpu-$uuid' style='cursor:pointer'>$vcpu</a></td>";
echo "<td>$mem</td>";

View File

@@ -22,7 +22,7 @@ if (substr($_SERVER['REQUEST_URI'],0,4) != '/VMs') {
require_once "$docroot/webGui/include/Translations.php";
}
switch ($display['theme']) {
switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout.php
case 'gray' : $bgcolor = '#121510'; $border = '#606e7f'; $top = -44; break;
case 'azure': $bgcolor = '#edeaef'; $border = '#606e7f'; $top = -44; break;
case 'black': $bgcolor = '#212121'; $border = '#2b2b2b'; $top = -64; break;
@@ -110,6 +110,7 @@ if (strpos($strSelectedTemplate,"User-") !== false) {
<form id="vmform" method="POST">
<input type="hidden" name="domain[type]" value="kvm" />
<input type="hidden" name="template[name]" value="<?=htmlspecialchars($strSelectedTemplateUT)?>" />
<input type="hidden" name="template[iconold]" value="<?=htmlspecialchars($arrLoad['icon'])?>" />
<table>
<tr>

View File

@@ -2257,6 +2257,18 @@ class Libvirt {
return ['pci' => $devs_pci, 'usb' => $devs_usb];
}
function domain_get_vm_pciids($domain) {
$hostdevs=$this->domain_get_host_devices_pci($domain);
$vmpcidevs=[];
foreach($hostdevs as $key => $dev) {
$vmpcidevs[$dev['id']] = [
'vendor_id' => ltrim($dev['vendor_id'] ?? "", '0x'),
'device_id' => ltrim($dev['product_id'] ?? "", '0x'),
];
}
return $vmpcidevs;
}
function get_nic_info($domain) {
$macs = $this->get_xpath($domain, "//domain/devices/interface/mac/@address", false);
if (!$macs) return $this->_set_last_error();

View File

@@ -83,7 +83,7 @@ function ajaxVMDispatchWebUI(params, spin){
}
},'json');
}
function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, fstype="QEMU",consolein="web;no",usage=false,webui=""){
function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, fstype="QEMU",consolein="web;no",usage=false,webui="",pcierror=false){
var opts = [];
var path = location.pathname;
var x = path.indexOf("?");
@@ -172,22 +172,28 @@ function addVMContext(name, uuid, template, state, vmrcurl, vmrcprotocol, log, f
ajaxVMDispatch({action:"domain-destroy", uuid:uuid}, "loadlist");
}});
} else {
opts.push({text:_("Start"), icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatch({action:"domain-start", uuid:uuid}, "loadlist");
}});
if (vmrcprotocol == "VNC" || vmrcprotocol == "SPICE") {
if (console == "web" || console == "both") {
opts.push({text:_("Start with console")+ " (" + vmrcprotocol + ")" , icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatchconsole({action:"domain-start-console", uuid:uuid, vmrcurl:vmrcurl}, "loadlist") ;
}});}
if (console == "remote" || console == "both") {
opts.push({text:_("Start with remote-viewer")+ " (" + vmrcprotocol + ")" , icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatchconsoleRV({action:"domain-start-consoleRV", uuid:uuid, vmrcurl:vmrcurl}, "loadlist") ;
}});
if (!pcierror) {
opts.push({text:_("Start"), icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatch({action:"domain-start", uuid:uuid}, "loadlist");
}});
if (vmrcprotocol == "VNC" || vmrcprotocol == "SPICE") {
if (console == "web" || console == "both") {
opts.push({text:_("Start with console")+ " (" + vmrcprotocol + ")" , icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatchconsole({action:"domain-start-console", uuid:uuid, vmrcurl:vmrcurl}, "loadlist") ;
}});}
if (console == "remote" || console == "both") {
opts.push({text:_("Start with remote-viewer")+ " (" + vmrcprotocol + ")" , icon:"fa-play", action:function(e) {
e.preventDefault();
ajaxVMDispatchconsoleRV({action:"domain-start-consoleRV", uuid:uuid, vmrcurl:vmrcurl}, "loadlist") ;
}});
}
}
} else {
opts.push({text:_("Start disabled due to PCI Changes"), icon:"fa fa-minus-circle orb red-orb", action:function(e) {
e.preventDefault();
}});
}
}
opts.push({divider:true});

View File

@@ -0,0 +1,34 @@
---
name: Bug report
about: Create a report to help us improve
---
**Describe the bug**
A clear and concise description of what the bug is.
**To reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Client (please complete the following information):**
- OS: [e.g. iOS]
- Browser: [e.g. chrome, safari]
- Browser version: [e.g. 22]
**Server (please complete the following information):**
- noVNC version: [e.g. 1.0.0 or git commit id]
- VNC server: [e.g. QEMU, TigerVNC]
- WebSocket proxy: [e.g. websockify]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question or discussion
url: https://groups.google.com/forum/?fromgroups#!forum/novnc
about: Ask a question or start a discussion

View File

@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,97 @@
name: Publish
on:
push:
pull_request:
release:
types: [published]
jobs:
npm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- uses: actions/setup-node@v4
with:
# Needs to be explicitly specified for auth to work
registry-url: 'https://registry.npmjs.org'
- run: npm install
- uses: actions/upload-artifact@v4
with:
name: npm
path: lib
- run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- run: npm publish --access public --tag beta
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- run: npm publish --access public --tag dev
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&
github.event.ref == 'refs/heads/master'
snap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: |
GITREV=$(git rev-parse --short HEAD)
echo $GITREV
sed -i "s/^\(.*\"version\".*\)\"\([^\"]\+\)\"\(.*\)\$/\1\"\2-g$GITREV\"\3/" package.json
if: github.event_name != 'release'
- run: |
VERSION=$(grep '"version"' package.json | cut -d '"' -f 4)
echo $VERSION
sed -i "s/^version:.*/version: '$VERSION'/" snap/snapcraft.yaml
- uses: snapcore/action-build@v1
id: snapcraft
- uses: actions/upload-artifact@v4
with:
name: snap
path: ${{ steps.snapcraft.outputs.snap }}
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: stable
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
!github.event.release.prerelease
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: beta
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'release' &&
github.event.release.prerelease
- uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snapcraft.outputs.snap }}
release: edge
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_LOGIN }}
if: |
github.repository == 'novnc/noVNC' &&
github.event_name == 'push' &&
github.event.ref == 'refs/heads/master'

View File

@@ -0,0 +1,19 @@
name: Lint
on: [push, pull_request]
jobs:
eslint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run lint
html:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: git ls-tree --name-only -r HEAD | grep -E "[.](html|css)$" | xargs ./utils/validate

View File

@@ -0,0 +1,28 @@
name: Test
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
os:
- ubuntu-latest
- windows-latest
browser:
- ChromeHeadless
- FirefoxHeadless
include:
- os: macos-latest
browser: Safari
- os: windows-latest
browser: EdgeHeadless
fail-fast: false
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: npm run test
env:
TEST_BROWSER_NAME: ${{ matrix.browser }}

View File

@@ -0,0 +1,15 @@
name: Translate
on: [push, pull_request]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm update
- run: sudo apt-get install gettext
- run: make -C po update-pot
- run: make -C po update-po
- run: make -C po update-js

View File

@@ -0,0 +1,12 @@
*.pyc
*.o
tests/data_*.js
utils/rebind.so
utils/websockify
/node_modules
/build
/lib
recordings
*.swp
*~
noVNC-*.tgz

View File

View File

@@ -0,0 +1,62 @@
noVNC is Copyright (C) 2022 The noVNC authors
(./AUTHORS)
The noVNC core library files are licensed under the MPL 2.0 (Mozilla
Public License 2.0). The noVNC core library is composed of the
Javascript code necessary for full noVNC operation. This includes (but
is not limited to):
core/**/*.js
app/*.js
test/playback.js
The HTML, CSS, font and images files that included with the noVNC
source distibution (or repository) are not considered part of the
noVNC core library and are licensed under more permissive licenses.
The intent is to allow easy integration of noVNC into existing web
sites and web applications.
The HTML, CSS, font and image files are licensed as follows:
*.html : 2-Clause BSD license
app/styles/*.css : 2-Clause BSD license
app/styles/Orbitron* : SIL Open Font License 1.1
(Copyright 2009 Matt McInerney)
app/images/ : Creative Commons Attribution-ShareAlike
http://creativecommons.org/licenses/by-sa/3.0/
Some portions of noVNC are copyright to their individual authors.
Please refer to the individual source files and/or to the noVNC commit
history: https://github.com/novnc/noVNC/commits/master
The are several files and projects that have been incorporated into
the noVNC core library. Here is a list of those files and the original
licenses (all MPL 2.0 compatible):
core/base64.js : MPL 2.0
core/des.js : Various BSD style licenses
vendor/pako/ : MIT
Any other files not mentioned above are typically marked with
a copyright/license header at the top of the file. The default noVNC
license is MPL-2.0.
The following license texts are included:
docs/LICENSE.MPL-2.0
docs/LICENSE.OFL-1.1
docs/LICENSE.BSD-3-Clause (New BSD)
docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)
vendor/pako/LICENSE (MIT)
Or alternatively the license texts may be found here:
http://www.mozilla.org/MPL/2.0/
http://scripts.sil.org/OFL
http://en.wikipedia.org/wiki/BSD_licenses
https://opensource.org/licenses/MIT

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -14,7 +14,7 @@
"Password is required": "Je vyžadováno heslo",
"noVNC encountered an error:": "noVNC narazilo na chybu:",
"Hide/Show the control bar": "Skrýt/zobrazit ovládací panel",
"Move/Drag Viewport": "Přesunout/přetáhnout výřez",
"Move/Drag viewport": "Přesunout/přetáhnout výřez",
"viewport drag": "přesun výřezu",
"Active Mouse Button": "Aktivní tlačítka myši",
"No mousebutton": "Žádné",
@@ -22,9 +22,9 @@
"Middle mousebutton": "Prostřední tlačítko myši",
"Right mousebutton": "Pravé tlačítko myši",
"Keyboard": "Klávesnice",
"Show Keyboard": "Zobrazit klávesnici",
"Show keyboard": "Zobrazit klávesnici",
"Extra keys": "Extra klávesy",
"Show Extra Keys": "Zobrazit extra klávesy",
"Show extra keys": "Zobrazit extra klávesy",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Přepnout Ctrl",
"Alt": "Alt",
@@ -45,13 +45,13 @@
"Clear": "Vymazat",
"Fullscreen": "Celá obrazovka",
"Settings": "Nastavení",
"Shared Mode": "Sdílený režim",
"View Only": "Pouze prohlížení",
"Clip to Window": "Přizpůsobit oknu",
"Scaling Mode:": "Přizpůsobení velikosti",
"Shared mode": "Sdílený režim",
"View only": "Pouze prohlížení",
"Clip to window": "Přizpůsobit oknu",
"Scaling mode:": "Přizpůsobení velikosti",
"None": "Žádné",
"Local Scaling": "Místní",
"Remote Resizing": "Vzdálené",
"Local scaling": "Místní",
"Remote resizing": "Vzdálené",
"Advanced": "Pokročilé",
"Repeater ID:": "ID opakovače",
"WebSocket": "WebSocket",
@@ -59,9 +59,9 @@
"Host:": "Hostitel:",
"Port:": "Port:",
"Path:": "Cesta",
"Automatic Reconnect": "Automatická obnova připojení",
"Reconnect Delay (ms):": "Zpoždění připojení (ms)",
"Show Dot when No Cursor": "Tečka místo chybějícího kurzoru myši",
"Automatic reconnect": "Automatická obnova připojení",
"Reconnect delay (ms):": "Zpoždění připojení (ms)",
"Show dot when no cursor": "Tečka místo chybějícího kurzoru myši",
"Logging:": "Logování:",
"Disconnect": "Odpojit",
"Connect": "Připojit",

View File

@@ -13,7 +13,7 @@
"Password is required": "Passwort ist erforderlich",
"noVNC encountered an error:": "Ein Fehler ist aufgetreten:",
"Hide/Show the control bar": "Kontrollleiste verstecken/anzeigen",
"Move/Drag Viewport": "Ansichtsfenster verschieben/ziehen",
"Move/Drag viewport": "Ansichtsfenster verschieben/ziehen",
"viewport drag": "Ansichtsfenster ziehen",
"Active Mouse Button": "Aktive Maustaste",
"No mousebutton": "Keine Maustaste",
@@ -21,9 +21,9 @@
"Middle mousebutton": "Mittlere Maustaste",
"Right mousebutton": "Rechte Maustaste",
"Keyboard": "Tastatur",
"Show Keyboard": "Tastatur anzeigen",
"Show keyboard": "Tastatur anzeigen",
"Extra keys": "Zusatztasten",
"Show Extra Keys": "Zusatztasten anzeigen",
"Show extra keys": "Zusatztasten anzeigen",
"Ctrl": "Strg",
"Toggle Ctrl": "Strg umschalten",
"Alt": "Alt",
@@ -44,13 +44,13 @@
"Clear": "Löschen",
"Fullscreen": "Vollbild",
"Settings": "Einstellungen",
"Shared Mode": "Geteilter Modus",
"View Only": "Nur betrachten",
"Clip to Window": "Auf Fenster begrenzen",
"Scaling Mode:": "Skalierungsmodus:",
"Shared mode": "Geteilter Modus",
"View only": "Nur betrachten",
"Clip to window": "Auf Fenster begrenzen",
"Scaling mode:": "Skalierungsmodus:",
"None": "Keiner",
"Local Scaling": "Lokales skalieren",
"Remote Resizing": "Serverseitiges skalieren",
"Local scaling": "Lokales skalieren",
"Remote resizing": "Serverseitiges skalieren",
"Advanced": "Erweitert",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
@@ -58,12 +58,17 @@
"Host:": "Server:",
"Port:": "Port:",
"Path:": "Pfad:",
"Automatic Reconnect": "Automatisch wiederverbinden",
"Reconnect Delay (ms):": "Wiederverbindungsverzögerung (ms):",
"Automatic reconnect": "Automatisch wiederverbinden",
"Reconnect delay (ms):": "Wiederverbindungsverzögerung (ms):",
"Logging:": "Protokollierung:",
"Disconnect": "Verbindung trennen",
"Connect": "Verbinden",
"Password:": "Passwort:",
"Cancel": "Abbrechen",
"Canvas not supported.": "Canvas nicht unterstützt."
"Canvas not supported.": "Canvas nicht unterstützt.",
"Disconnect timeout": "Zeitüberschreitung beim Trennen",
"Local Downscaling": "Lokales herunterskalieren",
"Local Cursor": "Lokaler Mauszeiger",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "'Clipping-Modus' aktiviert, Scrollbalken in 'IE-Vollbildmodus' werden nicht unterstützt",
"True Color": "True Color"
}

View File

@@ -41,6 +41,7 @@
"Reset": "Επαναφορά",
"Clipboard": "Πρόχειρο",
"Edit clipboard content in the textarea below.": "Επεξεργαστείτε το περιεχόμενο του πρόχειρου στην περιοχή κειμένου παρακάτω.",
"Full Screen": "Πλήρης Οθόνη",
"Settings": "Ρυθμίσεις",
"Shared Mode": "Κοινόχρηστη Λειτουργία",
"View Only": "Μόνο Θέαση",
@@ -75,5 +76,25 @@
"Username:": "Κωδικός Χρήστη:",
"Password:": "Κωδικός Πρόσβασης:",
"Send Credentials": "Αποστολή Διαπιστευτηρίων",
"Cancel": "Ακύρωση"
"Cancel": "Ακύρωση",
"Password is required": "Απαιτείται ο κωδικός πρόσβασης",
"viewport drag": "σύρσιμο θεατού πεδίου",
"Active Mouse Button": "Ενεργό Πλήκτρο Ποντικιού",
"No mousebutton": "Χωρίς Πλήκτρο Ποντικιού",
"Left mousebutton": "Αριστερό Πλήκτρο Ποντικιού",
"Middle mousebutton": "Μεσαίο Πλήκτρο Ποντικιού",
"Right mousebutton": "Δεξί Πλήκτρο Ποντικιού",
"Clear": "Καθάρισμα",
"Canvas not supported.": "Δεν υποστηρίζεται το στοιχείο Canvas",
"Disconnect timeout": "Παρέλευση χρονικού ορίου αποσύνδεσης",
"Local Downscaling": "Τοπική Συρρίκνωση",
"Local Cursor": "Τοπικός Δρομέας",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Εφαρμογή λειτουργίας αποκοπής αφού δεν υποστηρίζονται οι λωρίδες κύλισης σε πλήρη οθόνη στον IE",
"True Color": "Πραγματικά Χρώματα",
"Style:": "Στυλ:",
"default": "προεπιλεγμένο",
"Apply": "Εφαρμογή",
"Connection": "Σύνδεση",
"Token:": "Διακριτικό:",
"Send Password": "Αποστολή Κωδικού Πρόσβασης"
}

View File

@@ -10,7 +10,7 @@
"Disconnect timeout": "Tiempo de desconexión agotado",
"noVNC encountered an error:": "noVNC ha encontrado un error:",
"Hide/Show the control bar": "Ocultar/Mostrar la barra de control",
"Move/Drag Viewport": "Mover/Arrastrar la ventana",
"Move/Drag viewport": "Mover/Arrastrar la ventana",
"viewport drag": "Arrastrar la ventana",
"Active Mouse Button": "Botón activo del ratón",
"No mousebutton": "Ningún botón del ratón",
@@ -18,7 +18,7 @@
"Middle mousebutton": "Botón central del ratón",
"Right mousebutton": "Botón derecho del ratón",
"Keyboard": "Teclado",
"Show Keyboard": "Mostrar teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionales",
"Show Extra Keys": "Mostrar Teclas Adicionales",
"Ctrl": "Ctrl",
@@ -43,13 +43,13 @@
"Settings": "Configuraciones",
"Encrypt": "Encriptar",
"Shared Mode": "Modo Compartido",
"View Only": "Solo visualización",
"Clip to Window": "Recortar al tamaño de la ventana",
"Scaling Mode:": "Modo de escalado:",
"View only": "Solo visualización",
"Clip to window": "Recortar al tamaño de la ventana",
"Scaling mode:": "Modo de escalado:",
"None": "Ninguno",
"Local Scaling": "Escalado Local",
"Local Downscaling": "Reducción de escala local",
"Remote Resizing": "Cambio de tamaño remoto",
"Remote resizing": "Cambio de tamaño remoto",
"Advanced": "Avanzado",
"Local Cursor": "Cursor Local",
"Repeater ID:": "ID del Repetidor:",
@@ -57,8 +57,8 @@
"Host:": "Host:",
"Port:": "Puerto:",
"Path:": "Ruta:",
"Automatic Reconnect": "Reconexión automática",
"Reconnect Delay (ms):": "Retraso en la reconexión (ms):",
"Automatic reconnect": "Reconexión automática",
"Reconnect delay (ms):": "Retraso en la reconexión (ms):",
"Logging:": "Registrando:",
"Disconnect": "Desconectar",
"Connect": "Conectar",

View File

@@ -1,9 +1,10 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Lancer sans HTTPS n'est pas recommandé, crashs ou autres problèmes en vue.",
"Connecting...": "En cours de connexion...",
"Disconnecting...": "Déconnexion en cours...",
"Reconnecting...": "Reconnexion en cours...",
"Internal error": "Erreur interne",
"Must set host": "Doit définir l'hôte",
"Failed to connect to server: ": "Échec de connexion au serveur ",
"Connected (encrypted) to ": "Connecté (chiffré) à ",
"Connected (unencrypted) to ": "Connecté (non chiffré) à ",
"Something went wrong, connection is closed": "Quelque chose s'est mal passé, la connexion a été fermée",
@@ -15,19 +16,19 @@
"noVNC encountered an error:": "noVNC a rencontré une erreur :",
"Hide/Show the control bar": "Masquer/Afficher la barre de contrôle",
"Drag": "Faire glisser",
"Move/Drag Viewport": "Déplacer/faire glisser le Viewport",
"Move/Drag viewport": "Déplacer la fenêtre de visualisation",
"Keyboard": "Clavier",
"Show Keyboard": "Afficher le clavier",
"Show keyboard": "Afficher le clavier",
"Extra keys": "Touches supplémentaires",
"Show Extra Keys": "Afficher les touches supplémentaires",
"Show extra keys": "Afficher les touches supplémentaires",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Basculer Ctrl",
"Alt": "Alt",
"Toggle Alt": "Basculer Alt",
"Toggle Windows": "Basculer Windows",
"Windows": "Windows",
"Send Tab": "Envoyer l'onglet",
"Tab": "l'onglet",
"Windows": "Fenêtre",
"Send Tab": "Envoyer Tab",
"Tab": "Tabulation",
"Esc": "Esc",
"Send Escape": "Envoyer Escape",
"Ctrl+Alt+Del": "Ctrl+Alt+Del",
@@ -39,16 +40,16 @@
"Reboot": "Redémarrer",
"Reset": "Réinitialiser",
"Clipboard": "Presse-papiers",
"Clear": "Effacer",
"Fullscreen": "Plein écran",
"Edit clipboard content in the textarea below.": "Editer le contenu du presse-papier dans la zone ci-dessous.",
"Full screen": "Plein écran",
"Settings": "Paramètres",
"Shared Mode": "Mode partagé",
"View Only": "Afficher uniquement",
"Clip to Window": "Clip à fenêtre",
"Scaling Mode:": "Mode mise à l'échelle :",
"Shared mode": "Mode partagé",
"View only": "Afficher uniquement",
"Clip to window": "Ajuster à la fenêtre",
"Scaling mode:": "Mode mise à l'échelle :",
"None": "Aucun",
"Local Scaling": "Mise à l'échelle locale",
"Remote Resizing": "Redimensionnement à distance",
"Local scaling": "Mise à l'échelle locale",
"Remote resizing": "Redimensionnement à distance",
"Advanced": "Avancé",
"Quality:": "Qualité :",
"Compression level:": "Niveau de compression :",
@@ -58,15 +59,24 @@
"Host:": "Hôte :",
"Port:": "Port :",
"Path:": "Chemin :",
"Automatic Reconnect": "Reconnecter automatiquemen",
"Reconnect Delay (ms):": "Délai de reconnexion (ms) :",
"Show Dot when No Cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
"Automatic reconnect": "Reconnecter automatiquement",
"Reconnect delay (ms):": "Délai de reconnexion (ms) :",
"Show dot when no cursor": "Afficher le point lorsqu'il n'y a pas de curseur",
"Logging:": "Se connecter :",
"Version:": "Version :",
"Disconnect": "Déconnecter",
"Connect": "Connecter",
"Server identity": "Identité du serveur",
"The server has provided the following identifying information:": "Le serveur a fourni l'identification suivante :",
"Fingerprint:": "Empreinte digitale :",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "SVP, verifiez que l'information est correcte et pressez \"Accepter\". Sinon pressez \"Refuser\".",
"Approve": "Accepter",
"Reject": "Refuser",
"Credentials": "Envoyer les identifiants",
"Username:": "Nom d'utilisateur :",
"Password:": "Mot de passe :",
"Send Credentials": "Envoyer les identifiants",
"Cancel": "Annuler"
"Send credentials": "Envoyer les identifiants",
"Cancel": "Annuler",
"Must set host": "Doit définir l'hôte",
"Clear": "Effacer"
}

View File

@@ -15,7 +15,7 @@
"noVNC encountered an error:": "noVNC ha riscontrato un errore:",
"Hide/Show the control bar": "Nascondi/Mostra la barra di controllo",
"Keyboard": "Tastiera",
"Show Keyboard": "Mostra tastiera",
"Show keyboard": "Mostra tastiera",
"Extra keys": "Tasti Aggiuntivi",
"Show Extra Keys": "Mostra Tasti Aggiuntivi",
"Ctrl": "Ctrl",
@@ -40,9 +40,9 @@
"Clear": "Pulisci",
"Fullscreen": "Schermo intero",
"Settings": "Impostazioni",
"Shared Mode": "Modalità condivisa",
"Shared mode": "Modalità condivisa",
"View Only": "Sola Visualizzazione",
"Scaling Mode:": "Modalità di ridimensionamento:",
"Scaling mode:": "Modalità di ridimensionamento:",
"None": "Nessuna",
"Local Scaling": "Ridimensionamento Locale",
"Remote Resizing": "Ridimensionamento Remoto",

View File

@@ -1,13 +1,14 @@
{
"HTTPS is required for full functionality": "すべての機能を使用するにはHTTPS接続が必要です",
"Running without HTTPS is not recommended, crashes or other issues are likely.": "HTTPS接続なしで実行することは推奨されません。クラッシュしたりその他の問題が発生したりする可能性があります。",
"Connecting...": "接続しています...",
"Disconnecting...": "切断しています...",
"Reconnecting...": "再接続しています...",
"Internal error": "内部エラー",
"Must set host": "ホストを設定する必要があります",
"Failed to connect to server: ": "サーバーへの接続に失敗しました: ",
"Connected (encrypted) to ": "接続しました (暗号化済み): ",
"Connected (unencrypted) to ": "接続しました (暗号化されていません): ",
"Something went wrong, connection is closed": "何らかの問題で、接続が閉じられました",
"Something went wrong, connection is closed": "問題が発生したため、接続が閉じられました",
"Failed to connect to server": "サーバーへの接続に失敗しました",
"Disconnected": "切断しました",
"New connection has been rejected with reason: ": "新規接続は次の理由で拒否されました: ",
@@ -16,11 +17,11 @@
"noVNC encountered an error:": "noVNC でエラーが発生しました:",
"Hide/Show the control bar": "コントロールバーを隠す/表示する",
"Drag": "ドラッグ",
"Move/Drag Viewport": "ビューポートを移動/ドラッグ",
"Move/Drag viewport": "ビューポートを移動/ドラッグ",
"Keyboard": "キーボード",
"Show Keyboard": "キーボードを表示",
"Show keyboard": "キーボードを表示",
"Extra keys": "追加キー",
"Show Extra Keys": "追加キーを表示",
"Show extra keys": "追加キーを表示",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl キーをトグル",
"Alt": "Alt",
@@ -41,15 +42,15 @@
"Reset": "リセット",
"Clipboard": "クリップボード",
"Edit clipboard content in the textarea below.": "以下の入力欄からクリップボードの内容を編集できます。",
"Full Screen": "全画面表示",
"Full screen": "全画面表示",
"Settings": "設定",
"Shared Mode": "共有モード",
"View Only": "表示専用",
"Clip to Window": "ウィンドウにクリップ",
"Scaling Mode:": "スケーリングモード:",
"Shared mode": "共有モード",
"View only": "表示専用",
"Clip to window": "ウィンドウにクリップ",
"Scaling mode:": "スケーリングモード:",
"None": "なし",
"Local Scaling": "ローカルスケーリング",
"Remote Resizing": "リモートでリサイズ",
"Local scaling": "ローカルスケーリング",
"Remote resizing": "リモートでリサイズ",
"Advanced": "高度",
"Quality:": "品質:",
"Compression level:": "圧縮レベル:",
@@ -59,9 +60,9 @@
"Host:": "ホスト:",
"Port:": "ポート:",
"Path:": "パス:",
"Automatic Reconnect": "自動再接続",
"Reconnect Delay (ms):": "再接続する遅延 (ミリ秒):",
"Show Dot when No Cursor": "カーソルがないときにドットを表示する",
"Automatic reconnect": "自動再接続",
"Reconnect delay (ms):": "再接続する遅延 (ミリ秒):",
"Show dot when no cursor": "カーソルがないときにドットを表示する",
"Logging:": "ロギング:",
"Version:": "バージョン:",
"Disconnect": "切断",
@@ -75,6 +76,6 @@
"Credentials": "資格情報",
"Username:": "ユーザー名:",
"Password:": "パスワード:",
"Send Credentials": "資格情報を送信",
"Send credentials": "資格情報を送信",
"Cancel": "キャンセル"
}

View File

@@ -14,7 +14,7 @@
"Password is required": "비밀번호가 필요합니다.",
"noVNC encountered an error:": "noVNC에 오류가 발생했습니다:",
"Hide/Show the control bar": "컨트롤 바 숨기기/보이기",
"Move/Drag Viewport": "움직이기/드래그 뷰포트",
"Move/Drag viewport": "움직이기/드래그 뷰포트",
"viewport drag": "뷰포트 드래그",
"Active Mouse Button": "마우스 버튼 활성화",
"No mousebutton": "마우스 버튼 없음",
@@ -22,9 +22,9 @@
"Middle mousebutton": "중간 마우스 버튼",
"Right mousebutton": "오른쪽 마우스 버튼",
"Keyboard": "키보드",
"Show Keyboard": "키보드 보이기",
"Show keyboard": "키보드 보이기",
"Extra keys": "기타 키들",
"Show Extra Keys": "기타 키들 보이기",
"Show extra keys": "기타 키들 보이기",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl 켜기/끄기",
"Alt": "Alt",
@@ -45,13 +45,13 @@
"Clear": "지우기",
"Fullscreen": "전체화면",
"Settings": "설정",
"Shared Mode": "공유 모드",
"View Only": "보기 전용",
"Clip to Window": "창에 클립",
"Scaling Mode:": "스케일링 모드:",
"Shared mode": "공유 모드",
"View only": "보기 전용",
"Clip to window": "창에 클립",
"Scaling mode:": "스케일링 모드:",
"None": "없음",
"Local Scaling": "로컬 스케일링",
"Remote Resizing": "원격 크기 조절",
"Local scaling": "로컬 스케일링",
"Remote resizing": "원격 크기 조절",
"Advanced": "고급",
"Repeater ID:": "중계 ID",
"WebSocket": "웹소켓",
@@ -59,8 +59,8 @@
"Host:": "호스트:",
"Port:": "포트:",
"Path:": "위치:",
"Automatic Reconnect": "자동 재연결",
"Reconnect Delay (ms):": "재연결 지연 시간 (ms)",
"Automatic reconnect": "자동 재연결",
"Reconnect delay (ms):": "재연결 지연 시간 (ms)",
"Logging:": "로깅",
"Disconnect": "연결 해제",
"Connect": "연결",

View File

@@ -1,36 +1,32 @@
{
"Connecting...": "Verbinden...",
"Disconnecting...": "Verbinding verbreken...",
"Running without HTTPS is not recommended, crashes or other issues are likely.": "Het is niet aan te raden om zonder HTTPS te werken, crashes of andere problemen zijn dan waarschijnlijk.",
"Connecting...": "Aan het verbinden…",
"Disconnecting...": "Bezig om verbinding te verbreken...",
"Reconnecting...": "Opnieuw verbinding maken...",
"Internal error": "Interne fout",
"Must set host": "Host moeten worden ingesteld",
"Failed to connect to server: ": "Verbinding maken met server is mislukt",
"Connected (encrypted) to ": "Verbonden (versleuteld) met ",
"Connected (unencrypted) to ": "Verbonden (onversleuteld) met ",
"Something went wrong, connection is closed": "Er iets fout gelopen, verbinding werd verbroken",
"Failed to connect to server": "Verbinding maken met server is mislukt",
"Disconnected": "Verbinding verbroken",
"New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd omwille van de volgende reden: ",
"New connection has been rejected with reason: ": "Nieuwe verbinding is geweigerd met de volgende reden: ",
"New connection has been rejected": "Nieuwe verbinding is geweigerd",
"Password is required": "Wachtwoord is vereist",
"Credentials are required": "Inloggegevens zijn nodig",
"noVNC encountered an error:": "noVNC heeft een fout bemerkt:",
"Hide/Show the control bar": "Verberg/Toon de bedieningsbalk",
"Move/Drag Viewport": "Verplaats/Versleep Kijkvenster",
"viewport drag": "kijkvenster slepen",
"Active Mouse Button": "Actieve Muisknop",
"No mousebutton": "Geen muisknop",
"Left mousebutton": "Linker muisknop",
"Middle mousebutton": "Middelste muisknop",
"Right mousebutton": "Rechter muisknop",
"Drag": "Sleep",
"Move/Drag viewport": "Verplaats/Versleep Kijkvenster",
"Keyboard": "Toetsenbord",
"Show Keyboard": "Toon Toetsenbord",
"Show keyboard": "Toon Toetsenbord",
"Extra keys": "Extra toetsen",
"Show Extra Keys": "Toon Extra Toetsen",
"Show extra keys": "Toon Extra Toetsen",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl omschakelen",
"Alt": "Alt",
"Toggle Alt": "Alt omschakelen",
"Toggle Windows": "Windows omschakelen",
"Windows": "Windows",
"Toggle Windows": "Vensters omschakelen",
"Windows": "Vensters",
"Send Tab": "Tab Sturen",
"Tab": "Tab",
"Esc": "Esc",
@@ -44,30 +40,56 @@
"Reboot": "Herstarten",
"Reset": "Resetten",
"Clipboard": "Klembord",
"Clear": "Wissen",
"Fullscreen": "Volledig Scherm",
"Edit clipboard content in the textarea below.": "Edit de inhoud van het klembord in het tekstveld hieronder",
"Full screen": "Volledig Scherm",
"Settings": "Instellingen",
"Shared Mode": "Gedeelde Modus",
"View Only": "Alleen Kijken",
"Clip to Window": "Randen buiten venster afsnijden",
"Scaling Mode:": "Schaalmodus:",
"Shared mode": "Gedeelde Modus",
"View only": "Alleen Kijken",
"Clip to window": "Randen buiten venster afsnijden",
"Scaling mode:": "Schaalmodus:",
"None": "Geen",
"Local Scaling": "Lokaal Schalen",
"Remote Resizing": "Op Afstand Formaat Wijzigen",
"Local scaling": "Lokaal Schalen",
"Remote resizing": "Op Afstand Formaat Wijzigen",
"Advanced": "Geavanceerd",
"Quality:": "Kwaliteit:",
"Compression level:": "Compressieniveau:",
"Repeater ID:": "Repeater ID:",
"WebSocket": "WebSocket",
"Encrypt": "Versleutelen",
"Host:": "Host:",
"Port:": "Poort:",
"Path:": "Pad:",
"Automatic Reconnect": "Automatisch Opnieuw Verbinden",
"Reconnect Delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
"Show Dot when No Cursor": "Geef stip weer indien geen cursor",
"Automatic reconnect": "Automatisch Opnieuw Verbinden",
"Reconnect delay (ms):": "Vertraging voor Opnieuw Verbinden (ms):",
"Show dot when no cursor": "Geef stip weer indien geen cursor",
"Logging:": "Logmeldingen:",
"Version:": "Versie:",
"Disconnect": "Verbinding verbreken",
"Connect": "Verbinden",
"Server identity": "Serveridentiteit",
"The server has provided the following identifying information:": "De server geeft de volgende identificerende informatie:",
"Fingerprint:": "Vingerafdruk:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "Verifieer dat de informatie is correct en druk “OK”. Druk anders op “Afwijzen”.",
"Approve": "OK",
"Reject": "Afwijzen",
"Credentials": "Inloggegevens",
"Username:": "Gebruikersnaam:",
"Password:": "Wachtwoord:",
"Send credentials": "Stuur inloggegevens",
"Cancel": "Annuleren",
"Must set host": "Host moeten worden ingesteld",
"Password is required": "Wachtwoord is vereist",
"viewport drag": "kijkvenster slepen",
"Active Mouse Button": "Actieve Muisknop",
"No mousebutton": "Geen muisknop",
"Left mousebutton": "Linker muisknop",
"Middle mousebutton": "Middelste muisknop",
"Right mousebutton": "Rechter muisknop",
"Clear": "Wissen",
"Send Password": "Verzend Wachtwoord:",
"Cancel": "Annuleren"
"Disconnect timeout": "Timeout tijdens verbreken van verbinding",
"Local Downscaling": "Lokaal Neerschalen",
"Local Cursor": "Lokale Cursor",
"Canvas not supported.": "Canvas wordt niet ondersteund.",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "''Clipping mode' ingeschakeld, omdat schuifbalken in volledige-scherm-modus in IE niet worden ondersteund"
}

View File

@@ -21,9 +21,9 @@
"Middle mousebutton": "Środkowy przycisk myszy",
"Right mousebutton": "Prawy przycisk myszy",
"Keyboard": "Klawiatura",
"Show Keyboard": "Pokaż klawiaturę",
"Show keyboard": "Pokaż klawiaturę",
"Extra keys": "Przyciski dodatkowe",
"Show Extra Keys": "Pokaż przyciski dodatkowe",
"Show extra keys": "Pokaż przyciski dodatkowe",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Przełącz Ctrl",
"Alt": "Alt",
@@ -49,8 +49,8 @@
"Clip to Window": "Przytnij do Okna",
"Scaling Mode:": "Tryb Skalowania:",
"None": "Brak",
"Local Scaling": "Skalowanie lokalne",
"Remote Resizing": "Skalowanie zdalne",
"Local scaling": "Skalowanie lokalne",
"Remote resizing": "Skalowanie zdalne",
"Advanced": "Zaawansowane",
"Repeater ID:": "ID Repeatera:",
"WebSocket": "WebSocket",
@@ -58,12 +58,23 @@
"Host:": "Host:",
"Port:": "Port:",
"Path:": "Ścieżka:",
"Automatic Reconnect": "Automatycznie wznawiaj połączenie",
"Reconnect Delay (ms):": "Opóźnienie wznawiania (ms):",
"Automatic reconnect": "Automatycznie wznawiaj połączenie",
"Reconnect delay (ms):": "Opóźnienie wznawiania (ms):",
"Logging:": "Poziom logowania:",
"Disconnect": "Rozłącz",
"Connect": "Połącz",
"Password:": "Hasło:",
"Cancel": "Anuluj",
"Canvas not supported.": "Element Canvas nie jest wspierany."
"Canvas not supported.": "Element Canvas nie jest wspierany.",
"Disconnect timeout": "Timeout rozłączenia",
"Local Downscaling": "Downscaling lokalny",
"Local Cursor": "Lokalny kursor",
"Forcing clipping mode since scrollbars aren't supported by IE in fullscreen": "Wymuszam clipping mode ponieważ paski przewijania nie są wspierane przez IE w trybie pełnoekranowym",
"True Color": "True Color",
"Style:": "Styl:",
"default": "domyślny",
"Apply": "Zapisz",
"Connection": "Połączenie",
"Token:": "Token:",
"Send Password": "Wyślij Hasło"
}

View File

@@ -15,11 +15,11 @@
"noVNC encountered an error:": "O noVNC encontrou um erro:",
"Hide/Show the control bar": "Esconder/mostrar a barra de controles",
"Drag": "Arrastar",
"Move/Drag Viewport": "Mover/arrastar a janela",
"Move/Drag viewport": "Mover/arrastar a janela",
"Keyboard": "Teclado",
"Show Keyboard": "Mostrar teclado",
"Show keyboard": "Mostrar teclado",
"Extra keys": "Teclas adicionais",
"Show Extra Keys": "Mostar teclas adicionais",
"Show extra keys": "Mostar teclas adicionais",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Pressionar/soltar Ctrl",
"Alt": "Alt",
@@ -42,13 +42,13 @@
"Clear": "Limpar",
"Fullscreen": "Tela cheia",
"Settings": "Configurações",
"Shared Mode": "Modo compartilhado",
"View Only": "Apenas visualizar",
"Clip to Window": "Recortar à janela",
"Scaling Mode:": "Modo de dimensionamento:",
"Shared mode": "Modo compartilhado",
"View only": "Apenas visualizar",
"Clip to window": "Recortar à janela",
"Scaling mode:": "Modo de dimensionamento:",
"None": "Nenhum",
"Local Scaling": "Local",
"Remote Resizing": "Remoto",
"Local scaling": "Local",
"Remote resizing": "Remoto",
"Advanced": "Avançado",
"Quality:": "Qualidade:",
"Compression level:": "Nível de compressão:",
@@ -58,15 +58,15 @@
"Host:": "Host:",
"Port:": "Porta:",
"Path:": "Caminho:",
"Automatic Reconnect": "Reconexão automática",
"Reconnect Delay (ms):": "Atraso da reconexão (ms)",
"Show Dot when No Cursor": "Mostrar ponto quando não há cursor",
"Automatic reconnect": "Reconexão automática",
"Reconnect delay (ms):": "Atraso da reconexão (ms)",
"Show dot when no cursor": "Mostrar ponto quando não há cursor",
"Logging:": "Registros:",
"Version:": "Versão:",
"Disconnect": "Desconectar",
"Connect": "Conectar",
"Username:": "Nome de usuário:",
"Password:": "Senha:",
"Send Credentials": "Enviar credenciais",
"Send credentials": "Enviar credenciais",
"Cancel": "Cancelar"
}

View File

@@ -15,16 +15,16 @@
"noVNC encountered an error:": "Ошибка noVNC: ",
"Hide/Show the control bar": "Скрыть/Показать контрольную панель",
"Drag": "Переместить",
"Move/Drag Viewport": "Переместить окно",
"Move/Drag viewport": "Переместить окно",
"Keyboard": "Клавиатура",
"Show Keyboard": "Показать клавиатуру",
"Show keyboard": "Показать клавиатуру",
"Extra keys": "Дополнительные Кнопки",
"Show Extra Keys": "Показать Дополнительные Кнопки",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Переключение нажатия Ctrl",
"Toggle Ctrl": "Зажать Ctrl",
"Alt": "Alt",
"Toggle Alt": "Переключение нажатия Alt",
"Toggle Windows": "Переключение вкладок",
"Toggle Alt": "Зажать Alt",
"Toggle Windows": "Зажать Windows",
"Windows": "Вкладка",
"Send Tab": "Передать нажатие Tab",
"Tab": "Tab",
@@ -42,13 +42,13 @@
"Clear": "Очистить",
"Fullscreen": "Во весь экран",
"Settings": "Настройки",
"Shared Mode": "Общий режим",
"Shared mode": "Общий режим",
"View Only": "Только Просмотр",
"Clip to Window": "В окно",
"Scaling Mode:": "Масштаб:",
"Clip to window": "В окно",
"Scaling mode:": "Масштаб:",
"None": "Нет",
"Local Scaling": "Локльный масштаб",
"Remote Resizing": "Удаленная перенастройка размера",
"Local scaling": "Локальный масштаб",
"Remote resizing": "Удаленная перенастройка размера",
"Advanced": "Дополнительно",
"Quality:": "Качество",
"Compression level:": "Уровень Сжатия",
@@ -58,9 +58,9 @@
"Host:": "Сервер:",
"Port:": "Порт:",
"Path:": "Путь:",
"Automatic Reconnect": "Автоматическое переподключение",
"Reconnect Delay (ms):": "Задержка переподключения (мс):",
"Show Dot when No Cursor": "Показать точку вместо курсора",
"Automatic reconnect": "Автоматическое переподключение",
"Reconnect delay (ms):": "Задержка переподключения (мс):",
"Show dot when no cursor": "Показать точку вместо курсора",
"Logging:": "Лог:",
"Version:": "Версия",
"Disconnect": "Отключение",

View File

@@ -4,7 +4,6 @@
"Disconnecting...": "Kopplar ner...",
"Reconnecting...": "Återansluter...",
"Internal error": "Internt fel",
"Must set host": "Du måste specifiera en värd",
"Failed to connect to server: ": "Misslyckades att ansluta till servern: ",
"Connected (encrypted) to ": "Ansluten (krypterat) till ",
"Connected (unencrypted) to ": "Ansluten (okrypterat) till ",
@@ -17,11 +16,11 @@
"noVNC encountered an error:": "noVNC stötte på ett problem:",
"Hide/Show the control bar": "Göm/Visa kontrollbaren",
"Drag": "Dra",
"Move/Drag Viewport": "Flytta/Dra Vyn",
"Move/Drag viewport": "Flytta/Dra vyn",
"Keyboard": "Tangentbord",
"Show Keyboard": "Visa Tangentbord",
"Show keyboard": "Visa tangentbord",
"Extra keys": "Extraknappar",
"Show Extra Keys": "Visa Extraknappar",
"Show extra keys": "Visa extraknappar",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Växla Ctrl",
"Alt": "Alt",
@@ -42,15 +41,15 @@
"Reset": "Återställ",
"Clipboard": "Urklipp",
"Edit clipboard content in the textarea below.": "Redigera urklippets innehåll i fältet nedan.",
"Full Screen": "Fullskärm",
"Full screen": "Fullskärm",
"Settings": "Inställningar",
"Shared Mode": "Delat Läge",
"View Only": "Endast Visning",
"Clip to Window": "Begränsa till Fönster",
"Scaling Mode:": "Skalningsläge:",
"Shared mode": "Delat läge",
"View only": "Endast visning",
"Clip to window": "Begränsa till fönster",
"Scaling mode:": "Skalningsläge:",
"None": "Ingen",
"Local Scaling": "Lokal Skalning",
"Remote Resizing": "Ändra Storlek",
"Local scaling": "Lokal skalning",
"Remote resizing": "Ändra storlek",
"Advanced": "Avancerat",
"Quality:": "Kvalitet:",
"Compression level:": "Kompressionsnivå:",
@@ -60,9 +59,9 @@
"Host:": "Värd:",
"Port:": "Port:",
"Path:": "Sökväg:",
"Automatic Reconnect": "Automatisk Återanslutning",
"Reconnect Delay (ms):": "Fördröjning (ms):",
"Show Dot when No Cursor": "Visa prick när ingen muspekare finns",
"Automatic reconnect": "Automatisk återanslutning",
"Reconnect delay (ms):": "Fördröjning (ms):",
"Show dot when no cursor": "Visa prick när ingen muspekare finns",
"Logging:": "Loggning:",
"Version:": "Version:",
"Disconnect": "Koppla från",
@@ -76,6 +75,9 @@
"Credentials": "Användaruppgifter",
"Username:": "Användarnamn:",
"Password:": "Lösenord:",
"Send Credentials": "Skicka Användaruppgifter",
"Cancel": "Avbryt"
"Send credentials": "Skicka användaruppgifter",
"Cancel": "Avbryt",
"Must set host": "Du måste specifiera en värd",
"HTTPS is required for full functionality": "HTTPS krävs för full funktionalitet",
"Clear": "Rensa"
}

View File

@@ -23,7 +23,7 @@
"Keyboard": "Klavye",
"Show Keyboard": "Klavye Düzenini Göster",
"Extra keys": "Ekstra tuşlar",
"Show Extra Keys": "Ekstra tuşları göster",
"Show extra keys": "Ekstra tuşları göster",
"Ctrl": "Ctrl",
"Toggle Ctrl": "Ctrl Değiştir ",
"Alt": "Alt",

View File

@@ -1,31 +1,33 @@
{
"Running without HTTPS is not recommended, crashes or other issues are likely.": "不建议在没有 HTTPS 的情况下运行,可能会出现崩溃或出现其他问题。",
"Connecting...": "连接中...",
"Disconnecting...": "正在断开连接...",
"Reconnecting...": "重新连接中...",
"Internal error": "内部错误",
"Must set host": "必须设置主机",
"Failed to connect to server: ": "无法连接到服务器:",
"Connected (encrypted) to ": "已连接(已加密)到",
"Connected (unencrypted) to ": "已连接(未加密)到",
"Disconnecting...": "正在断开连接...",
"Something went wrong, connection is closed": "出了点问题,连接已关闭",
"Failed to connect to server": "无法连接到服务器",
"Disconnected": "已断开连接",
"Must set host": "必须设置主机",
"Reconnecting...": "新连接中...",
"Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"New connection has been rejected with reason: ": "新连接被拒绝,原因如下:",
"New connection has been rejected": "新连接已被拒绝",
"Credentials are required": "需要凭证",
"noVNC encountered an error:": "noVNC 遇到一个错误:",
"Hide/Show the control bar": "显示/隐藏控制栏",
"Move/Drag Viewport": "移动/拖动窗口",
"viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按键",
"No mousebutton": "禁用鼠标按键",
"Left mousebutton": "鼠标左键",
"Middle mousebutton": "鼠标中键",
"Right mousebutton": "鼠标右键",
"Drag": "拖动",
"Move/Drag viewport": "移动/拖动窗口",
"Keyboard": "键盘",
"Show Keyboard": "显示键盘",
"Show keyboard": "显示键盘",
"Extra keys": "额外按键",
"Show Extra Keys": "显示额外按键",
"Show extra keys": "显示额外按键",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切换 Ctrl",
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Alt": "Alt",
"Toggle Alt": "切换 Alt",
"Toggle Windows": "切换窗口",
"Windows": "窗口",
"Send Tab": "发送 Tab 键",
"Tab": "Tab",
"Esc": "Esc",
@@ -39,31 +41,53 @@
"Reboot": "重启",
"Reset": "重置",
"Clipboard": "剪贴板",
"Clear": "清除",
"Fullscreen": "全屏",
"Edit clipboard content in the textarea below.": "在下面的文本区域中编辑剪贴板内容。",
"Full screen": "全屏",
"Settings": "设置",
"Encrypt": "加密",
"Shared Mode": "分享模式",
"View Only": "仅查看",
"Clip to Window": "限制/裁切窗口大小",
"Scaling Mode:": "缩放模式:",
"Shared mode": "分享模式",
"View only": "仅查看",
"Clip to window": "限制/裁切窗口大小",
"Scaling mode:": "缩放模式:",
"None": "无",
"Local Scaling": "本地缩放",
"Local Downscaling": "降低本地尺寸",
"Remote Resizing": "远程调整大小",
"Local scaling": "本地缩放",
"Remote resizing": "远程调整大小",
"Advanced": "高级",
"Local Cursor": "本地光标",
"Quality:": "品质:",
"Compression level:": "压缩级别:",
"Repeater ID:": "中继站 ID",
"WebSocket": "WebSocket",
"Encrypt": "加密",
"Host:": "主机:",
"Port:": "端口:",
"Path:": "路径:",
"Automatic Reconnect": "自动重新连接",
"Reconnect Delay (ms):": "重新连接间隔 (ms)",
"Automatic reconnect": "自动重新连接",
"Reconnect delay (ms):": "重新连接间隔 (ms)",
"Show dot when no cursor": "无光标时显示点",
"Logging:": "日志级别:",
"Version:": "版本:",
"Disconnect": "断开连接",
"Connect": "连接",
"Server identity": "服务器身份",
"The server has provided the following identifying information:": "服务器提供了以下识别信息:",
"Fingerprint:": "指纹:",
"Please verify that the information is correct and press \"Approve\". Otherwise press \"Reject\".": "请核实信息是否正确,并按 “同意”,否则按 “拒绝”。",
"Approve": "同意",
"Reject": "拒绝",
"Credentials": "凭证",
"Username:": "用户名:",
"Password:": "密码:",
"Send credentials": "发送凭证",
"Cancel": "取消",
"Password is required": "请提供密码",
"Disconnect timeout": "超时断开",
"viewport drag": "窗口拖动",
"Active Mouse Button": "启动鼠标按键",
"No mousebutton": "禁用鼠标按键",
"Left mousebutton": "鼠标左键",
"Middle mousebutton": "鼠标中键",
"Right mousebutton": "鼠标右键",
"Clear": "清除",
"Local Downscaling": "降低本地尺寸",
"Local Cursor": "本地光标",
"Canvas not supported.": "不支持 Canvas。"
}

View File

@@ -14,7 +14,7 @@
"Password is required": "請提供密碼",
"noVNC encountered an error:": "noVNC 遇到一個錯誤:",
"Hide/Show the control bar": "顯示/隱藏控制列",
"Move/Drag Viewport": "拖放顯示範圍",
"Move/Drag viewport": "拖放顯示範圍",
"viewport drag": "顯示範圍拖放",
"Active Mouse Button": "啟用滑鼠按鍵",
"No mousebutton": "無滑鼠按鍵",
@@ -22,9 +22,9 @@
"Middle mousebutton": "滑鼠中鍵",
"Right mousebutton": "滑鼠右鍵",
"Keyboard": "鍵盤",
"Show Keyboard": "顯示鍵盤",
"Show keyboard": "顯示鍵盤",
"Extra keys": "額外按鍵",
"Show Extra Keys": "顯示額外按鍵",
"Show extra keys": "顯示額外按鍵",
"Ctrl": "Ctrl",
"Toggle Ctrl": "切換 Ctrl",
"Alt": "Alt",
@@ -45,13 +45,13 @@
"Clear": "清除",
"Fullscreen": "全螢幕",
"Settings": "設定",
"Shared Mode": "分享模式",
"View Only": "僅檢視",
"Clip to Window": "限制/裁切視窗大小",
"Scaling Mode:": "縮放模式:",
"Shared mode": "分享模式",
"View only": "僅檢視",
"Clip to window": "限制/裁切視窗大小",
"Scaling mode:": "縮放模式:",
"None": "無",
"Local Scaling": "本機縮放",
"Remote Resizing": "遠端調整大小",
"Local scaling": "本機縮放",
"Remote resizing": "遠端調整大小",
"Advanced": "進階",
"Repeater ID:": "中繼站 ID",
"WebSocket": "WebSocket",
@@ -59,8 +59,8 @@
"Host:": "主機:",
"Port:": "連接埠:",
"Path:": "路徑:",
"Automatic Reconnect": "自動重新連線",
"Reconnect Delay (ms):": "重新連線間隔 (ms)",
"Automatic reconnect": "自動重新連線",
"Reconnect delay (ms):": "重新連線間隔 (ms)",
"Logging:": "日誌級別:",
"Disconnect": "中斷連線",
"Connect": "連線",

View File

@@ -1,13 +1,13 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*/
/*
* Localization Utilities
* Localization utilities
*/
export class Localizer {

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,30 @@
/*
* noVNC general CSS constant variables
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/* ---------- COLORS ----------- */
:root {
--novnc-grey: rgb(128, 128, 128);
--novnc-lightgrey: rgb(192, 192, 192);
--novnc-darkgrey: rgb(92, 92, 92);
/* Transparent to make button colors adapt to the background */
--novnc-buttongrey: rgba(192, 192, 192, 0.5);
--novnc-blue: rgb(110, 132, 163);
--novnc-lightblue: rgb(74, 144, 217);
--novnc-darkblue: rgb(83, 99, 122);
--novnc-green: rgb(0, 128, 0);
--novnc-yellow: rgb(255, 255, 0);
}
/* ------ MISC PROPERTIES ------ */
:root {
--input-xpadding: 1em;
}

View File

@@ -1,32 +1,170 @@
/*
* noVNC general input element CSS
* Copyright (C) 2022 The noVNC Authors
* Copyright (C) 2025 The noVNC authors
* noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
* This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
*/
/*
* Common for all inputs
*/
input, input::file-selector-button, button, select, textarea {
/* Respect standard font settings */
font: inherit;
/* ------- SHARED BETWEEN INPUT ELEMENTS -------- */
/* Disable default rendering */
appearance: none;
background: none;
input,
textarea,
button,
select,
input::file-selector-button {
padding: 0.5em var(--input-xpadding);
border-radius: 6px;
appearance: none;
text-overflow: ellipsis;
padding: 5px;
border: 1px solid rgb(192, 192, 192);
border-radius: 5px;
color: black;
--bg-gradient: linear-gradient(to top, rgb(255, 255, 255) 80%, rgb(240, 240, 240));
background-image: var(--bg-gradient);
/* Respect standard font settings */
font: inherit;
line-height: 1.6;
}
input:disabled,
textarea:disabled,
button:disabled,
select:disabled,
label[disabled] {
opacity: 0.4;
}
/*
* Buttons
*/
input:focus-visible,
textarea:focus-visible,
button:focus-visible,
select:focus-visible,
input:focus-visible::file-selector-button {
outline: 2px solid var(--novnc-lightblue);
outline-offset: 1px;
}
/* ------- TEXT INPUT -------- */
input:not([type]),
input[type=date],
input[type=datetime-local],
input[type=email],
input[type=month],
input[type=number],
input[type=password],
input[type=search],
input[type=tel],
input[type=text],
input[type=time],
input[type=url],
input[type=week],
textarea {
border: 1px solid var(--novnc-lightgrey);
/* Account for borders on text inputs, buttons dont have borders */
padding: calc(0.5em - 1px) var(--input-xpadding);
}
input:not([type]):focus-visible,
input[type=date]:focus-visible,
input[type=datetime-local]:focus-visible,
input[type=email]:focus-visible,
input[type=month]:focus-visible,
input[type=number]:focus-visible,
input[type=password]:focus-visible,
input[type=search]:focus-visible,
input[type=tel]:focus-visible,
input[type=text]:focus-visible,
input[type=time]:focus-visible,
input[type=url]:focus-visible,
input[type=week]:focus-visible,
textarea:focus-visible {
outline-offset: -1px;
}
textarea {
margin: unset; /* Remove Firefox's built in margin */
/* Prevent layout from shifting when scrollbars show */
scrollbar-gutter: stable;
/* Make textareas show at minimum one line. This does not work when
using box-sizing border-box, in which case, vertical padding and
border width needs to be taken into account. */
min-height: 1lh;
vertical-align: baseline; /* Firefox gives "text-bottom" by default */
}
/* ------- NUMBER PICKERS ------- */
/* We can't style the number spinner buttons:
https://github.com/w3c/csswg-drafts/issues/8777 */
input[type=number]::-webkit-inner-spin-button,
input[type=number]::-webkit-outer-spin-button {
/* Get rid of increase/decrease buttons in WebKit */
appearance: none;
}
input[type=number] {
/* Get rid of increase/decrease buttons in Firefox */
appearance: textfield;
}
/* ------- BUTTON ACTIVATIONS -------- */
/* A color overlay that depends on the activation level. The level can then be
set for different states on an element, for example hover and click on a
<button>. */
input, button, select, option,
input::file-selector-button,
.button-activations {
--button-activation-level: 0;
/* Note that CSS variables aren't functions, beware when inheriting */
--button-activation-alpha: calc(0.08 * var(--button-activation-level));
/* FIXME: We want the image() function instead of the linear-gradient()
function below. But it's not supported in the browsers yet. */
--button-activation-overlay:
linear-gradient(rgba(0, 0, 0, var(--button-activation-alpha))
100%, transparent);
--button-activation-overlay-light:
linear-gradient(rgba(255, 255, 255, calc(0.23 * var(--button-activation-level)))
100%, transparent);
}
.button-activations {
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
}
/* When we want the light overlay on activations instead.
This is best used on elements with darker backgrounds. */
.button-activations.light-overlay {
background-image: var(--button-activation-overlay-light);
/* Can't use the normal blend mode since that gives washed out colors. */
/* FIXME: For elements with these activation overlays we'd like only
the luminosity to change. The proprty "background-blend-mode" set
to "luminosity" sounds good, but it doesn't work as intended,
see: https://bugzilla.mozilla.org/show_bug.cgi?id=1806417 */
background-blend-mode: overlay;
}
input:hover, button:hover, select:hover, option:hover,
input::file-selector-button:hover,
.button-activations:hover {
--button-activation-level: 1;
}
/* Unfortunately we have to disable the :hover effect on touch devices,
otherwise the style lingers after tapping the button. */
@media (any-pointer: coarse) {
input:hover, button:hover, select:hover, option:hover,
input::file-selector-button:hover,
.button-activations:hover {
--button-activation-level: 0;
}
}
input:active, button:active, select:active, option:active,
input::file-selector-button:active,
.button-activations:active {
--button-activation-level: 2;
}
input:disabled, button:disabled, select:disabled, select:disabled option,
input:disabled::file-selector-button,
.button-activations:disabled {
--button-activation-level: 0;
}
/* ------- BUTTONS -------- */
input[type=button],
input[type=color],
input[type=image],
@@ -35,226 +173,15 @@ input[type=submit],
input::file-selector-button,
button,
select {
border-bottom-width: 2px;
/* This avoids it jumping around when :active */
vertical-align: middle;
margin-top: 0;
padding-left: 20px;
padding-right: 20px;
/* Disable Chrome's touch tap highlight */
-webkit-tap-highlight-color: transparent;
}
/*
* Select dropdowns
*/
select {
--select-arrow: url('data:image/svg+xml;utf8, \
<svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
xmlns="http://www.w3.org/2000/svg"> \
<path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
stroke="rgb(31,31,31)" fill="none" \
stroke-linecap="round" stroke-linejoin="round" /> \
</svg>');
background-image: var(--select-arrow), var(--bg-gradient);
background-position: calc(100% - 7px), left top;
background-repeat: no-repeat;
padding-right: calc(2*7px + 8px);
padding-left: 7px;
}
/* FIXME: :active isn't set when the <select> is opened in Firefox:
https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
select:active {
/* Rotated arrow */
background-image: url('data:image/svg+xml;utf8, \
<svg width="8" height="6" version="1.1" viewBox="0 0 8 6" \
xmlns="http://www.w3.org/2000/svg" transform="rotate(180)" > \
<path d="m6.5 1.5 -2.5 3 -2.5 -3 5 0" stroke-width="3" \
stroke="rgb(31,31,31)" fill="none" \
stroke-linecap="round" stroke-linejoin="round" /> \
</svg>'), var(--bg-gradient);
}
option {
color: black;
background: white;
}
/*
* Checkboxes
*/
input[type=checkbox] {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: white;
background-image: unset;
border: 1px solid dimgrey;
border-radius: 3px;
width: 13px;
height: 13px;
padding: 0;
margin-right: 6px;
vertical-align: bottom;
transition: 0.2s background-color linear;
}
input[type=checkbox]:checked {
background-color: rgb(110, 132, 163);
border-color: rgb(110, 132, 163);
}
input[type=checkbox]:checked::after {
content: "";
display: block; /* width & height doesn't work on inline elements */
width: 3px;
height: 7px;
border: 1px solid white;
border-width: 0 2px 2px 0;
transform: rotate(40deg) translateY(-1px);
}
/*
* Radiobuttons
*/
input[type=radio] {
border-radius: 50%;
border: 1px solid dimgrey;
width: 12px;
height: 12px;
padding: 0;
margin-right: 6px;
transition: 0.2s border linear;
}
input[type=radio]:checked {
border: 6px solid rgb(110, 132, 163);
}
/*
* Range sliders
*/
input[type=range] {
border: unset;
border-radius: 3px;
height: 20px;
padding: 0;
background: transparent;
}
/* -webkit-slider.. & -moz-range.. cant be in selector lists:
https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
input[type=range]::-webkit-slider-runnable-track {
background-color: rgb(110, 132, 163);
height: 6px;
border-radius: 3px;
}
input[type=range]::-moz-range-track {
background-color: rgb(110, 132, 163);
height: 6px;
border-radius: 3px;
}
input[type=range]::-webkit-slider-thumb {
appearance: none;
width: 18px;
height: 20px;
border-radius: 5px;
background-color: white;
border: 1px solid dimgray;
margin-top: -7px;
}
input[type=range]::-moz-range-thumb {
appearance: none;
width: 18px;
height: 20px;
border-radius: 5px;
background-color: white;
border: 1px solid dimgray;
margin-top: -7px;
}
/*
* File choosers
*/
input[type=file] {
background-image: none;
border: none;
}
input::file-selector-button {
margin-right: 6px;
}
/*
* Hover
*/
input[type=button]:hover,
input[type=color]:hover,
input[type=image]:hover,
input[type=reset]:hover,
input[type=submit]:hover,
input::file-selector-button:hover,
button:hover {
background-image: linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
}
select:hover {
background-image: var(--select-arrow),
linear-gradient(to top, rgb(255, 255, 255), rgb(250, 250, 250));
background-position: calc(100% - 7px), left top;
background-repeat: no-repeat;
}
@media (any-pointer: coarse) {
/* We don't want a hover style after touch input */
input[type=button]:hover,
input[type=color]:hover,
input[type=image]:hover,
input[type=reset]:hover,
input[type=submit]:hover,
input::file-selector-button:hover,
button:hover {
background-image: var(--bg-gradient);
}
select:hover {
background-image: var(--select-arrow), var(--bg-gradient);
}
}
/*
* Active (clicked)
*/
input[type=button]:active,
input[type=color]:active,
input[type=image]:active,
input[type=reset]:active,
input[type=submit]:active,
input::file-selector-button:active,
button:active,
select:active {
border-bottom-width: 1px;
margin-top: 1px;
}
/*
* Focus (tab)
*/
input:focus-visible,
input:focus-visible::file-selector-button,
button:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 2px solid rgb(74, 144, 217);
outline-offset: 1px;
}
input[type=file]:focus-visible {
outline: none; /* We outline the button instead of the entire element */
}
/*
* Disabled
*/
input:disabled,
input:disabled::file-selector-button,
button:disabled,
select:disabled,
textarea:disabled {
opacity: 0.4;
min-width: 8em;
border: none;
color: black;
font-weight: bold;
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
cursor: pointer;
/* Disable Chrome's touch tap highlight */
-webkit-tap-highlight-color: transparent;
}
input[type=button]:disabled,
input[type=color]:disabled,
@@ -264,18 +191,438 @@ input[type=submit]:disabled,
input:disabled::file-selector-button,
button:disabled,
select:disabled {
background-image: var(--bg-gradient);
border-bottom-width: 2px;
margin-top: 0;
/* See Firefox bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
cursor: default;
}
input[type=file]:disabled {
background-image: none;
input[type=button],
input[type=color],
input[type=reset],
input[type=submit] {
/* Workaround for text-overflow bugs in Firefox and Chromium:
https://bugzilla.mozilla.org/show_bug.cgi?id=1800077
https://bugs.chromium.org/p/chromium/issues/detail?id=1383144 */
overflow: clip;
}
/* ------- COLOR PICKERS ------- */
input[type=color] {
min-width: unset;
box-sizing: content-box;
width: 1.4em;
height: 1.4em;
}
input[type=color]::-webkit-color-swatch-wrapper {
padding: 0;
}
/* -webkit-color-swatch & -moz-color-swatch cant be in a selector list:
https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
input[type=color]::-webkit-color-swatch {
border: none;
border-radius: 6px;
}
input[type=color]::-moz-color-swatch {
border: none;
border-radius: 6px;
}
/* -- SHARED BETWEEN CHECKBOXES, RADIOBUTTONS AND THE TOGGLE CLASS -- */
input[type=radio],
input[type=checkbox] {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
width: 16px;
--checkradio-height: 16px;
height: var(--checkradio-height);
padding: 0;
margin: 0 6px 0 0;
/* Don't have transitions for outline in order to be consistent
with other elements */
transition: all 0.2s, outline-color 0s, outline-offset 0s;
/* A transparent outline in order to work around a graphical clipping issue
in WebKit. See bug: https://bugs.webkit.org/show_bug.cgi?id=256003 */
outline: 1px solid transparent;
position: relative; /* Since ::before & ::after are absolute positioned */
/* We want to align with the middle of capital letters, this requires
a workaround. The default behavior is to align the bottom of the element
on top of the text baseline, this is too far up.
We want to push the element down half the difference in height between
it and a capital X. In our font, the height of a capital "X" is 0.698em.
*/
vertical-align: calc(0px - (var(--checkradio-height) - 0.698em) / 2);
/* FIXME: Could write 1cap instead of 0.698em, but it's only supported in
Firefox as of 2023 */
/* FIXME: We probably want to use round() here, see bug 8148 */
}
input[type=radio]:focus-visible,
input[type=checkbox]:focus-visible {
outline-color: var(--novnc-lightblue);
}
input[type=checkbox]::before,
input[type=checkbox]:not(.toggle)::after,
input[type=radio]::before,
input[type=radio]::after {
content: "";
display: block; /* width & height doesn't work on inline elements */
transition: inherit;
/* Let's prevent the pseudo-elements from taking up layout space so that
the ::before and ::after pseudo-elements can be in the same place. This
is also required for vertical-align: baseline to work like we want it to
on radio/checkboxes. If the pseudo-elements take up layout space, the
baseline of text inside them will be used instead. */
position: absolute;
}
input[type=checkbox]:not(.toggle)::after,
input[type=radio]::after {
width: 10px;
height: 2px;
background-color: transparent;
border-radius: 2px;
}
/* ------- CHECKBOXES ------- */
input[type=checkbox]:not(.toggle) {
border-radius: 4px;
}
input[type=checkbox]:not(.toggle):checked,
input[type=checkbox]:not(.toggle):indeterminate {
background-color: var(--novnc-blue);
background-image: var(--button-activation-overlay-light);
background-blend-mode: overlay;
}
input[type=checkbox]:not(.toggle)::before {
width: 25%;
height: 55%;
border-style: solid;
border-color: transparent;
border-width: 0 2px 2px 0;
border-radius: 1px;
transform: translateY(-1px) rotate(35deg);
}
input[type=checkbox]:not(.toggle):checked::before {
border-color: white;
}
input[type=checkbox]:not(.toggle):indeterminate::after {
background-color: white;
}
/* ------- RADIO BUTTONS ------- */
input[type=radio] {
border-radius: 50%;
border: 1px solid transparent; /* To ensure a smooth transition */
}
input[type=radio]:checked {
border: 4px solid var(--novnc-blue);
background-color: white;
/* button-activation-overlay should be removed from the radio
element to not interfere with button-activation-overlay-light
that is set on the ::before element. */
background-image: none;
}
input[type=radio]::before {
width: inherit;
height: inherit;
border-radius: inherit;
/* We can achieve the highlight overlay effect on border colors by
setting button-activation-overlay-light on an element that stays
on top (z-axis) of the element with a border. */
background-image: var(--button-activation-overlay-light);
mix-blend-mode: overlay;
opacity: 0;
}
input[type=radio]:checked::before {
opacity: 1;
}
input[type=radio]:indeterminate::after {
background-color: black;
}
/* ------- TOGGLE SWITCHES ------- */
/* These are meant to be used instead of checkboxes in some cases. If all of
the following critera are true you should use a toggle switch:
* The choice is a simple ON/OFF or ENABLE/DISABLE
* The choice doesn't give the feeling of "I agree" or "I confirm"
* There are not multiple related & grouped options
*/
input[type=checkbox].toggle {
display: inline-block;
--checkradio-height: 18px; /* Height value used in calc, see above */
width: 31px;
cursor: pointer;
user-select: none;
-webkit-user-select: none;
border-radius: 9px;
}
input[type=checkbox].toggle:disabled {
cursor: default;
}
input[type=checkbox].toggle:indeterminate {
background-color: var(--novnc-buttongrey);
background-image: var(--button-activation-overlay);
}
input[type=checkbox].toggle:checked {
background-color: var(--novnc-blue);
background-image: var(--button-activation-overlay-light);
background-blend-mode: overlay;
}
input[type=checkbox].toggle::before {
--circle-diameter: 10px;
--circle-offset: 4px;
width: var(--circle-diameter);
height: var(--circle-diameter);
top: var(--circle-offset);
left: var(--circle-offset);
background: white;
border-radius: 6px;
}
input[type=checkbox].toggle:checked::before {
left: calc(100% - var(--circle-offset) - var(--circle-diameter));
}
input[type=checkbox].toggle:indeterminate::before {
left: calc(50% - var(--circle-diameter) / 2);
}
/* ------- RANGE SLIDERS ------- */
input[type=range] {
border: unset;
border-radius: 8px;
height: 15px;
padding: 0;
background: transparent;
/* Needed to get properly rounded corners on -moz-range-progress
when the thumb is all the way to the right. Without overflow
hidden, the pointy edges of the progress track shows to the
right of the thumb. */
overflow: hidden;
}
@supports selector(::-webkit-slider-thumb) {
input[type=range] {
/* Needs a fixed width to match clip-path */
width: 125px;
/* overflow: hidden is not ideal for hiding the left part of the box
shadow of -webkit-slider-thumb since it doesn't match the smaller
border-radius of the progress track. The below clip-path has two
circular sides to make the ends of the track have correctly rounded
corners. The clip path shape looks something like this:
+-------------------------------+
/---| |---\
| |
\---| |---/
+-------------------------------+
The larger middle part of the clip path is made to have room for the
thumb. By using margins on the track, we prevent the thumb from
touching the ends of the track.
*/
clip-path: path(' \
M 4.5 3 \
L 4.5 0 \
L 120.5 0 \
L 120.5 3 \
A 1 1 0 0 1 120.5 12 \
L 120.5 15 \
L 4.5 15 \
L 4.5 12 \
A 1 1 0 0 1 4.5 3 \
');
}
}
input[type=range]:hover {
cursor: grab;
}
input[type=range]:active {
cursor: grabbing;
}
input[type=range]:disabled {
cursor: default;
}
input[type=range]:focus-visible {
clip-path: none; /* Otherwise it hides the outline */
}
/* -webkit-slider.. & -moz-range.. cant be in selector lists:
https://bugs.chromium.org/p/chromium/issues/detail?id=1154623 */
input[type=range]::-webkit-slider-runnable-track {
background-color: var(--novnc-buttongrey);
height: 7px;
border-radius: 4px;
margin: 0 3px;
}
input[type=range]::-moz-range-track {
background-color: var(--novnc-buttongrey);
height: 7px;
border-radius: 4px;
}
input[type=range]::-moz-range-progress {
background-color: var(--novnc-blue);
height: 9px;
/* Needs rounded corners only on the left side. Otherwise the rounding of
the progress track starts before the thumb, when the thumb is close to
the left edge. */
border-radius: 5px 0 0 5px;
}
input[type=range]::-webkit-slider-thumb {
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
background-color: white;
background-image: var(--button-activation-overlay);
/* Disable Chrome's touch tap highlight to avoid conflicts with overlay */
-webkit-tap-highlight-color: transparent;
border: 3px solid var(--novnc-blue);
margin-top: -4px; /* (track height / 2) - (thumb height /2) */
/* Since there is no way to style the left part of the range track in
webkit, we add a large shadow (1000px wide) to the left of the thumb and
then crop it with a clip-path shaped like this:
___
+-------------------/ \
| progress |Thumb|
+-------------------\ ___ /
The large left part of the shadow is clipped by another clip-path on on
the main range input element. */
/* FIXME: We can remove the box shadow workaround when this is standardized:
https://github.com/w3c/csswg-drafts/issues/4410 */
box-shadow: calc(-100vw - 8px) 0 0 100vw var(--novnc-blue);
clip-path: path(' \
M -1000 3 \
L 3 3 \
L 15 7.5 \
A 1 1 0 0 1 0 7.5 \
A 1 1 0 0 1 15 7.5 \
L 3 12 \
L -1000 12 Z \
');
}
input[type=range]::-moz-range-thumb {
appearance: none;
width: 15px;
height: 15px;
border-radius: 50%;
box-sizing: border-box;
background-color: white;
background-image: var(--button-activation-overlay);
border: 3px solid var(--novnc-blue);
margin-top: -7px;
}
/* ------- FILE CHOOSERS ------- */
input[type=file] {
background-image: none;
border: none;
}
input::file-selector-button {
margin-right: 6px;
}
input[type=file]:focus-visible {
outline: none; /* We outline the button instead of the entire element */
}
/* ------- SELECT BUTTONS ------- */
select {
--select-arrow: url('data:image/svg+xml;utf8, \
<svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
xmlns="http://www.w3.org/2000/svg"> \
<path d="m10.5.5-5 5-5-5" fill="none" \
stroke="black" stroke-width="1.5" \
stroke-linecap="round" stroke-linejoin="round"/> \
</svg>');
/* FIXME: A bug in Firefox, requires a workaround for the background:
https://bugzilla.mozilla.org/show_bug.cgi?id=1810958 */
/* The dropdown list will show the select element's background above and
below the options in Firefox. We want the entire dropdown to be white. */
background-color: white;
/* However, we don't want the select element to actually show a white
background, so let's place a gradient above it with the color we want. */
--grey-background: linear-gradient(var(--novnc-buttongrey) 100%,
transparent);
background-image:
var(--select-arrow),
var(--button-activation-overlay),
var(--grey-background);
background-position: calc(100% - var(--input-xpadding)), left top, left top;
background-repeat: no-repeat;
padding-right: calc(2*var(--input-xpadding) + 11px);
overflow: auto;
}
/* FIXME: :active isn't set when the <select> is opened in Firefox:
https://bugzilla.mozilla.org/show_bug.cgi?id=1805406 */
select:active {
/* Rotated arrow */
background-image: url('data:image/svg+xml;utf8, \
<svg width="11" height="6" version="1.1" viewBox="0 0 11 6" \
xmlns="http://www.w3.org/2000/svg" transform="rotate(180)"> \
<path d="m10.5.5-5 5-5-5" fill="none" \
stroke="black" stroke-width="1.5" \
stroke-linecap="round" stroke-linejoin="round"/> \
</svg>'),
var(--button-activation-overlay),
var(--grey-background);
}
select:disabled {
background-image: var(--select-arrow), var(--bg-gradient);
background-image:
var(--select-arrow),
var(--grey-background);
}
input[type=image]:disabled {
/* See Firefox bug:
https://bugzilla.mozilla.org/show_bug.cgi?id=1798304 */
cursor: default;
/* Note that styling for <option> doesn't work in all browsers
since its often drawn directly by the OS. We are generally very
limited in what we can change here. */
option {
/* Prevent Chrome from inheriting background-color from the <select> */
background-color: white;
color: black;
font-weight: normal;
background-image: var(--button-activation-overlay);
}
option:checked {
background-color: var(--novnc-lightgrey);
}
/* Change the look when the <select> isn't used as a dropdown. When "size"
or "multiple" are set, these elements behaves more like lists. */
select[size]:not([size="1"]), select[multiple] {
background-color: white;
background-image: unset; /* Don't show the arrow and other gradients */
border: 1px solid var(--novnc-lightgrey);
padding: 0;
font-weight: normal; /* Without this, options get bold font in WebKit. */
/* As an exception to the "list"-look, multi-selects in Chrome on Android,
and Safari on iOS, are unfortunately designed to be shown as a single
line. We can mitigate this inconsistency by at least fixing the height
here. By setting a min-height that matches other input elements, it
doesn't look too much out of place:
(1px border * 2) + (6.5px padding * 2) + 24px line-height = 39px */
min-height: 39px;
}
select[size]:not([size="1"]):focus-visible,
select[multiple]:focus-visible {
/* Text input style focus-visible highlight */
outline-offset: -1px;
}
select[size]:not([size="1"]) option, select[multiple] option {
overflow: hidden;
text-overflow: ellipsis;
padding: 4px var(--input-xpadding);
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -20,8 +20,12 @@ import * as WebUtil from "./webutil.js";
const PAGE_TITLE = "noVNC";
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
const UI = {
customSettings: {},
connected: false,
desktopName: "",
@@ -42,20 +46,31 @@ const UI = {
reconnectCallback: null,
reconnectPassword: null,
prime() {
return WebUtil.initSettings().then(() => {
if (document.readyState === "interactive" || document.readyState === "complete") {
return UI.start();
}
async start(options={}) {
UI.customSettings = options.settings || {};
if (UI.customSettings.defaults === undefined) {
UI.customSettings.defaults = {};
}
if (UI.customSettings.mandatory === undefined) {
UI.customSettings.mandatory = {};
}
return new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', () => UI.start().then(resolve).catch(reject));
// Set up translations
try {
await l10n.setup(LINGUAS, "app/locale/");
} catch (err) {
Log.Error("Failed to load translations: " + err);
}
// Initialize setting storage
await WebUtil.initSettings();
// Wait for the page to load
if (document.readyState !== "interactive" && document.readyState !== "complete") {
await new Promise((resolve, reject) => {
document.addEventListener('DOMContentLoaded', resolve);
});
});
},
// Render default UI and initialize settings menu
start() {
}
UI.initSettings();
@@ -70,22 +85,20 @@ const UI = {
}
// Try to fetch version number
fetch('./package.json')
.then((response) => {
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
return response.json();
})
.then((packageInfo) => {
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
})
.catch((err) => {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
});
try {
let response = await fetch('./package.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
let packageInfo = await response.json();
Array.from(document.getElementsByClassName('noVNC_version')).forEach(el => el.innerText = packageInfo.version);
} catch (err) {
Log.Error("Couldn't fetch package.json: " + err);
Array.from(document.getElementsByClassName('noVNC_version_wrapper'))
.concat(Array.from(document.getElementsByClassName('noVNC_version_separator')))
.forEach(el => el.style.display = 'none');
}
// Adapt the interface for touch screen devices
if (isTouchDevice) {
@@ -120,7 +133,7 @@ const UI = {
document.documentElement.classList.remove("noVNC_loading");
let autoconnect = WebUtil.getConfigVar('autoconnect', false);
let autoconnect = UI.getSetting('autoconnect');
if (autoconnect === 'true' || autoconnect == '1') {
autoconnect = true;
UI.connect();
@@ -129,8 +142,6 @@ const UI = {
// Show the connect panel on first load unless autoconnecting
UI.openConnectPanel();
}
return Promise.resolve(UI.rfb);
},
initFullscreen() {
@@ -158,34 +169,26 @@ const UI = {
UI.initSetting('logging', 'warn');
UI.updateLogging();
// if port == 80 (or 443) then it won't be present and should be
// set manually
let port = window.location.port;
if (!port) {
if (window.location.protocol.substring(0, 5) == 'https') {
port = 443;
} else if (window.location.protocol.substring(0, 4) == 'http') {
port = 80;
}
}
UI.setupSettingLabels();
/* Populate the controls if defaults are provided in the URL */
UI.initSetting('host', window.location.hostname);
UI.initSetting('port', port);
UI.initSetting('host', '');
UI.initSetting('port', 0);
UI.initSetting('encrypt', (window.location.protocol === "https:"));
UI.initSetting('password');
UI.initSetting('autoconnect', false);
UI.initSetting('view_clip', false);
UI.initSetting('resize', 'off');
UI.initSetting('quality', 6);
UI.initSetting('compression', 2);
UI.initSetting('shared', true);
UI.initSetting('bell', 'on');
UI.initSetting('view_only', false);
UI.initSetting('show_dot', false);
UI.initSetting('path', 'websockify');
UI.initSetting('repeaterID', '');
UI.initSetting('reconnect', false);
UI.initSetting('reconnect_delay', 5000);
UI.setupSettingLabels();
},
// Adds a link to the label elements on the corresponding input elements
setupSettingLabels() {
@@ -747,6 +750,10 @@ const UI = {
// Initial page load read/initialization of settings
initSetting(name, defVal) {
// Has the user overridden the default value?
if (name in UI.customSettings.defaults) {
defVal = UI.customSettings.defaults[name];
}
// Check Query string followed by cookie
let val = WebUtil.getConfigVar(name);
if (val === null) {
@@ -754,6 +761,11 @@ const UI = {
}
WebUtil.setSetting(name, val);
UI.updateSetting(name);
// Has the user forced a value?
if (name in UI.customSettings.mandatory) {
val = UI.customSettings.mandatory[name];
UI.forceSetting(name, val);
}
return val;
},
@@ -772,9 +784,12 @@ const UI = {
let value = UI.getSetting(name);
const ctrl = document.getElementById('noVNC_setting_' + name);
if (ctrl === null) {
return;
}
if (ctrl.type === 'checkbox') {
ctrl.checked = value;
} else if (typeof ctrl.options !== 'undefined') {
for (let i = 0; i < ctrl.options.length; i += 1) {
if (ctrl.options[i].value === value) {
@@ -807,7 +822,8 @@ const UI = {
getSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
let val = WebUtil.readSetting(name);
if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') {
if (typeof val !== 'undefined' && val !== null &&
ctrl !== null && ctrl.type === 'checkbox') {
if (val.toString().toLowerCase() in {'0': 1, 'no': 1, 'false': 1}) {
val = false;
} else {
@@ -822,14 +838,22 @@ const UI = {
// disable the labels that belong to disabled input elements.
disableSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
ctrl.disabled = true;
ctrl.label.classList.add('noVNC_disabled');
if (ctrl !== null) {
ctrl.disabled = true;
if (ctrl.label !== undefined) {
ctrl.label.classList.add('noVNC_disabled');
}
}
},
enableSetting(name) {
const ctrl = document.getElementById('noVNC_setting_' + name);
ctrl.disabled = false;
ctrl.label.classList.remove('noVNC_disabled');
if (ctrl !== null) {
ctrl.disabled = false;
if (ctrl.label !== undefined) {
ctrl.label.classList.remove('noVNC_disabled');
}
}
},
/* ------^-------
@@ -1011,7 +1035,7 @@ const UI = {
const path = UI.getSetting('path');
if (typeof password === 'undefined') {
password = WebUtil.getConfigVar('password');
password = UI.getSetting('password');
UI.reconnectPassword = password;
}
@@ -1021,28 +1045,36 @@ const UI = {
UI.hideStatus();
if (!host) {
Log.Error("Can't connect when host is: " + host);
UI.showStatus(_("Must set host"), 'error');
return;
}
UI.closeConnectPanel();
UI.updateVisualState('connecting');
let url;
url = UI.getSetting('encrypt') ? 'wss' : 'ws';
if (host) {
url = new URL("https://" + host);
url += '://' + host;
if (port) {
url += ':' + port;
url.protocol = UI.getSetting('encrypt') ? 'wss:' : 'ws:';
if (port) {
url.port = port;
}
// "./" is needed to force URL() to interpret the path-variable as
// a path and not as an URL. This is relevant if for example path
// starts with more than one "/", in which case it would be
// interpreted as a host name instead.
url = new URL("./" + path, url);
} else {
// Current (May 2024) browsers support relative WebSocket
// URLs natively, but we need to support older browsers for
// some time.
url = new URL(path, location.href);
url.protocol = (window.location.protocol === "https:") ? 'wss:' : 'ws:';
}
url += '/' + path;
try {
UI.rfb = new RFB(document.getElementById('noVNC_container'), url,
UI.rfb = new RFB(document.getElementById('noVNC_container'),
url.href,
{ shared: UI.getSetting('shared'),
repeaterID: UI.getSetting('repeaterID'),
credentials: { password: password } });
@@ -1738,7 +1770,7 @@ const UI = {
},
bell(e) {
if (WebUtil.getConfigVar('bell', 'on') === 'on') {
if (UI.getSetting('bell') === 'on') {
const promise = document.getElementById('noVNC_bell').play();
// The standards disagree on the return value here
if (promise) {
@@ -1769,10 +1801,4 @@ const UI = {
*/
};
// Set up translations
const LINGUAS = ["cs", "de", "el", "es", "fr", "it", "ja", "ko", "nl", "pl", "pt_BR", "ru", "sv", "tr", "zh_CN", "zh_TW"];
l10n.setup(LINGUAS, "app/locale/")
.catch(err => Log.Error("Failed to load translations: " + err))
.then(UI.prime);
export default UI;

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -27,7 +27,7 @@ export function initLogging(level) {
// the url can be requested in the following way:
// https://www.example.com#myqueryparam=myvalue&password=secretvalue
//
// Even Mixing public and non public parameters will work:
// Even mixing public and non public parameters will work:
// https://www.example.com?nonsecretparam=example.com#password=secretvalue
export function getQueryVar(name, defVal) {
"use strict";

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2021 The noVNC Authors
* Copyright (C) 2021 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -0,0 +1,321 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2024 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
import * as Log from '../util/logging.js';
export class H264Parser {
constructor(data) {
this._data = data;
this._index = 0;
this.profileIdc = null;
this.constraintSet = null;
this.levelIdc = null;
}
_getStartSequenceLen(index) {
let data = this._data;
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {
return 4;
}
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
return 3;
}
return 0;
}
_indexOfNextNalUnit(index) {
let data = this._data;
for (let i = index; i < data.length; ++i) {
if (this._getStartSequenceLen(i) != 0) {
return i;
}
}
return -1;
}
_parseSps(index) {
this.profileIdc = this._data[index];
this.constraintSet = this._data[index + 1];
this.levelIdc = this._data[index + 2];
}
_parseNalUnit(index) {
const firstByte = this._data[index];
if (firstByte & 0x80) {
throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');
}
const unitType = firstByte & 0x1f;
switch (unitType) {
case 1: // coded slice, non-idr
return { slice: true };
case 5: // coded slice, idr
return { slice: true, key: true };
case 6: // sei
return {};
case 7: // sps
this._parseSps(index + 1);
return {};
case 8: // pps
return {};
default:
Log.Warn("Unhandled unit type: ", unitType);
break;
}
return {};
}
parse() {
const startIndex = this._index;
let isKey = false;
while (this._index < this._data.length) {
const startSequenceLen = this._getStartSequenceLen(this._index);
if (startSequenceLen == 0) {
throw new Error('Invalid start sequence in bit stream');
}
const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);
let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);
if (nextIndex == -1) {
this._index = this._data.length;
} else {
this._index = nextIndex;
}
if (key) {
isKey = true;
}
if (slice) {
break;
}
}
if (startIndex === this._index) {
return null;
}
return {
frame: this._data.subarray(startIndex, this._index),
key: isKey,
};
}
}
export class H264Context {
constructor(width, height) {
this.lastUsed = 0;
this._width = width;
this._height = height;
this._profileIdc = null;
this._constraintSet = null;
this._levelIdc = null;
this._decoder = null;
this._pendingFrames = [];
}
_handleFrame(frame) {
let pending = this._pendingFrames.shift();
if (pending === undefined) {
throw new Error("Pending frame queue empty when receiving frame from decoder");
}
if (pending.timestamp != frame.timestamp) {
throw new Error("Video frame timestamp mismatch. Expected " +
frame.timestamp + " but but got " + pending.timestamp);
}
pending.frame = frame;
pending.ready = true;
pending.resolve();
if (!pending.keep) {
frame.close();
}
}
_handleError(e) {
throw new Error("Failed to decode frame: " + e.message);
}
_configureDecoder(profileIdc, constraintSet, levelIdc) {
if (this._decoder === null || this._decoder.state === 'closed') {
this._decoder = new VideoDecoder({
output: frame => this._handleFrame(frame),
error: e => this._handleError(e),
});
}
const codec = 'avc1.' +
profileIdc.toString(16).padStart(2, '0') +
constraintSet.toString(16).padStart(2, '0') +
levelIdc.toString(16).padStart(2, '0');
this._decoder.configure({
codec: codec,
codedWidth: this._width,
codedHeight: this._height,
optimizeForLatency: true,
});
}
_preparePendingFrame(timestamp) {
let pending = {
timestamp: timestamp,
promise: null,
resolve: null,
frame: null,
ready: false,
keep: false,
};
pending.promise = new Promise((resolve) => {
pending.resolve = resolve;
});
this._pendingFrames.push(pending);
return pending;
}
decode(payload) {
let parser = new H264Parser(payload);
let result = null;
// Ideally, this timestamp should come from the server, but we'll just
// approximate it instead.
let timestamp = Math.round(window.performance.now() * 1e3);
while (true) {
let encodedFrame = parser.parse();
if (encodedFrame === null) {
break;
}
if (parser.profileIdc !== null) {
self._profileIdc = parser.profileIdc;
self._constraintSet = parser.constraintSet;
self._levelIdc = parser.levelIdc;
}
if (this._decoder === null || this._decoder.state !== 'configured') {
if (!encodedFrame.key) {
Log.Warn("Missing key frame. Can't decode until one arrives");
continue;
}
if (self._profileIdc === null) {
Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');
continue;
}
this._configureDecoder(self._profileIdc, self._constraintSet,
self._levelIdc);
}
result = this._preparePendingFrame(timestamp);
const chunk = new EncodedVideoChunk({
timestamp: timestamp,
type: encodedFrame.key ? 'key' : 'delta',
data: encodedFrame.frame,
});
try {
this._decoder.decode(chunk);
} catch (e) {
Log.Warn("Failed to decode:", e);
}
}
// We only keep last frame of each payload
if (result !== null) {
result.keep = true;
}
return result;
}
}
export default class H264Decoder {
constructor() {
this._tick = 0;
this._contexts = {};
}
_contextId(x, y, width, height) {
return [x, y, width, height].join(',');
}
_findOldestContextId() {
let oldestTick = Number.MAX_VALUE;
let oldestKey = undefined;
for (const [key, value] of Object.entries(this._contexts)) {
if (value.lastUsed < oldestTick) {
oldestTick = value.lastUsed;
oldestKey = key;
}
}
return oldestKey;
}
_createContext(x, y, width, height) {
const maxContexts = 64;
if (Object.keys(this._contexts).length >= maxContexts) {
let oldestContextId = this._findOldestContextId();
delete this._contexts[oldestContextId];
}
let context = new H264Context(width, height);
this._contexts[this._contextId(x, y, width, height)] = context;
return context;
}
_getContext(x, y, width, height) {
let context = this._contexts[this._contextId(x, y, width, height)];
return context !== undefined ? context : this._createContext(x, y, width, height);
}
_resetContext(x, y, width, height) {
delete this._contexts[this._contextId(x, y, width, height)];
}
_resetAllContexts() {
this._contexts = {};
}
decodeRect(x, y, width, height, sock, display, depth) {
const resetContextFlag = 1;
const resetAllContextsFlag = 2;
if (sock.rQwait("h264 header", 8)) {
return false;
}
const length = sock.rQshift32();
const flags = sock.rQshift32();
if (sock.rQwait("h264 payload", length, 8)) {
return false;
}
if (flags & resetAllContextsFlag) {
this._resetAllContexts();
} else if (flags & resetContextFlag) {
this._resetContext(x, y, width, height);
}
let context = this._getContext(x, y, width, height);
context.lastUsed = this._tick++;
if (length !== 0) {
let payload = sock.rQshiftBytes(length, false);
let frame = context.decode(payload);
if (frame !== null) {
display.videoFrame(x, y, width, height, frame);
}
}
return true;
}
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
* Licensed under MPL 2.0 (see LICENSE.txt)
*

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -0,0 +1,51 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2024 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
*
*/
import Inflator from "../inflator.js";
export default class ZlibDecoder {
constructor() {
this._zlib = new Inflator();
this._length = 0;
}
decodeRect(x, y, width, height, sock, display, depth) {
if ((width === 0) || (height === 0)) {
return true;
}
if (this._length === 0) {
if (sock.rQwait("ZLIB", 4)) {
return false;
}
this._length = sock.rQshift32();
}
if (sock.rQwait("ZLIB", this._length)) {
return false;
}
let data = new Uint8Array(sock.rQshiftBytes(this._length, false));
this._length = 0;
this._zlib.setInput(data);
data = this._zlib.inflate(width * height * 4);
this._zlib.setInput(null);
// Max sure the image is fully opaque
for (let i = 0; i < width * height; i++) {
data[i * 4 + 3] = 255;
}
display.blitImage(x, y, width, height, data, 0);
return true;
}
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2021 The noVNC Authors
* Copyright (C) 2021 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -380,6 +380,17 @@ export default class Display {
});
}
videoFrame(x, y, width, height, frame) {
this._renderQPush({
'type': 'frame',
'frame': frame,
'x': x,
'y': y,
'width': width,
'height': height
});
}
blitImage(x, y, width, height, arr, offset, fromQueue) {
if (this._renderQ.length !== 0 && !fromQueue) {
// NB(directxman12): it's technically more performant here to use preallocated arrays,
@@ -406,9 +417,16 @@ export default class Display {
}
}
drawImage(img, x, y) {
this._drawCtx.drawImage(img, x, y);
this._damage(x, y, img.width, img.height);
drawImage(img, ...args) {
this._drawCtx.drawImage(img, ...args);
if (args.length <= 4) {
const [x, y] = args;
this._damage(x, y, img.width, img.height);
} else {
const [,, sw, sh, dx, dy] = args;
this._damage(dx, dy, sw, sh);
}
}
autoscale(containerWidth, containerHeight) {
@@ -511,6 +529,35 @@ export default class Display {
ready = false;
}
break;
case 'frame':
if (a.frame.ready) {
// The encoded frame may be larger than the rect due to
// limitations of the encoder, so we need to crop the
// frame.
let frame = a.frame.frame;
if (frame.codedWidth < a.width || frame.codedHeight < a.height) {
Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
a.width + "x" + a.height + " but got " +
frame.codedWidth + "x" + frame.codedHeight);
}
const sx = 0;
const sy = 0;
const sw = a.width;
const sh = a.height;
const dx = a.x;
const dy = a.y;
const dw = sw;
const dh = sh;
this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);
frame.close();
} else {
let display = this;
a.frame.promise.then(() => {
display._scanRenderQ();
});
ready = false;
}
break;
}
if (ready) {

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -11,10 +11,12 @@ export const encodings = {
encodingCopyRect: 1,
encodingRRE: 2,
encodingHextile: 5,
encodingZlib: 6,
encodingTight: 7,
encodingZRLE: 16,
encodingTightPNG: -260,
encodingJPEG: 21,
encodingH264: 50,
pseudoEncodingQualityLevel9: -23,
pseudoEncodingQualityLevel0: -32,
@@ -28,6 +30,7 @@ export const encodings = {
pseudoEncodingXvp: -309,
pseudoEncodingFence: -312,
pseudoEncodingContinuousUpdates: -313,
pseudoEncodingExtendedMouseButtons: -316,
pseudoEncodingCompressLevel9: -247,
pseudoEncodingCompressLevel0: -256,
pseudoEncodingVMwareCursor: 0x574d5664,
@@ -40,10 +43,12 @@ export function encodingName(num) {
case encodings.encodingCopyRect: return "CopyRect";
case encodings.encodingRRE: return "RRE";
case encodings.encodingHextile: return "Hextile";
case encodings.encodingZlib: return "Zlib";
case encodings.encodingTight: return "Tight";
case encodings.encodingZRLE: return "ZRLE";
case encodings.encodingTightPNG: return "TightPNG";
case encodings.encodingJPEG: return "JPEG";
case encodings.encodingH264: return "H.264";
default: return "[unknown encoding " + num + "]";
}
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/
@@ -203,7 +203,7 @@ export default class Keyboard {
if ((code === "ControlLeft") && browser.isWindows() &&
!("ControlLeft" in this._keyDownList)) {
this._altGrArmed = true;
this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100);
this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100);
this._altGrCtrlTime = e.timeStamp;
return;
}
@@ -218,11 +218,7 @@ export default class Keyboard {
// We can't get a release in the middle of an AltGr sequence, so
// abort that detection
if (this._altGrArmed) {
this._altGrArmed = false;
clearTimeout(this._altGrTimeout);
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
}
this._interruptAltGrSequence();
// See comment in _handleKeyDown()
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
@@ -249,14 +245,20 @@ export default class Keyboard {
}
}
_handleAltGrTimeout() {
this._altGrArmed = false;
clearTimeout(this._altGrTimeout);
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
_interruptAltGrSequence() {
if (this._altGrArmed) {
this._altGrArmed = false;
clearTimeout(this._altGrTimeout);
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
}
}
_allKeysUp() {
Log.Debug(">> Keyboard.allKeysUp");
// Prevent control key being processed after losing focus.
this._interruptAltGrSequence();
for (let code in this._keyDownList) {
this._sendKeyEvent(this._keyDownList[code], code, false);
}

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -10,7 +10,7 @@
import { toUnsigned32bit, toSigned32bit } from './util/int.js';
import * as Log from './util/logging.js';
import { encodeUTF8, decodeUTF8 } from './util/strings.js';
import { dragThreshold } from './util/browser.js';
import { dragThreshold, supportsWebCodecsH264Decode } from './util/browser.js';
import { clientToElement } from './util/element.js';
import { setCapture } from './util/events.js';
import EventTargetMixin from './util/eventtarget.js';
@@ -31,10 +31,12 @@ import RawDecoder from "./decoders/raw.js";
import CopyRectDecoder from "./decoders/copyrect.js";
import RREDecoder from "./decoders/rre.js";
import HextileDecoder from "./decoders/hextile.js";
import ZlibDecoder from './decoders/zlib.js';
import TightDecoder from "./decoders/tight.js";
import TightPNGDecoder from "./decoders/tightpng.js";
import ZRLEDecoder from "./decoders/zrle.js";
import JPEGDecoder from "./decoders/jpeg.js";
import H264Decoder from "./decoders/h264.js";
// How many seconds to wait for a disconnect to finish
const DISCONNECT_TIMEOUT = 3;
@@ -147,9 +149,13 @@ export default class RFB extends EventTargetMixin {
this._supportsSetDesktopSize = false;
this._screenID = 0;
this._screenFlags = 0;
this._pendingRemoteResize = false;
this._lastResize = 0;
this._qemuExtKeyEventSupported = false;
this._extendedPointerEventSupported = false;
this._clipboardText = null;
this._clipboardServerCapabilitiesActions = {};
this._clipboardServerCapabilitiesFormats = {};
@@ -244,10 +250,12 @@ export default class RFB extends EventTargetMixin {
this._decoders[encodings.encodingCopyRect] = new CopyRectDecoder();
this._decoders[encodings.encodingRRE] = new RREDecoder();
this._decoders[encodings.encodingHextile] = new HextileDecoder();
this._decoders[encodings.encodingZlib] = new ZlibDecoder();
this._decoders[encodings.encodingTight] = new TightDecoder();
this._decoders[encodings.encodingTightPNG] = new TightPNGDecoder();
this._decoders[encodings.encodingZRLE] = new ZRLEDecoder();
this._decoders[encodings.encodingJPEG] = new JPEGDecoder();
this._decoders[encodings.encodingH264] = new H264Decoder();
// NB: nothing that needs explicit teardown should be done
// before this point, since this can throw an exception
@@ -716,6 +724,7 @@ export default class RFB extends EventTargetMixin {
currentHeight == this._expectedClientHeight;
}
// Handle browser window resizes
_handleResize() {
// Don't change anything if the client size is already as expected
if (this._clientHasExpectedSize()) {
@@ -726,17 +735,12 @@ export default class RFB extends EventTargetMixin {
window.requestAnimationFrame(() => {
this._updateClip();
this._updateScale();
this._saveExpectedClientSize();
});
if (this._resizeSession) {
// Request changing the resolution of the remote display to
// the size of the local browser viewport.
// In order to not send multiple requests before the browser-resize
// is finished we wait 0.5 seconds before sending the request.
clearTimeout(this._resizeTimeout);
this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500);
}
// Request changing the resolution of the remote display to
// the size of the local browser viewport.
this._requestRemoteResize();
}
// Update state of clipping in Display object, and make sure the
@@ -786,16 +790,39 @@ export default class RFB extends EventTargetMixin {
// Requests a change of remote desktop size. This message is an extension
// and may only be sent if we have received an ExtendedDesktopSize message
_requestRemoteResize() {
clearTimeout(this._resizeTimeout);
this._resizeTimeout = null;
if (!this._resizeSession || this._viewOnly ||
!this._supportsSetDesktopSize) {
if (!this._resizeSession) {
return;
}
if (this._viewOnly) {
return;
}
if (!this._supportsSetDesktopSize) {
return;
}
// Rate limit to one pending resize at a time
if (this._pendingRemoteResize) {
return;
}
// And no more than once every 100ms
if ((Date.now() - this._lastResize) < 100) {
clearTimeout(this._resizeTimeout);
this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this),
100 - (Date.now() - this._lastResize));
return;
}
this._resizeTimeout = null;
const size = this._screenSize();
// Do we actually change anything?
if (size.w === this._fbWidth && size.h === this._fbHeight) {
return;
}
this._pendingRemoteResize = true;
this._lastResize = Date.now();
RFB.messages.setDesktopSize(this._sock,
Math.floor(size.w), Math.floor(size.h),
this._screenID, this._screenFlags);
@@ -1027,6 +1054,36 @@ export default class RFB extends EventTargetMixin {
this.sendKey(keysym, code, down);
}
static _convertButtonMask(buttons) {
/* The bits in MouseEvent.buttons property correspond
* to the following mouse buttons:
* 0: Left
* 1: Right
* 2: Middle
* 3: Back
* 4: Forward
*
* These bits needs to be converted to what they are defined as
* in the RFB protocol.
*/
const buttonMaskMap = {
0: 1 << 0, // Left
1: 1 << 2, // Right
2: 1 << 1, // Middle
3: 1 << 7, // Back
4: 1 << 8, // Forward
};
let bmask = 0;
for (let i = 0; i < 5; i++) {
if (buttons & (1 << i)) {
bmask |= buttonMaskMap[i];
}
}
return bmask;
}
_handleMouse(ev) {
/*
* We don't check connection status or viewOnly here as the
@@ -1056,80 +1113,75 @@ export default class RFB extends EventTargetMixin {
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
let bmask = RFB._convertButtonMask(ev.buttons);
let down = ev.type == 'mousedown';
switch (ev.type) {
case 'mousedown':
setCapture(this._canvas);
this._handleMouseButton(pos.x, pos.y,
true, 1 << ev.button);
break;
case 'mouseup':
this._handleMouseButton(pos.x, pos.y,
false, 1 << ev.button);
if (this.dragViewport) {
if (down && !this._viewportDragging) {
this._viewportDragging = true;
this._viewportDragPos = {'x': pos.x, 'y': pos.y};
this._viewportHasMoved = false;
this._flushMouseMoveTimer(pos.x, pos.y);
// Skip sending mouse events, instead save the current
// mouse mask so we can send it later.
this._mouseButtonMask = bmask;
break;
} else {
this._viewportDragging = false;
// If we actually performed a drag then we are done
// here and should not send any mouse events
if (this._viewportHasMoved) {
this._mouseButtonMask = bmask;
break;
}
// Otherwise we treat this as a mouse click event.
// Send the previously saved button mask, followed
// by the current button mask at the end of this
// function.
this._sendMouse(pos.x, pos.y, this._mouseButtonMask);
}
}
if (down) {
setCapture(this._canvas);
}
this._handleMouseButton(pos.x, pos.y, bmask);
break;
case 'mousemove':
if (this._viewportDragging) {
const deltaX = this._viewportDragPos.x - pos.x;
const deltaY = this._viewportDragPos.y - pos.y;
if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
Math.abs(deltaY) > dragThreshold)) {
this._viewportHasMoved = true;
this._viewportDragPos = {'x': pos.x, 'y': pos.y};
this._display.viewportChangePos(deltaX, deltaY);
}
// Skip sending mouse events
break;
}
this._handleMouseMove(pos.x, pos.y);
break;
}
}
_handleMouseButton(x, y, down, bmask) {
if (this.dragViewport) {
if (down && !this._viewportDragging) {
this._viewportDragging = true;
this._viewportDragPos = {'x': x, 'y': y};
this._viewportHasMoved = false;
// Skip sending mouse events
return;
} else {
this._viewportDragging = false;
// If we actually performed a drag then we are done
// here and should not send any mouse events
if (this._viewportHasMoved) {
return;
}
// Otherwise we treat this as a mouse click event.
// Send the button down event here, as the button up
// event is sent at the end of this function.
this._sendMouse(x, y, bmask);
}
}
_handleMouseButton(x, y, bmask) {
// Flush waiting move event first
if (this._mouseMoveTimer !== null) {
clearTimeout(this._mouseMoveTimer);
this._mouseMoveTimer = null;
this._sendMouse(x, y, this._mouseButtonMask);
}
if (down) {
this._mouseButtonMask |= bmask;
} else {
this._mouseButtonMask &= ~bmask;
}
this._flushMouseMoveTimer(x, y);
this._mouseButtonMask = bmask;
this._sendMouse(x, y, this._mouseButtonMask);
}
_handleMouseMove(x, y) {
if (this._viewportDragging) {
const deltaX = this._viewportDragPos.x - x;
const deltaY = this._viewportDragPos.y - y;
if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
Math.abs(deltaY) > dragThreshold)) {
this._viewportHasMoved = true;
this._viewportDragPos = {'x': x, 'y': y};
this._display.viewportChangePos(deltaX, deltaY);
}
// Skip sending mouse events
return;
}
this._mousePos = { 'x': x, 'y': y };
// Limit many mouse move events to one every MOUSE_MOVE_DELAY ms
@@ -1159,8 +1211,20 @@ export default class RFB extends EventTargetMixin {
if (this._rfbConnectionState !== 'connected') { return; }
if (this._viewOnly) { return; } // View only, skip mouse events
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
// Highest bit in mask is never sent to the server
if (mask & 0x8000) {
throw new Error("Illegal mouse button mask (mask: " + mask + ")");
}
let extendedMouseButtons = mask & 0x7f80;
if (this._extendedPointerEventSupported && extendedMouseButtons) {
RFB.messages.extendedPointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
} else {
RFB.messages.pointerEvent(this._sock, this._display.absX(x),
this._display.absY(y), mask);
}
}
_handleWheel(ev) {
@@ -1173,6 +1237,7 @@ export default class RFB extends EventTargetMixin {
let pos = clientToElement(ev.clientX, ev.clientY,
this._canvas);
let bmask = RFB._convertButtonMask(ev.buttons);
let dX = ev.deltaX;
let dY = ev.deltaY;
@@ -1192,26 +1257,27 @@ export default class RFB extends EventTargetMixin {
this._accumulatedWheelDeltaX += dX;
this._accumulatedWheelDeltaY += dY;
// Generate a mouse wheel step event when the accumulated delta
// for one of the axes is large enough.
if (Math.abs(this._accumulatedWheelDeltaX) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaX < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 5);
this._handleMouseButton(pos.x, pos.y, false, 1 << 5);
this._handleMouseButton(pos.x, pos.y, bmask | 1 << 5);
this._handleMouseButton(pos.x, pos.y, bmask);
} else if (this._accumulatedWheelDeltaX > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 6);
this._handleMouseButton(pos.x, pos.y, false, 1 << 6);
this._handleMouseButton(pos.x, pos.y, bmask | 1 << 6);
this._handleMouseButton(pos.x, pos.y, bmask);
}
this._accumulatedWheelDeltaX = 0;
}
if (Math.abs(this._accumulatedWheelDeltaY) >= WHEEL_STEP) {
if (this._accumulatedWheelDeltaY < 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 3);
this._handleMouseButton(pos.x, pos.y, false, 1 << 3);
this._handleMouseButton(pos.x, pos.y, bmask | 1 << 3);
this._handleMouseButton(pos.x, pos.y, bmask);
} else if (this._accumulatedWheelDeltaY > 0) {
this._handleMouseButton(pos.x, pos.y, true, 1 << 4);
this._handleMouseButton(pos.x, pos.y, false, 1 << 4);
this._handleMouseButton(pos.x, pos.y, bmask | 1 << 4);
this._handleMouseButton(pos.x, pos.y, bmask);
}
this._accumulatedWheelDeltaY = 0;
@@ -1250,8 +1316,8 @@ export default class RFB extends EventTargetMixin {
this._gestureLastTapTime = Date.now();
this._fakeMouseMove(this._gestureFirstDoubleTapEv, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, bmask);
this._handleMouseButton(pos.x, pos.y, false, bmask);
this._handleMouseButton(pos.x, pos.y, bmask);
this._handleMouseButton(pos.x, pos.y, 0x0);
}
_handleGesture(ev) {
@@ -1272,14 +1338,27 @@ export default class RFB extends EventTargetMixin {
this._handleTapEvent(ev, 0x2);
break;
case 'drag':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x1);
if (this.dragViewport) {
this._viewportHasMoved = false;
this._viewportDragging = true;
this._viewportDragPos = {'x': pos.x, 'y': pos.y};
} else {
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, 0x1);
}
break;
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, true, 0x4);
if (this.dragViewport) {
// If dragViewport is true, we need to wait to see
// if we have dragged outside the threshold before
// sending any events to the server.
this._viewportHasMoved = false;
this._viewportDragPos = {'x': pos.x, 'y': pos.y};
} else {
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, 0x4);
}
break;
case 'twodrag':
this._gestureLastMagnitudeX = ev.detail.magnitudeX;
this._gestureLastMagnitudeY = ev.detail.magnitudeY;
@@ -1301,7 +1380,21 @@ export default class RFB extends EventTargetMixin {
break;
case 'drag':
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
if (this.dragViewport) {
this._viewportDragging = true;
const deltaX = this._viewportDragPos.x - pos.x;
const deltaY = this._viewportDragPos.y - pos.y;
if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold ||
Math.abs(deltaY) > dragThreshold)) {
this._viewportHasMoved = true;
this._viewportDragPos = {'x': pos.x, 'y': pos.y};
this._display.viewportChangePos(deltaX, deltaY);
}
} else {
this._fakeMouseMove(ev, pos.x, pos.y);
}
break;
case 'twodrag':
// Always scroll in the same position.
@@ -1309,23 +1402,23 @@ export default class RFB extends EventTargetMixin {
// every update.
this._fakeMouseMove(ev, pos.x, pos.y);
while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) > GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x8);
this._handleMouseButton(pos.x, pos.y, false, 0x8);
this._handleMouseButton(pos.x, pos.y, 0x8);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeY += GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeY - this._gestureLastMagnitudeY) < -GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x10);
this._handleMouseButton(pos.x, pos.y, false, 0x10);
this._handleMouseButton(pos.x, pos.y, 0x10);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeY -= GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) > GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x20);
this._handleMouseButton(pos.x, pos.y, false, 0x20);
this._handleMouseButton(pos.x, pos.y, 0x20);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeX += GESTURE_SCRLSENS;
}
while ((ev.detail.magnitudeX - this._gestureLastMagnitudeX) < -GESTURE_SCRLSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x40);
this._handleMouseButton(pos.x, pos.y, false, 0x40);
this._handleMouseButton(pos.x, pos.y, 0x40);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeX -= GESTURE_SCRLSENS;
}
break;
@@ -1338,13 +1431,13 @@ export default class RFB extends EventTargetMixin {
if (Math.abs(magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
this._handleKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
while ((magnitude - this._gestureLastMagnitudeX) > GESTURE_ZOOMSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x8);
this._handleMouseButton(pos.x, pos.y, false, 0x8);
this._handleMouseButton(pos.x, pos.y, 0x8);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeX += GESTURE_ZOOMSENS;
}
while ((magnitude - this._gestureLastMagnitudeX) < -GESTURE_ZOOMSENS) {
this._handleMouseButton(pos.x, pos.y, true, 0x10);
this._handleMouseButton(pos.x, pos.y, false, 0x10);
this._handleMouseButton(pos.x, pos.y, 0x10);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._gestureLastMagnitudeX -= GESTURE_ZOOMSENS;
}
}
@@ -1362,19 +1455,47 @@ export default class RFB extends EventTargetMixin {
case 'twodrag':
break;
case 'drag':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x1);
if (this.dragViewport) {
this._viewportDragging = false;
} else {
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, 0x0);
}
break;
case 'longpress':
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, false, 0x4);
if (this._viewportHasMoved) {
// We don't want to send any events if we have moved
// our viewport
break;
}
if (this.dragViewport && !this._viewportHasMoved) {
this._fakeMouseMove(ev, pos.x, pos.y);
// If dragViewport is true, we need to wait to see
// if we have dragged outside the threshold before
// sending any events to the server.
this._handleMouseButton(pos.x, pos.y, 0x4);
this._handleMouseButton(pos.x, pos.y, 0x0);
this._viewportDragging = false;
} else {
this._fakeMouseMove(ev, pos.x, pos.y);
this._handleMouseButton(pos.x, pos.y, 0x0);
}
break;
}
break;
}
}
// Message Handlers
_flushMouseMoveTimer(x, y) {
if (this._mouseMoveTimer !== null) {
clearTimeout(this._mouseMoveTimer);
this._mouseMoveTimer = null;
this._sendMouse(x, y, this._mouseButtonMask);
}
}
// Message handlers
_negotiateProtocolVersion() {
if (this._sock.rQwait("version", 12)) {
@@ -2115,12 +2236,16 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.encodingCopyRect);
// Only supported with full depth support
if (this._fbDepth == 24) {
if (supportsWebCodecsH264Decode) {
encs.push(encodings.encodingH264);
}
encs.push(encodings.encodingTight);
encs.push(encodings.encodingTightPNG);
encs.push(encodings.encodingZRLE);
encs.push(encodings.encodingJPEG);
encs.push(encodings.encodingHextile);
encs.push(encodings.encodingRRE);
encs.push(encodings.encodingZlib);
}
encs.push(encodings.encodingRaw);
@@ -2138,6 +2263,7 @@ export default class RFB extends EventTargetMixin {
encs.push(encodings.pseudoEncodingContinuousUpdates);
encs.push(encodings.pseudoEncodingDesktopName);
encs.push(encodings.pseudoEncodingExtendedClipboard);
encs.push(encodings.pseudoEncodingExtendedMouseButtons);
if (this._fbDepth == 24) {
encs.push(encodings.pseudoEncodingVMwareCursor);
@@ -2419,7 +2545,7 @@ export default class RFB extends EventTargetMixin {
switch (xvpMsg) {
case 0: // XVP_FAIL
Log.Error("XVP Operation Failed");
Log.Error("XVP operation failed");
break;
case 1: // XVP_INIT
this._rfbXvpVer = xvpVer;
@@ -2567,6 +2693,10 @@ export default class RFB extends EventTargetMixin {
case encodings.pseudoEncodingExtendedDesktopSize:
return this._handleExtendedDesktopSize();
case encodings.pseudoEncodingExtendedMouseButtons:
this._extendedPointerEventSupported = true;
return true;
case encodings.pseudoEncodingQEMULedEvent:
return this._handleLedEvent();
@@ -2748,7 +2878,7 @@ export default class RFB extends EventTargetMixin {
}
_handleLedEvent() {
if (this._sock.rQwait("LED Status", 1)) {
if (this._sock.rQwait("LED status", 1)) {
return false;
}
@@ -2802,6 +2932,10 @@ export default class RFB extends EventTargetMixin {
* 2 - another client requested the resize
*/
if (this._FBU.x === 1) {
this._pendingRemoteResize = false;
}
// We need to handle errors when we requested the resize.
if (this._FBU.x === 1 && this._FBU.y !== 0) {
let msg = "";
@@ -2834,6 +2968,12 @@ export default class RFB extends EventTargetMixin {
this._requestRemoteResize();
}
if (this._FBU.x === 1 && this._FBU.y === 0) {
// We might have resized again whilst waiting for the
// previous request, so check if we are in sync
this._requestRemoteResize();
}
return true;
}
@@ -2863,6 +3003,7 @@ export default class RFB extends EventTargetMixin {
this._fbWidth, this._fbHeight);
}
// Handle resize-messages from the server
_resize(width, height) {
this._fbWidth = width;
this._fbHeight = height;
@@ -2975,6 +3116,10 @@ RFB.messages = {
pointerEvent(sock, x, y, mask) {
sock.sQpush8(5); // msg-type
// Marker bit must be set to 0, otherwise the server might
// confuse the marker bit with the highest bit in a normal
// PointerEvent message.
mask = mask & 0x7f;
sock.sQpush8(mask);
sock.sQpush16(x);
@@ -2983,6 +3128,27 @@ RFB.messages = {
sock.flush();
},
extendedPointerEvent(sock, x, y, mask) {
sock.sQpush8(5); // msg-type
let higherBits = (mask >> 7) & 0xff;
// Bits 2-7 are reserved
if (higherBits & 0xfc) {
throw new Error("Invalid mouse button mask: " + mask);
}
let lowerBits = mask & 0x7f;
lowerBits |= 0x80; // Set marker bit to 1
sock.sQpush8(lowerBits);
sock.sQpush16(x);
sock.sQpush16(y);
sock.sQpush8(higherBits);
sock.flush();
},
// Used to build Notify and Request data.
_buildExtendedClipboardFlags(actions, formats) {
let data = new Uint8Array(4);

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.
@@ -9,10 +9,11 @@
*/
import * as Log from './logging.js';
import Base64 from '../base64.js';
// Touch detection
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
// requried for Chrome debugger
// required for Chrome debugger
(document.ontouchstart !== undefined) ||
// required for MS Surface
(navigator.maxTouchPoints > 0) ||
@@ -70,6 +71,86 @@ try {
}
export const hasScrollbarGutter = _hasScrollbarGutter;
export let supportsWebCodecsH264Decode = false;
async function _checkWebCodecsH264DecodeSupport() {
if (!('VideoDecoder' in window)) {
return false;
}
// We'll need to make do with some placeholders here
const config = {
codec: 'avc1.42401f',
codedWidth: 1920,
codedHeight: 1080,
optimizeForLatency: true,
};
let support = await VideoDecoder.isConfigSupported(config);
if (!support.supported) {
return false;
}
// Firefox incorrectly reports supports for H.264 under some
// circumstances, so we need to actually test a real frame
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932392
const data = new Uint8Array(Base64.decode(
'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' +
'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' +
'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' +
'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' +
'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' +
'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' +
'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' +
'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' +
'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' +
'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' +
'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' +
'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' +
'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' +
'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw=='));
let gotframe = false;
let error = null;
let decoder = new VideoDecoder({
output: (frame) => { gotframe = true; },
error: (e) => { error = e; },
});
let chunk = new EncodedVideoChunk({
timestamp: 0,
type: 'key',
data: data,
});
decoder.configure(config);
decoder.decode(chunk);
try {
await decoder.flush();
} catch (e) {
// Firefox incorrectly throws an exception here
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932566
error = e;
}
// Firefox fails to deliver the error on Windows, so we need to
// check if we got a frame instead
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932579
if (!gotframe) {
return false;
}
if (error !== null) {
return false;
}
return true;
}
supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
/*
* The functions for detection of platforms and browsers below are exported
* but the use of these should be minimized as much as possible.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
*/

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2018 The noVNC Authors
* Copyright (C) 2018 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2020 The noVNC Authors
* Copyright (C) 2020 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* noVNC: HTML5 VNC client
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* See README.md for usage and integration instructions.

View File

@@ -1,6 +1,6 @@
/*
* Websock: high-performance buffering wrapper
* Copyright (C) 2019 The noVNC Authors
* Copyright (C) 2019 The noVNC authors
* Licensed under MPL 2.0 (see LICENSE.txt)
*
* Websock is similar to the standard WebSocket / RTCDataChannel object
@@ -70,7 +70,7 @@ export default class Websock {
};
}
// Getters and Setters
// Getters and setters
get readyState() {
let subState;
@@ -94,7 +94,7 @@ export default class Websock {
return "unknown";
}
// Receive Queue
// Receive queue
rQpeek8() {
return this._rQ[this._rQi];
}
@@ -173,7 +173,7 @@ export default class Websock {
return false;
}
// Send Queue
// Send queue
sQpush8(num) {
this._sQensureSpace(1);
@@ -208,7 +208,7 @@ export default class Websock {
chunkSize = bytes.length - offset;
}
this._sQ.set(bytes.subarray(offset, chunkSize), this._sQlen);
this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
this._sQlen += chunkSize;
offset += chunkSize;
}
@@ -227,7 +227,7 @@ export default class Websock {
}
}
// Event Handlers
// Event handlers
off(evt) {
this._eventHandlers[evt] = () => {};
}
@@ -325,7 +325,7 @@ export default class Websock {
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
this._rQbufferSize = MAX_RQ_GROW_SIZE;
if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
throw new Error("Receive queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
}
}

View File

@@ -1,6 +1,6 @@
{
"name": "@novnc/novnc",
"version": "1.5.0",
"version": "1.6.0",
"description": "An HTML5 VNC client",
"browser": "lib/rfb",
"directories": {
@@ -55,10 +55,8 @@
"karma-mocha-reporter": "latest",
"karma-safari-launcher": "latest",
"karma-script-launcher": "latest",
"karma-sinon-chai": "latest",
"mocha": "latest",
"node-getopt": "latest",
"po2json": "latest",
"pofile": "latest",
"sinon": "latest",
"sinon-chai": "latest"
},

View File

@@ -0,0 +1,416 @@
<!DOCTYPE html>
<html lang="en" class="noVNC_loading">
<head>
<!--
noVNC example: simple example using default UI
Copyright (C) 2019 The noVNC authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&encrypt=1
or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
-->
<title>noVNC</title>
<link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
<meta name="theme-color" content="#313131">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- @2x -->
<link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
<link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
<link rel="apple-touch-icon" sizes="80x80" type="image/png" href="app/images/icons/novnc-ios-80.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-ios-152.png">
<link rel="apple-touch-icon" sizes="167x167" type="image/png" href="app/images/icons/novnc-ios-167.png">
<!-- @3x -->
<link rel="apple-touch-icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-ios-60.png">
<link rel="apple-touch-icon" sizes="87x87" type="image/png" href="app/images/icons/novnc-ios-87.png">
<link rel="apple-touch-icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-ios-120.png">
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/constants.css">
<link rel="stylesheet" href="app/styles/base.css">
<link rel="stylesheet" href="app/styles/input.css">
<!-- Images that will later appear via CSS -->
<link rel="preload" as="image" href="app/images/info.svg">
<link rel="preload" as="image" href="app/images/error.svg">
<link rel="preload" as="image" href="app/images/warning.svg">
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module">
import UI from "./app/ui.js";
import * as Log from './core/util/logging.js';
let response;
let defaults = {};
let mandatory = {};
// Default settings will be loaded from defaults.json. Mandatory
// settings will be loaded from mandatory.json, which the user
// cannot change.
try {
response = await fetch('./defaults.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
defaults = await response.json();
} catch (err) {
Log.Error("Couldn't fetch defaults.json: " + err);
}
try {
response = await fetch('./mandatory.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
mandatory = await response.json();
} catch (err) {
Log.Error("Couldn't fetch mandatory.json: " + err);
}
// You can also override any defaults you need here:
//
// defaults['host'] = 'vnc.example.com';
// Or force a specific setting, preventing the user from
// changing it:
//
// mandatory['view_only'] = true;
// See docs/EMBEDDING.md for a list of possible settings.
UI.start({ settings: { defaults: defaults,
mandatory: mandatory } });
</script>
</head>
<body>
<div id="noVNC_fallback_error" class="noVNC_center">
<div>
<div>noVNC encountered an error:</div>
<br>
<div id="noVNC_fallback_errormsg"></div>
</div>
</div>
<!-- noVNC control bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
<div id="noVNC_control_bar_handle" title="Hide/Show the control bar"><div></div></div>
<div class="noVNC_scroll">
<h1 class="noVNC_logo" translate="no"><span>no</span><br>VNC</h1>
<hr>
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
title="Move/Drag viewport">
<!--noVNC touch device only buttons-->
<div id="noVNC_mobile_buttons">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
</div>
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show extra keys">
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
id="noVNC_toggle_ctrl_button" class="noVNC_button"
title="Toggle Ctrl">
<input type="image" alt="Alt" src="app/images/alt.svg"
id="noVNC_toggle_alt_button" class="noVNC_button"
title="Toggle Alt">
<input type="image" alt="Windows" src="app/images/windows.svg"
id="noVNC_toggle_windows_button" class="noVNC_button"
title="Toggle Windows">
<input type="image" alt="Tab" src="app/images/tab.svg"
id="noVNC_send_tab_button" class="noVNC_button"
title="Send Tab">
<input type="image" alt="Esc" src="app/images/esc.svg"
id="noVNC_send_esc_button" class="noVNC_button"
title="Send Escape">
<input type="image" alt="Ctrl+Alt+Del" src="app/images/ctrlaltdel.svg"
id="noVNC_send_ctrl_alt_del_button" class="noVNC_button"
title="Send Ctrl-Alt-Del">
</div>
</div>
<!-- Shutdown/Reboot -->
<input type="image" alt="Shutdown/Reboot" src="app/images/power.svg"
id="noVNC_power_button" class="noVNC_button"
title="Shutdown/Reboot...">
<div class="noVNC_vcenter">
<div id="noVNC_power" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/power.svg"> Power
</div>
<input type="button" id="noVNC_shutdown_button" value="Shutdown">
<input type="button" id="noVNC_reboot_button" value="Reboot">
<input type="button" id="noVNC_reset_button" value="Reset">
</div>
</div>
<!-- Clipboard -->
<input type="image" alt="Clipboard" src="app/images/clipboard.svg"
id="noVNC_clipboard_button" class="noVNC_button"
title="Clipboard">
<div class="noVNC_vcenter">
<div id="noVNC_clipboard" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/clipboard.svg"> Clipboard
</div>
<p class="noVNC_subheading">
Edit clipboard content in the textarea below.
</p>
<textarea id="noVNC_clipboard_text" rows=5></textarea>
</div>
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Full screen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
title="Full screen">
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg"
id="noVNC_settings_button" class="noVNC_button"
title="Settings">
<div class="noVNC_vcenter">
<div id="noVNC_settings" class="noVNC_panel">
<div class="noVNC_heading">
<img alt="" src="app/images/settings.svg"> Settings
</div>
<ul>
<li>
<label>
<input id="noVNC_setting_shared" type="checkbox"
class="toggle">
Shared mode
</label>
</li>
<li>
<label>
<input id="noVNC_setting_view_only" type="checkbox"
class="toggle">
View only
</label>
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_view_clip" type="checkbox"
class="toggle">
Clip to window
</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local scaling</option>
<option value="remote">Remote resizing</option>
</select>
</li>
<li><hr></li>
<li>
<div class="noVNC_expander">Advanced</div>
<div><ul>
<li>
<label for="noVNC_setting_quality">Quality:</label>
<input id="noVNC_setting_quality" type="range" min="0" max="9" value="6">
</li>
<li>
<label for="noVNC_setting_compression">Compression level:</label>
<input id="noVNC_setting_compression" type="range" min="0" max="9" value="2">
</li>
<li><hr></li>
<li>
<label for="noVNC_setting_repeaterID">Repeater ID:</label>
<input id="noVNC_setting_repeaterID" type="text" value="">
</li>
<li>
<div class="noVNC_expander">WebSocket</div>
<div><ul>
<li>
<label>
<input id="noVNC_setting_encrypt" type="checkbox"
class="toggle">
Encrypt
</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
<input id="noVNC_setting_host">
</li>
<li>
<label for="noVNC_setting_port">Port:</label>
<input id="noVNC_setting_port" type="number">
</li>
<li>
<label for="noVNC_setting_path">Path:</label>
<input id="noVNC_setting_path" type="text" value="websockify">
</li>
</ul></div>
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_reconnect" type="checkbox"
class="toggle">
Automatic reconnect
</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label>
<input id="noVNC_setting_show_dot" type="checkbox"
class="toggle">
Show dot when no cursor
</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
<li>
<label>Logging:
<select id="noVNC_setting_logging" name="vncLogging">
</select>
</label>
</li>
</ul></div>
</li>
<li class="noVNC_version_separator"><hr></li>
<li class="noVNC_version_wrapper">
<span>Version:</span>
<span class="noVNC_version"></span>
</li>
</ul>
</div>
</div>
<!-- Connection controls -->
<input type="image" alt="Disconnect" src="app/images/disconnect.svg"
id="noVNC_disconnect_button" class="noVNC_button"
title="Disconnect">
</div>
</div>
</div> <!-- End of noVNC_control_bar -->
<div id="noVNC_hint_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar_hint">
</div>
</div>
<!-- Status dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
<div class="noVNC_center">
<div id="noVNC_connect_dlg">
<p class="noVNC_logo" translate="no"><span>no</span>VNC</p>
<div>
<button id="noVNC_connect_button">
<img alt="" src="app/images/connect.svg"> Connect
</button>
</div>
</div>
</div>
<!-- Server key verification dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
Server identity
</div>
<div>
The server has provided the following identifying information:
</div>
<div id="noVNC_fingerprint_block">
Fingerprint:
<span id="noVNC_fingerprint"></span>
</div>
<div>
Please verify that the information is correct and press
"Approve". Otherwise press "Reject".
</div>
<div class="button_row">
<input id="noVNC_approve_server_button" type="submit" value="Approve">
<input id="noVNC_reject_server_button" type="button" value="Reject">
</div>
</form></div>
</div>
<!-- Password dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
Credentials
</div>
<div id="noVNC_username_block">
<label for="noVNC_username_input">Username:</label>
<input id="noVNC_username_input">
</div>
<div id="noVNC_password_block">
<label for="noVNC_password_input">Password:</label>
<input id="noVNC_password_input" type="password">
</div>
<div class="button_row">
<input id="noVNC_credentials_button" type="submit" value="Send credentials">
</div>
</form></div>
</div>
<!-- Transition screens -->
<div id="noVNC_transition">
<div id="noVNC_transition_text"></div>
<div>
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel">
</div>
<div class="noVNC_spinner"></div>
</div>
<!-- This is where the RFB elements will attach -->
<div id="noVNC_container">
<!-- Note that Google Chrome on Android doesn't respect any of these,
html attributes which attempt to disable text suggestions on the
on-screen keyboard. Let's hope Chrome implements the ime-mode
style for example -->
<textarea id="noVNC_keyboardinput" autocapitalize="off"
autocomplete="off" spellcheck="false" tabindex="-1"></textarea>
</div>
<audio id="noVNC_bell">
<source src="app/sounds/bell.oga" type="audio/ogg">
<source src="app/sounds/bell.mp3" type="audio/mpeg">
</audio>
</body>
</html>

View File

@@ -0,0 +1,180 @@
<!DOCTYPE html>
<html lang="en">
<head>
<!--
noVNC example: lightweight example using minimal UI and features
This is a self-contained file which doesn't import WebUtil or external CSS.
Copyright (C) 2019 The noVNC authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
Connect parameters are provided in query string:
http://example.com/?host=HOST&port=PORT&scale=true
-->
<title>noVNC</title>
<style>
body {
margin: 0;
background-color: dimgrey;
height: 100%;
display: flex;
flex-direction: column;
}
html {
height: 100%;
}
#top_bar {
background-color: #6e84a3;
color: white;
font: bold 12px Helvetica;
padding: 6px 5px 4px 5px;
border-bottom: 1px outset;
}
#status {
text-align: center;
}
#sendCtrlAltDelButton {
position: fixed;
top: 0px;
right: 0px;
border: 1px outset;
padding: 5px 5px 4px 5px;
cursor: pointer;
}
#screen {
flex: 1; /* fill remaining space */
overflow: hidden;
}
</style>
<script type="module" crossorigin="anonymous">
// RFB holds the API to connect and communicate with a VNC server
import RFB from './core/rfb.js';
let rfb;
let desktopName;
// When this function is called we have
// successfully connected to a server
function connectedToServer(e) {
status("Connected to " + desktopName);
}
// This function is called when we are disconnected
function disconnectedFromServer(e) {
if (e.detail.clean) {
status("Disconnected");
} else {
status("Something went wrong, connection is closed");
}
}
// When this function is called, the server requires
// credentials to authenticate
function credentialsAreRequired(e) {
const password = prompt("Password required:");
rfb.sendCredentials({ password: password });
}
// When this function is called we have received
// a desktop name from the server
function updateDesktopName(e) {
desktopName = e.detail.name;
}
// Since most operating systems will catch Ctrl+Alt+Del
// before they get a chance to be intercepted by the browser,
// we provide a way to emulate this key sequence.
function sendCtrlAltDel() {
rfb.sendCtrlAltDel();
return false;
}
// Show a status text in the top bar
function status(text) {
document.getElementById('status').textContent = text;
}
// This function extracts the value of one variable from the
// query string. If the variable isn't defined in the URL
// it returns the default value instead.
function readQueryVariable(name, defaultValue) {
// A URL with a query parameter can look like this:
// https://www.example.com?myqueryparam=myvalue
//
// Note that we use location.href instead of location.search
// because Firefox < 53 has a bug w.r.t location.search
const re = new RegExp('.*[?&]' + name + '=([^&#]*)'),
match = document.location.href.match(re);
if (match) {
// We have to decode the URL since want the cleartext value
return decodeURIComponent(match[1]);
}
return defaultValue;
}
document.getElementById('sendCtrlAltDelButton')
.onclick = sendCtrlAltDel;
// Read parameters specified in the URL query string
// By default, use the host and port of server that served this file
const host = readQueryVariable('host', window.location.hostname);
let port = readQueryVariable('port', window.location.port);
const password = readQueryVariable('password');
const path = readQueryVariable('path', 'websockify');
// | | | | | |
// | | | Connect | | |
// v v v v v v
status("Connecting");
// Build the websocket URL used to connect
let url;
if (window.location.protocol === "https:") {
url = 'wss';
} else {
url = 'ws';
}
url += '://' + host;
if(port) {
url += ':' + port;
}
url += '/' + path;
// Creating a new RFB object will start a new connection
rfb = new RFB(document.getElementById('screen'), url,
{ credentials: { password: password } });
// Add listeners to important events from the RFB module
rfb.addEventListener("connect", connectedToServer);
rfb.addEventListener("disconnect", disconnectedFromServer);
rfb.addEventListener("credentialsrequired", credentialsAreRequired);
rfb.addEventListener("desktopname", updateDesktopName);
// Set parameters that can be changed on an active connection
rfb.viewOnly = readQueryVariable('view_only', false);
rfb.scaleViewport = readQueryVariable('scale', false);
</script>
</head>
<body>
<div id="top_bar">
<div id="status">Loading</div>
<div id="sendCtrlAltDelButton">Send CtrlAltDel</div>
</div>
<div id="screen">
<!-- This is where the remote screen will appear -->
</div>
</body>
</html>

View File

@@ -0,0 +1,37 @@
#!/usr/bin/php
<?php
/* Copyright 2005-2024, Lime Technology
* Copyright 2024, Simon Fairweather
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
# Command for bash script /usr/libexec/virtiofsd
# eval exec /usr/bin/virtiofsd $(/usr/local/emhttp/plugins/dynamix.vm.manager/scripts/virtiofsd.php "$@")
$docroot ??= ($_SERVER['DOCUMENT_ROOT'] ?: '/usr/local/emhttp');
require_once "$docroot/webGui/include/Helpers.php";
require_once "$docroot/plugins/dynamix.vm.manager/include/libvirt_helpers.php";
$pci_device_changes = comparePCIData();
$pcierror = false;
$pci_addresses = [];
foreach ($argv as $arg) {
if (preg_match('/"host"\s*:\s*"([^"]+)"/', $arg, $matches)) {
$pci_addresses[] = $matches[1];
}
}
foreach($pci_addresses as $pciid) {
if (isset($pci_device_changes[$pciid])) {
$pcierror = true;
}
}
echo $pcierror == true ? "yes" : "no";
?>

View File

@@ -245,6 +245,7 @@ if (isset($_POST['updatevm'])) {
$xml = $_POST['xmldesc'];
$arrExistingConfig = custom::createArray('domain',$xml);
$newuuid = $arrExistingConfig['uuid'];
if ($_POST['template']['iconold'] != $_POST['template']['icon']) $xml = preg_replace('/icon="[^"]*"/','icon="' . $_POST['template']['icon'] . '"',$xml);
$xml = str_replace($olduuid,$newuuid,$xml);
} else {
// form view

View File

@@ -4,7 +4,7 @@
<!--
noVNC example: simple example using default UI
Copyright (C) 2019 The noVNC Authors
Copyright (C) 2019 The noVNC authors
noVNC is licensed under the MPL 2.0 (see LICENSE.txt)
This file is licensed under the 2-Clause BSD license (see LICENSE.txt).
@@ -12,43 +12,17 @@
http://example.com/?host=HOST&port=PORT&encrypt=1
or the fragment:
http://example.com/#host=HOST&port=PORT&encrypt=1
1.5
-->
<title>noVNC</title>
<base href="novnc/">
<meta charset="utf-8">
<!-- Always force latest IE rendering engine (even in intranet) & Chrome Frame
Remove this if you use the .htaccess -->
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<!-- Icons (see app/images/icons/Makefile for what the sizes are for) -->
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
<link rel="icon" sizes="24x24" type="image/png" href="app/images/icons/novnc-24x24.png">
<link rel="icon" sizes="32x32" type="image/png" href="app/images/icons/novnc-32x32.png">
<link rel="icon" sizes="48x48" type="image/png" href="app/images/icons/novnc-48x48.png">
<link rel="icon" sizes="60x60" type="image/png" href="app/images/icons/novnc-60x60.png">
<link rel="icon" sizes="64x64" type="image/png" href="app/images/icons/novnc-64x64.png">
<link rel="icon" sizes="72x72" type="image/png" href="app/images/icons/novnc-72x72.png">
<link rel="icon" sizes="76x76" type="image/png" href="app/images/icons/novnc-76x76.png">
<link rel="icon" sizes="96x96" type="image/png" href="app/images/icons/novnc-96x96.png">
<link rel="icon" sizes="120x120" type="image/png" href="app/images/icons/novnc-120x120.png">
<link rel="icon" sizes="144x144" type="image/png" href="app/images/icons/novnc-144x144.png">
<link rel="icon" sizes="152x152" type="image/png" href="app/images/icons/novnc-152x152.png">
<link rel="icon" sizes="192x192" type="image/png" href="app/images/icons/novnc-192x192.png">
<!-- Firefox currently mishandles SVG, see #1419039
<link rel="icon" sizes="any" type="image/svg+xml" href="app/images/icons/novnc-icon.svg">
-->
<!-- Repeated last so that legacy handling will pick this -->
<link rel="icon" sizes="16x16" type="image/png" href="app/images/icons/novnc-16x16.png">
<link rel="icon" type="image/x-icon" href="app/images/icons/novnc.ico">
<meta name="theme-color" content="#313131">
<!-- Apple iOS Safari settings -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<!-- Home Screen Icons (favourites and bookmarks use the normal icons) -->
<!-- @2x -->
<link rel="apple-touch-icon" sizes="40x40" type="image/png" href="app/images/icons/novnc-ios-40.png">
<link rel="apple-touch-icon" sizes="58x58" type="image/png" href="app/images/icons/novnc-ios-58.png">
@@ -63,6 +37,7 @@
<link rel="apple-touch-icon" sizes="180x180" type="image/png" href="app/images/icons/novnc-ios-180.png">
<!-- Stylesheets -->
<link rel="stylesheet" href="app/styles/constants.css">
<link rel="stylesheet" href="app/styles/base.css">
<link rel="stylesheet" href="app/styles/input.css">
@@ -72,7 +47,56 @@
<link rel="preload" as="image" href="app/images/warning.svg">
<script type="module" crossorigin="anonymous" src="app/error-handler.js"></script>
<script type="module" crossorigin="anonymous" src="app/ui.js"></script>
<script type="module">
import UI from "./app/ui.js";
import * as Log from './core/util/logging.js';
let response;
let defaults = {};
let mandatory = {};
// Default settings will be loaded from defaults.json. Mandatory
// settings will be loaded from mandatory.json, which the user
// cannot change.
try {
response = await fetch('./defaults.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
defaults = await response.json();
} catch (err) {
Log.Error("Couldn't fetch defaults.json: " + err);
}
try {
response = await fetch('./mandatory.json');
if (!response.ok) {
throw Error("" + response.status + " " + response.statusText);
}
mandatory = await response.json();
} catch (err) {
Log.Error("Couldn't fetch mandatory.json: " + err);
}
// You can also override any defaults you need here:
//
// defaults['host'] = 'vnc.example.com';
// Or force a specific setting, preventing the user from
// changing it:
//
// mandatory['view_only'] = true;
// See docs/EMBEDDING.md for a list of possible settings.
UI.start({ settings: { defaults: defaults,
mandatory: mandatory } });
</script>
</head>
<body>
@@ -85,7 +109,7 @@
</div>
</div>
<!-- noVNC Control Bar -->
<!-- noVNC control bar -->
<div id="noVNC_control_bar_anchor" class="noVNC_vcenter">
<div id="noVNC_control_bar">
@@ -100,18 +124,18 @@
<!-- Drag/Pan the viewport -->
<input type="image" alt="Drag" src="app/images/drag.svg"
id="noVNC_view_drag_button" class="noVNC_button noVNC_hidden"
title="Move/Drag Viewport">
title="Move/Drag viewport">
<!--noVNC Touch Device only buttons-->
<!--noVNC touch device only buttons-->
<div id="noVNC_mobile_buttons">
<input type="image" alt="Keyboard" src="app/images/keyboard.svg"
id="noVNC_keyboard_button" class="noVNC_button" title="Show Keyboard">
id="noVNC_keyboard_button" class="noVNC_button" title="Show keyboard">
</div>
<!-- Extra manual keys -->
<input type="image" alt="Extra keys" src="app/images/toggleextrakeys.svg"
id="noVNC_toggle_extra_keys_button" class="noVNC_button"
title="Show Extra Keys">
title="Show extra keys">
<div class="noVNC_vcenter">
<div id="noVNC_modifiers" class="noVNC_panel">
<input type="image" alt="Ctrl" src="app/images/ctrl.svg"
@@ -167,9 +191,9 @@
</div>
<!-- Toggle fullscreen -->
<input type="image" alt="Full Screen" src="app/images/fullscreen.svg"
<input type="image" alt="Full screen" src="app/images/fullscreen.svg"
id="noVNC_fullscreen_button" class="noVNC_button noVNC_hidden"
title="Full Screen">
title="Full screen">
<!-- Settings -->
<input type="image" alt="Settings" src="app/images/settings.svg"
@@ -182,21 +206,33 @@
</div>
<ul>
<li>
<label><input id="noVNC_setting_shared" type="checkbox"> Shared Mode</label>
<label>
<input id="noVNC_setting_shared" type="checkbox"
class="toggle">
Shared mode
</label>
</li>
<li>
<label><input id="noVNC_setting_view_only" type="checkbox"> View Only</label>
<label>
<input id="noVNC_setting_view_only" type="checkbox"
class="toggle">
View only
</label>
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_view_clip" type="checkbox"> Clip to Window</label>
<label>
<input id="noVNC_setting_view_clip" type="checkbox"
class="toggle">
Clip to window
</label>
</li>
<li>
<label for="noVNC_setting_resize">Scaling Mode:</label>
<label for="noVNC_setting_resize">Scaling mode:</label>
<select id="noVNC_setting_resize" name="vncResize">
<option value="off">None</option>
<option value="scale">Local Scaling</option>
<option value="remote">Remote Resizing</option>
<option value="scale">Local scaling</option>
<option value="remote">Remote resizing</option>
</select>
</li>
<li><hr></li>
@@ -220,7 +256,11 @@
<div class="noVNC_expander">WebSocket</div>
<div><ul>
<li>
<label><input id="noVNC_setting_encrypt" type="checkbox"> Encrypt</label>
<label>
<input id="noVNC_setting_encrypt" type="checkbox"
class="toggle">
Encrypt
</label>
</li>
<li>
<label for="noVNC_setting_host">Host:</label>
@@ -238,15 +278,23 @@
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_reconnect" type="checkbox"> Automatic Reconnect</label>
<label>
<input id="noVNC_setting_reconnect" type="checkbox"
class="toggle">
Automatic reconnect
</label>
</li>
<li>
<label for="noVNC_setting_reconnect_delay">Reconnect Delay (ms):</label>
<label for="noVNC_setting_reconnect_delay">Reconnect delay (ms):</label>
<input id="noVNC_setting_reconnect_delay" type="number">
</li>
<li><hr></li>
<li>
<label><input id="noVNC_setting_show_dot" type="checkbox"> Show Dot when No Cursor</label>
<label>
<input id="noVNC_setting_show_dot" type="checkbox"
class="toggle">
Show dot when no cursor
</label>
</li>
<li><hr></li>
<!-- Logging selection dropdown -->
@@ -267,7 +315,7 @@
</div>
</div>
<!-- Connection Controls -->
<!-- Connection controls -->
<input type="image" alt="Disconnect" src="app/images/disconnect.svg"
id="noVNC_disconnect_button" class="noVNC_button"
title="Disconnect">
@@ -282,7 +330,7 @@
</div>
</div>
<!-- Status Dialog -->
<!-- Status dialog -->
<div id="noVNC_status"></div>
<!-- Connect button -->
@@ -297,7 +345,7 @@
</div>
</div>
<!-- Server Key Verification Dialog -->
<!-- Server key verification dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_verify_server_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
@@ -307,21 +355,21 @@
The server has provided the following identifying information:
</div>
<div id="noVNC_fingerprint_block">
<b>Fingerprint:</b>
Fingerprint:
<span id="noVNC_fingerprint"></span>
</div>
<div>
Please verify that the information is correct and press
"Approve". Otherwise press "Reject".
</div>
<div>
<input id="noVNC_approve_server_button" type="submit" value="Approve" class="noVNC_submit">
<input id="noVNC_reject_server_button" type="button" value="Reject" class="noVNC_submit">
<div class="button_row">
<input id="noVNC_approve_server_button" type="submit" value="Approve">
<input id="noVNC_reject_server_button" type="button" value="Reject">
</div>
</form></div>
</div>
<!-- Password Dialog -->
<!-- Password dialog -->
<div class="noVNC_center noVNC_connect_layer">
<div id="noVNC_credentials_dlg" class="noVNC_panel"><form>
<div class="noVNC_heading">
@@ -335,17 +383,17 @@
<label for="noVNC_password_input">Password:</label>
<input id="noVNC_password_input" type="password">
</div>
<div>
<input id="noVNC_credentials_button" type="submit" value="Send Credentials" class="noVNC_submit">
<div class="button_row">
<input id="noVNC_credentials_button" type="submit" value="Send credentials">
</div>
</form></div>
</div>
<!-- Transition Screens -->
<!-- Transition screens -->
<div id="noVNC_transition">
<div id="noVNC_transition_text"></div>
<div>
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel" class="noVNC_submit">
<input type="button" id="noVNC_cancel_reconnect_button" value="Cancel">
</div>
<div class="noVNC_spinner"></div>
</div>

View File

@@ -466,11 +466,11 @@ function formatWarning(val) {
window.onunload = function(){
<?if (_var($var,'fsState')=='Started'):?>
mymonitor.stop();
devices.stop();
try {mymonitor.stop();} catch(e) {}
try {devices.stop();} catch(e) {}
<?endif;?>
<?if ($spot==0):?>
paritymonitor.stop();
try {paritymonitor.stop();} catch(e) {}
<?endif;?>
}
</script>

View File

@@ -360,10 +360,10 @@ function doAction(action, title, id) {
if (!access.includes(path[0])) return;
var user = /^(user0?|rootshare)$/.test(path[1]);
var root = '/'+path[0]+(user ? '/'+path[1] : '');
var share = user||!path[2]||(link&&path.length==3) ? '' : path[2]+'/';
var share = user || !path[2] || (link && path.length==3) ? '' : path[2]+'/';
var ud = ['disks','remotes'].includes(path[1]); // unassigned devices
var match = ud||user ? '' : '^(?!\\/mnt\\/user0?\\/).*$';
var name = path.pop()||path.pop();
var match = ud || user ? '' : '^(?!\\/mnt\\/(user0?|rootshare)\\/).*$';
var name = path.pop() || path.pop();
var hdlink = "<?=$var['fuse_directio'] == 1 ? '1' : ''?>";
dfm.window = $("#dfm_dialogWindow");
switch (action) {
@@ -488,7 +488,7 @@ function doAction(action, title, id) {
case 4: // move folder
case 9: // move file
// disallow mixing of disk and user shares
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
// check if 'mv' can be used
if (path.length > 2) {
if (user) {
@@ -520,7 +520,7 @@ function doAction(action, title, id) {
break;
default:
// disallow mixing of disk and user shares
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
break;
}
if (!target || !valid.test(target)) {errorTarget(); return;}
@@ -558,7 +558,7 @@ function doAction(action, title, id) {
case 4: // move folder
case 9: // move file
// disallow mixing of disk and user shares
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
// check if 'mv' can be used
if (path.length > 2) {
if (user) {
@@ -583,7 +583,7 @@ function doAction(action, title, id) {
break;
default:
// disallow mixing of disk and user shares
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
break;
}
if (!target || !valid.test(target)) {errorTarget(); return;}
@@ -625,10 +625,10 @@ function doActions(action, title) {
if (!access.includes(path[0])) return;
var user = /^(user0?|rootshare)$/.test(path[1]);
var root = '/'+path[0]+(user ? '/'+path[1] : '');
var share = user||!path[2] ? '' : path[2]+'/';
var share = user || !path[2] ? '' : path[2]+'/';
var ud = ['disks','remotes'].includes(path[1]); // unassigned devices
var match = ud||user ? '' : '^(?!.*(user0?|rootshare)).*$';
var name = path.pop()||path.pop();
var match = ud || user ? '' : '^(?!\\/mnt\\/(user0?|rootshare)\\/).*$';
var name = path.pop() || path.pop();
var hdlink = "<?=$var['fuse_directio'] == 1 ? '1' : ''?>";
var u = false;
var d = false;
@@ -767,7 +767,7 @@ function doActions(action, title) {
break;
case 4: // move object
// disallow mixing of disk and user shares
var valid = ud ? /\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
// check if 'mv' can be used
if (path.length > 2) {
if (user) {
@@ -801,7 +801,7 @@ function doActions(action, title) {
break;
default:
// disallow mixing of disk and user shares
var valid = ud ? /\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
break;
}
if (!target || !valid.test(target)) {errorTarget(); return;}
@@ -839,7 +839,7 @@ function doActions(action, title) {
break;
case 4: // move object
// disallow mixing of disk and user shares
var valid = ud ? /\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
// check if 'mv' can be used
if (path.length > 2) {
if (user) {
@@ -866,7 +866,7 @@ function doActions(action, title) {
break;
default:
// disallow mixing of disk and user shares
var valid = ud ? /\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|rootshare)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)).+$|^\/boot\/.+/);
var valid = ud ? /^\/mnt\/.+/ : (user ? /^\/mnt\/(user0?|disks|remotes)\/.+/ : /^\/mnt\/(?!.*(user0?|rootshare)\/).+$|^\/boot\/.+/);
break;
}
if (!target || !valid.test(target)) {errorTarget(); return;}

View File

@@ -37,8 +37,7 @@ function customTiles($column) {
}
// adjust the text color in log window
$fgcolor = in_array($theme,['white','azure']) ? '#1c1c1c' : '#f2f2f2';
exec("sed -ri 's/^\.logLine\{color:#......;/.logLine{color:$fgcolor;/' $docroot/plugins/dynamix.docker.manager/log.htm >/dev/null &");
$themeHelper->updateDockerLogColor($docroot); // $themeHelper set in DefaultPageLayout.php
exec("/etc/rc.d/rc.docker status >/dev/null",$dummy,$dockerd);
exec("/etc/rc.d/rc.libvirt status >/dev/null",$dummy,$libvirtd);
@@ -108,17 +107,25 @@ $names = [];
$SMBpublic = $NFSpublic= 0;
foreach ($sec as $share => $prop) {
if ( $prop['export'] == "-") continue;
if ( $prop['security'] == "public") $SMBpublic++;
if ( ($var['shareDisk']??"") == "no" && $share !=="flash" ) continue;
if ( $prop['security'] == "public") {
if ( $share == "flash" || (! isset($disks[$share]) || (isset($disks[$share]) && ($var['shareDisk']??"") == "yes" && $share !=="flash") ) )
$SMBpublic++;
}
if ( ($var['shareDisk']??"") !== "yes" && $share !=="flash" ) continue;
if ( ! $started && isset($disks[$share]) && $share !=="flash" ) continue;
if ( ! isset($shares[$share]) && isset($disks[$share]) ) {$shares[$share]=$disks[$share]; $shares[$share]['diskexport'] = true;}
if ( (! isset($shares[$share]) && isset($disks[$share]) && ($var['shareDisk']??"") == "yes" ) || $share == "flash" ) {$shares[$share]=$disks[$share]; $shares[$share]['diskexport'] = true;}
}
foreach ($sec_nfs as $share => $prop) {
if ( $prop['export'] == "-") continue;
if ( $prop['security'] == "public") $NFSpublic++;
if ( ($var['shareDisk']??"") == "no" && $share !=="flash" ) continue;
if ( ! $started && isset($disks[$share]) && $share !=="flash" ) continue;
if ( ! isset($shares[$share]) && isset($disks[$share]) ) {$shares[$share]=$disks[$share]; $shares[$share]['diskexport'] = true;}
if ($var['shareNFSEnabled']=='yes') {
foreach ($sec_nfs as $share => $prop) {
if ( $prop['export'] == "-") continue;
if ( $prop['security'] == "public") {
if ( $share == "flash" || (! isset($disks[$share]) || (isset($disks[$share]) && ($var['shareDisk']??"") == "yes" && $share !== "flash" ) ) )
$NFSpublic++;
}
if ( ($var['shareDisk']??"") == "no" && $share !=="flash" ) continue;
if ( ! $started && isset($disks[$share]) && $share !=="flash" ) continue;
if ( ( ! isset($shares[$share]) && isset($disks[$share]) && ($var['shareDisk']??"") == "yes") || $share == "flash" ) {$shares[$share]=$disks[$share]; $shares[$share]['diskexport'] = true;}
}
}
$passwd = $nopass = 0;
@@ -180,7 +187,7 @@ $ramsize = my_scale($total,$unit,0,-1,1024)." $unit";
$low = $memory_maximum < $memory_installed;
if ($low) $memory_maximum = pow(2,ceil(log($memory_installed)/log(2)));
switch ($theme) {
switch ($themeHelper->getThemeName()) { // $themeHelper set in DefaultPageLayout.php
case 'white': $color = '#1c1b1b'; $grid = '#e3e3e3'; $c0 = '#a8a8a8'; $c1 = '#dcdcdc'; break;
case 'black': $color = '#f2f2f2'; $grid = '#2b2b2b'; $c0 = '#787878'; $c1 = '#444444'; break;
case 'azure': $color = '#606e7f'; $grid = '#f3f0f4'; $c0 = '#606e7f'; $c1 = '#eceaec'; break;
@@ -781,8 +788,10 @@ jQuery.prototype.mixedView = function(s) {
}
}
<?if (file_exists($cookie)):?>
var cookie = JSON.parse('<?=file_get_contents($cookie)?>');
<?
$cookie_content = @file_get_contents($cookie);
if ( @json_decode($cookie_content,true) ):?>
var cookie = JSON.parse('<?=trim($cookie_content);?>');
<?else:?>
var cookie = {};
<?endif;?>
@@ -1547,7 +1556,7 @@ var sortableHelper = function(e,ui){
function LockButton() {
if ($.cookie('lockbutton') == null) {
$.cookie('lockbutton','lockbutton');
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton a').prop('title',"_(Lock sortable items)_");
$('div.nav-item.LockButton b').removeClass('icon-u-lock green-text').addClass('icon-u-lock-open red-text');
<?endif;?>
@@ -1579,7 +1588,7 @@ function LockButton() {
}});
} else {
$.removeCookie('lockbutton');
<?if ($themes1):?>
<?if ($themeHelper->isTopNavTheme()):?>
$('div.nav-item.LockButton a').prop('title',"_(Unlock sortable items)_");
$('div.nav-item.LockButton b').removeClass('icon-u-lock-open red-text').addClass('icon-u-lock green-text');
<?endif;?>

View File

@@ -3,7 +3,7 @@ Title="Display Settings"
Icon="icon-display"
Tag="desktop"
---
<?PHP
<?php
/* Copyright 2005-2025, Lime Technology
* Copyright 2012-2025, Bergware International.
*
@@ -15,7 +15,7 @@ Tag="desktop"
* all copies or substantial portions of the Software.
*/
?>
<?
<?php
$void = "<img src='/webGui/images/banner.png' id='image' width='330' height='30' onclick='$(&quot;#drop&quot;).click()' style='cursor:pointer' title='_(Click to select PNG file)_'>";
$icon = "<i class='fa fa-trash top' title='_(Restore default image)_' onclick='restore()'></i>";
$plugins = '/var/log/plugins';
@@ -129,8 +129,8 @@ $(function() {
_(Display width)_:
: <select name="width">
<?=mk_option($display['width'], "",_('Boxed'))?>
<?=mk_option($display['width'], "1",_('Unlimited'))?>
<?=mk_option($display['width'], "", _('Boxed'))?>
<?=mk_option($display['width'], "1", _('Unlimited'))?>
</select>
:display_width_help:
@@ -148,124 +148,123 @@ _(Language)_:
_(Font size)_:
: <select name="font" id='font'>
<?=mk_option($display['font'], "50",_('Very small'))?>
<?=mk_option($display['font'], "56.25",_('Small'))?>
<?=mk_option($display['font'], "",_('Normal'))?>
<?=mk_option($display['font'], "68.75",_('Large'))?>
<?=mk_option($display['font'], "75",_('Very large'))?>
<?=mk_option($display['font'], "80",_('Huge'))?>
<?=mk_option($display['font'], "50", _('Very small'))?>
<?=mk_option($display['font'], "56.25", _('Small'))?>
<?=mk_option($display['font'], "", _('Normal'))?>
<?=mk_option($display['font'], "68.75", _('Large'))?>
<?=mk_option($display['font'], "75", _('Very large'))?>
<?=mk_option($display['font'], "80", _('Huge'))?>
</select>
:display_font_size_help:
_(Terminal font size)_:
: <select name="tty" id="tty">
<?=mk_option($display['tty'], "11",_('Very small'))?>
<?=mk_option($display['tty'], "13",_('Small'))?>
<?=mk_option($display['tty'], "15",_('Normal'))?>
<?=mk_option($display['tty'], "17",_('Large'))?>
<?=mk_option($display['tty'], "19",_('Very large'))?>
<?=mk_option($display['tty'], "21",_('Huge'))?>
<?=mk_option($display['tty'], "11", _('Very small'))?>
<?=mk_option($display['tty'], "13", _('Small'))?>
<?=mk_option($display['tty'], "15", _('Normal'))?>
<?=mk_option($display['tty'], "17", _('Large'))?>
<?=mk_option($display['tty'], "19", _('Very large'))?>
<?=mk_option($display['tty'], "21", _('Huge'))?>
</select>
:display_tty_size_help:
_(Number format)_:
: <select name="number">
<?=mk_option($display['number'], ".,",_('[D] dot : [G] comma'))?>
<?=mk_option($display['number'], ". ",_('[D] dot : [G] space'))?>
<?=mk_option($display['number'], ".",_('[D] dot : [G] none'))?>
<?=mk_option($display['number'], ",.",_('[D] comma : [G] dot'))?>
<?=mk_option($display['number'], ", ",_('[D] comma : [G] space'))?>
<?=mk_option($display['number'], ",",_('[D] comma : [G] none'))?>
<?=mk_option($display['number'], ".,", _('[D] dot : [G] comma'))?>
<?=mk_option($display['number'], ". ", _('[D] dot : [G] space'))?>
<?=mk_option($display['number'], ".", _('[D] dot : [G] none'))?>
<?=mk_option($display['number'], ",.", _('[D] comma : [G] dot'))?>
<?=mk_option($display['number'], ", ", _('[D] comma : [G] space'))?>
<?=mk_option($display['number'], ",", _('[D] comma : [G] none'))?>
</select>
_(Number scaling)_:
: <select name="scale">
<?=mk_option($display['scale'], "-1",_('Automatic'))?>
<?=mk_option($display['scale'], "0",_('Disabled'))?>
<?=mk_option($display['scale'], "1",_('KB'))?>
<?=mk_option($display['scale'], "2",_('MB'))?>
<?=mk_option($display['scale'], "3",_('GB'))?>
<?=mk_option($display['scale'], "4",_('TB'))?>
<?=mk_option($display['scale'], "5",_('PB'))?>
<?=mk_option($display['scale'], "-1", _('Automatic'))?>
<?=mk_option($display['scale'], "0", _('Disabled'))?>
<?=mk_option($display['scale'], "1", _('KB'))?>
<?=mk_option($display['scale'], "2", _('MB'))?>
<?=mk_option($display['scale'], "3", _('GB'))?>
<?=mk_option($display['scale'], "4", _('TB'))?>
<?=mk_option($display['scale'], "5", _('PB'))?>
</select>
_(Page view)_:
: <select name="tabs">
<?=mk_option($display['tabs'], "0",_('Tabbed'))?>
<?=mk_option($display['tabs'], "1",_('Non-tabbed'))?>
<?=mk_option($display['tabs'], "0", _('Tabbed'))?>
<?=mk_option($display['tabs'], "1", _('Non-tabbed'))?>
</select>
:display_page_view_help:
_(Placement of Users menu)_:
: <select name="users">
<?=mk_option($display['users'], "Tasks:3",_('Header menu'))?>
<?=mk_option($display['users'], "UserPreferences",_('Settings menu'))?>
<?=mk_option($display['users'], "Tasks:3", _('Header menu'))?>
<?=mk_option($display['users'], "UserPreferences", _('Settings menu'))?>
</select>
:display_users_menu_help:
_(Listing height)_:
: <select name="resize">
<?=mk_option($display['resize'], "0",_('Automatic'))?>
<?=mk_option($display['resize'], "1",_('Fixed'))?>
<?=mk_option($display['resize'], "0", _('Automatic'))?>
<?=mk_option($display['resize'], "1", _('Fixed'))?>
</select>
:display_listing_height_help:
_(Display device name)_:
: <select name="raw">
<?=mk_option($display['raw'], "",_('Normalized'))?>
<?=mk_option($display['raw'], "1",_('Raw'))?>
<?=mk_option($display['raw'], "", _('Normalized'))?>
<?=mk_option($display['raw'], "1", _('Raw'))?>
</select>
_(Display world-wide-name in device ID)_:
: <select name="wwn">
<?=mk_option($display['wwn'], "0",_('Disabled'))?>
<?=mk_option($display['wwn'], "1",_('Automatic'))?>
<?=mk_option($display['wwn'], "0", _('Disabled'))?>
<?=mk_option($display['wwn'], "1", _('Automatic'))?>
</select>
:display_wwn_device_id_help:
_(Display array totals)_:
: <select name="total">
<?=mk_option($display['total'], "0",_('No'))?>
<?=mk_option($display['total'], "1",_('Yes'))?>
<?=mk_option($display['total'], "0", _('No'))?>
<?=mk_option($display['total'], "1", _('Yes'))?>
</select>
_(Show array utilization indicator)_:
: <select name="usage">
<?=mk_option($display['usage'], "0",_('No'))?>
<?=mk_option($display['usage'], "1",_('Yes'))?>
<?=mk_option($display['usage'], "0", _('No'))?>
<?=mk_option($display['usage'], "1", _('Yes'))?>
</select>
_(Temperature unit)_:
: <select name="unit">
<?=mk_option($display['unit'], "C",_('Celsius'))?>
<?=mk_option($display['unit'], "F",_('Fahrenheit'))?>
<?=mk_option($display['unit'], "C", _('Celsius'))?>
<?=mk_option($display['unit'], "F", _('Fahrenheit'))?>
</select>
:display_temperature_unit_help:
_(Dynamix color theme)_:
: <select name="theme">
<?foreach (glob("$docroot/webGui/styles/themes/*.css") as $themes):?>
<?$theme = basename($themes, '.css');?>
<?=mk_option($display['theme'], $theme, _(ucfirst($theme)))?>
<?endforeach;?>
<?foreach ($themeHelper->getThemesFromFileSystem($docroot) as $guiTheme):?>
<?=mk_option($themeHelper->getThemeName(), $guiTheme, _(ucfirst($guiTheme)))?>
<?endforeach; // $themeHelper set in DefaultPageLayout.php ?>
</select>
_(Used / Free columns)_:
: <select name="text">
<?=mk_option($display['text'], "0",_('Text'))?>
<?=mk_option($display['text'], "1",_('Bar (gray)'))?>
<?=mk_option($display['text'], "2",_('Bar (color)'))?>
<?=mk_option($display['text'], "10",_('Text - Bar (gray)'))?>
<?=mk_option($display['text'], "20",_('Text - Bar (color)'))?>
<?=mk_option($display['text'], "11",_('Bar (gray) - Text'))?>
<?=mk_option($display['text'], "21",_('Bar (color) - Text'))?>
<?=mk_option($display['text'], "0", _('Text'))?>
<?=mk_option($display['text'], "1", _('Bar (gray)'))?>
<?=mk_option($display['text'], "2", _('Bar (color)'))?>
<?=mk_option($display['text'], "10", _('Text - Bar (gray)'))?>
<?=mk_option($display['text'], "20", _('Text - Bar (color)'))?>
<?=mk_option($display['text'], "11", _('Bar (gray) - Text'))?>
<?=mk_option($display['text'], "21", _('Bar (color) - Text'))?>
</select>
_(Header custom text color)_:
@@ -283,14 +282,14 @@ _(Header custom background color)_:
_(Header show description)_:
: <select name="headerdescription">
<?=mk_option($display['headerdescription'], "yes",_('Yes'))?>
<?=mk_option($display['headerdescription'], "no",_('No'))?>
<?=mk_option($display['headerdescription'], "yes", _('Yes'))?>
<?=mk_option($display['headerdescription'], "no", _('No'))?>
</select>
_(Show banner)_:
: <select name="banner" onchange="presetBanner(this.form)">
<?=mk_option($display['banner'], "",_('No'))?>
<?=mk_option($display['banner'], "image",_('Yes'))?>
<?=mk_option($display['banner'], "", _('No'))?>
<?=mk_option($display['banner'], "image", _('Yes'))?>
</select>
<div class="js-bannerSettings" markdown="1" style="display:none">
@@ -310,23 +309,23 @@ _(Custom banner)_:
<div class="js-bannerSettings" markdown="1" style="display:none">
_(Show banner background color fade)_:
: <select name="showBannerGradient">
<?=mk_option($display['showBannerGradient'], "no",_('No'))?>
<?=mk_option($display['showBannerGradient'], "yes",_('Yes'))?>
<?=mk_option($display['showBannerGradient'], "no", _('No'))?>
<?=mk_option($display['showBannerGradient'], "yes", _('Yes'))?>
</select>
</div>
_(Favorites enabled)_:
: <select name="favorites">
<?=mk_option($display['favorites'], "yes",_('Yes'))?>
<?=mk_option($display['favorites'], "no",_('No'))?>
<?=mk_option($display['favorites'], "yes", _('Yes'))?>
<?=mk_option($display['favorites'], "no", _('No'))?>
</select>
:display_favorites_enabled_help:
_(Allow realtime updates on inactive browsers)_:
: <select name='liveUpdate'>
<?=mk_option($display['liveUpdate'],"no",_('No'))?>
<?=mk_option($display['liveUpdate'],"yes",_('Yes'))?>
<?=mk_option($display['liveUpdate'], "no", _('No'))?>
<?=mk_option($display['liveUpdate'], "yes", _('Yes'))?>
</select>
<input type="submit" name="#default" value="_(Default)_" onclick="filename='reset'">

View File

@@ -33,7 +33,7 @@ $(function(){
<input type="hidden" name="#include" value="/webGui/include/update.file.php">
<input type="hidden" name="#file" value="<?=$file;?>">
_(Samba extra configuration)_:
: <textarea spellcheck="false" cols="80" rows="<?=substr_count($text,"\n")+1?>" maxlength="2048" name="text" style="resize:none;font-family:bitstream;width:65.5%" <?if ($var['fsState']=="Started"):?>disabled<?endif;?>><?=htmlspecialchars($text)?></textarea>
: <textarea spellcheck="false" cols="80" rows="<?=substr_count($text,"\n")+1?>" maxlength="2048" name="text" <?if ($var['fsState']=="Started"):?>disabled<?endif;?>><?=htmlspecialchars($text)?></textarea>
&nbsp;
: <input type="submit" value="_(Apply)_" disabled><input type="button" value="_(Done)_" onclick="done()"><?if ($var['fsState']=="Started"):?>*_(Array must be **Stopped** to change)_*<?endif;?>

View File

@@ -46,6 +46,58 @@ function applyCfg() {
$("#applycfg").attr("disabled",true);
});
}
function formatFullInput(input) {
return input
.split(';')
.filter(Boolean) // remove empty trailing entry
.map(entry => {
let [pci, status] = entry.split(',');
status = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
return `${pci} _(${status})_`;
})
.join('<br>');
}
function formatVMS(input) {
return input
.split(';')
.filter(Boolean) // remove empty trailing entry
.join('<br>');
}
function ackPCI(pcidevice, action) {
$.post('/webGui/include/PCIUpdate.php', { action: "getvm", pciid: pcidevice }).done(function(vms) {
let swaltext = "";
switch(action) {
case 'removed':
swaltext = "_(Acknowledge removal of PCI Address)_: " + pcidevice + "<br>_(VMs where PCI device used)_<br>" + formatVMS(vms);
break;
case 'changed':
swaltext = "_(Acknowledge update of PCI Address)_: " + pcidevice + "<br>_(VMs where PCI device used)_<br>"+ formatVMS(vms);
break;
case 'added':
swaltext = "_(Acknowledge addition of PCI Address)_: " + pcidevice + "<br>_(VMs where PCI device used)_<br>" + formatVMS(vms);
break;
case 'all':
swaltext = "_(Acknowledge all PCI Address modifications)_: <br>" + formatFullInput(pcidevice) + "<br>_(VMs where PCI device used)_<br>" + formatVMS(vms);
break;
}
swal({
title: "Are you sure?",
text: swaltext,
type: "warning",
html: true,
showCancelButton: true
}, function() {
$.post('/webGui/include/PCIUpdate.php', { action: action, pciid: pcidevice }).done(function(d) {
if (d === "OK") {
$('#t1').load('/webGui/include/SysDevs.php', { table: 't1' });
}
});
});
});
}
</script>
<?exec('ls /sys/kernel/iommu_groups/',$groups); ?>

View File

@@ -83,7 +83,7 @@ function update_wifi(load) {
var wifi = JSON.parse(text);
$('#connected').html(wifi.active);
$('#my_networks').html(wifi.saved);
if (wifi.other.length) $('#other_networks').html(wifi.other);
if (typeof wifi.other == 'string') $('#other_networks').html(wifi.other);
}
});
timers.wifi = setTimeout(update_wifi,6000);

View File

@@ -163,11 +163,14 @@ $myFile = "case-model.cfg";
$myCase = file_exists("$boot/$myFile") ? file_get_contents("$boot/$myFile") : false;
extract(parse_plugin_cfg('dynamix', true));
$theme_dark = in_array($display['theme'], ['black', 'gray']);
require_once "$docroot/plugins/dynamix/include/ThemeHelper.php";
$themeHelper = new ThemeHelper($display['theme']);
$isDarkTheme = $themeHelper->isDarkTheme();
?>
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="<?= $themeHelper->getThemeHtmlClass() ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
@@ -201,8 +204,8 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
/
/************************/
body {
background: <?=$theme_dark ? '#1C1B1B' : '#F2F2F2'?>;
color: <?=$theme_dark ? '#fff' : '#1c1b1b'?>;
background: <?=$isDarkTheme?'#1C1B1B':'#F2F2F2'?>;
color: <?=$isDarkTheme?'#fff':'#1c1b1b'?>;
font-family: clear-sans, sans-serif;
font-size: .875rem;
padding: 0;
@@ -286,7 +289,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
width: 500px;
margin: 6rem auto;
border-radius: 10px;
background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
background: <?=$isDarkTheme?'#2B2A29':'#fff'?>;
}
#login::after {
content: "";
@@ -378,7 +381,7 @@ $theme_dark = in_array($display['theme'], ['black', 'gray']);
/************************/
@media (max-width: 500px) {
body {
background: <?=$theme_dark ? '#2B2A29' : '#fff'?>;
background: <?=$isDarkTheme?'#2B2A29':'#fff'?>;
}
[type=email], [type=number], [type=password], [type=search], [type=tel], [type=text], [type=url], textarea {
font-size: 16px; /* This prevents the mobile browser from zooming in on the input-field. */

View File

@@ -40,10 +40,12 @@ if (!empty($_POST['password']) && !empty($_POST['confirmPassword'])) {
return $POST_ERROR = $VALIDATION_MESSAGES['saveError'];
}
$THEME_DARK = in_array($display['theme'],['black','gray']);
require_once "$docroot/plugins/dynamix/include/ThemeHelper.php";
$themeHelper = new ThemeHelper($display['theme']);
$isDarkTheme = $themeHelper->isDarkTheme();
?>
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="<?= $themeHelper->getThemeHtmlClass() ?>">
<head>
<meta charset="utf-8">
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
@@ -78,13 +80,13 @@ $THEME_DARK = in_array($display['theme'],['black','gray']);
/
/************************/
:root {
--body-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--body-text-color: <?= $THEME_DARK ? '#fff' : '#1c1b1b' ?>;
--section-bg: <?= $THEME_DARK ? '#1c1b1b' : '#f2f2f2' ?>;
--shadow: <?= $THEME_DARK ? 'rgba(115,115,115,.12)' : 'rgba(0,0,0,.12)' ?>;
--form-text-color: <?= $THEME_DARK ? '#f2f2f2' : '#1c1b1b' ?>;
--form-bg-color: <?= $THEME_DARK ? 'rgba(26,26,26,0.4)' : '#f2f2f2' ?>;
--form-border-color: <?= $THEME_DARK ? '#2B2A29' : '#ccc' ?>;
--body-bg: <?= $isDarkTheme ? '#1c1b1b' : '#f2f2f2' ?>;
--body-text-color: <?= $isDarkTheme ? '#fff' : '#1c1b1b' ?>;
--section-bg: <?= $isDarkTheme ? '#1c1b1b' : '#f2f2f2' ?>;
--shadow: <?= $isDarkTheme ? 'rgba(115,115,115,.12)' : 'rgba(0,0,0,.12)' ?>;
--form-text-color: <?= $isDarkTheme ? '#f2f2f2' : '#1c1b1b' ?>;
--form-bg-color: <?= $isDarkTheme ? 'rgba(26,26,26,0.4)' : '#f2f2f2' ?>;
--form-border-color: <?= $isDarkTheme ? '#2B2A29' : '#ccc' ?>;
}
body {
background: var(--body-bg);

View File

@@ -24,16 +24,12 @@ $var = parse_ini_file("/var/local/emhttp/var.ini");
/**
* Just like DefaultPageLayout.php
*/
$theme = strtok($display['theme'],'-');
$themes1 = in_array($theme,['black','white']);
$themes2 = in_array($theme,['gray','azure']);
$themeHtmlClass = "Theme--$theme";
if ($themes2) {
$themeHtmlClass .= " Theme--sidebar";
}
require_once "$docroot/plugins/dynamix/include/ThemeHelper.php";
$themeHelper = new ThemeHelper($display['theme']);
$themeName = $themeHelper->getThemeName();
?>
<!DOCTYPE HTML>
<html <?=$display['rtl']?>lang="<?=strtok($locale,'_')?:'en'?>" class="<?= $themeHtmlClass ?>">
<html <?=$display['rtl']?>lang="<?=strtok($locale,'_')?:'en'?>" class="<?= $themeHelper->getThemeHtmlClass() ?>">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
@@ -46,7 +42,7 @@ if ($themes2) {
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-color-palette.css")?>">
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/default-base.css")?>">
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/themes/{$display['theme']}.css")?>">
<link type="text/css" rel="stylesheet" href="<?autov("/webGui/styles/themes/{$themeName}.css")?>">
<style>
.boot-title {

View File

@@ -25,18 +25,18 @@ function write(&$rows) {
function validdir($dir) {
$path = realpath($dir);
return in_array(explode('/',$path)[1]??'',['mnt','boot']) ? $path : '';
return in_array(explode('/', $path)[1] ?? '', ['mnt','boot']) ? $path : '';
}
function escapeQuote($data) {
return str_replace('"','&#34;',$data);
return str_replace('"','&#34;', $data);
}
function add($number, $name, $single='', $plural='s') {
return $number.' '._($name.($number==1 ? $single : $plural));
}
function age($number,$time) {
function age($number, $time) {
return sprintf(_('%s '.($number==1 ? $time : $time.'s').' ago'),$number);
}
@@ -44,25 +44,25 @@ function my_age($time) {
if (!is_numeric($time)) $time = time();
$age = new DateTime('@'.$time);
$age = date_create('now')->diff($age);
if ($age->y > 0) return age($age->y,'year');
if ($age->m > 0) return age($age->m,'month');
if ($age->d > 0) return age($age->d,'day');
if ($age->h > 0) return age($age->h,'hour');
if ($age->i > 0) return age($age->i,'minute');
return age($age->s,'second');
if ($age->y > 0) return age($age->y, 'year');
if ($age->m > 0) return age($age->m, 'month');
if ($age->d > 0) return age($age->d, 'day');
if ($age->h > 0) return age($age->h, 'hour');
if ($age->i > 0) return age($age->i, 'minute');
return age($age->s, 'second');
}
function parent_link() {
global $dir,$path;
global $dir, $path;
$parent = dirname($dir);
return $parent=='/' ? false : '<a href="/'.$path.'?dir='.rawurlencode(htmlspecialchars($parent)).'">'._('Parent Directory').'</a>';
return $parent == '/' ? false : '<a href="/'.$path.'?dir='.rawurlencode(htmlspecialchars($parent)).'">'._('Parent Directory').'</a>';
}
function my_devs(&$devs,$name,$menu) {
global $disks,$lock;
global $disks, $lock;
$text = []; $i = 0;
foreach ($devs as $dev) {
if ($lock=='---') {
if ($lock == '---') {
$text[$i] = '<a class="info" onclick="return false"><i class="lock fa fa-fw fa-hdd-o grey-text"></i></a>&nbsp;---';
} else {
switch ($disks[$dev]['luksState']??0) {
@@ -72,7 +72,7 @@ function my_devs(&$devs,$name,$menu) {
case 3: $text[$i] = '<span class="dfm_device"><a class="info" onclick="return false"><i class="lock fa fa-fw fa-lock red-text"></i><span>'._('Locked: wrong encryption key').'</span></a>'; break;
default: $text[$i] = '<span class="dfm_device"><a class="info" onclick="return false"><i class="lock fa fa-fw fa-lock red-text"></i><span>'._('Locked: unknown error').'</span></a>'; break;
}
$root = $dev=='flash' ? "/boot/$name" : "/mnt/$dev/$name";
$root = ($dev == 'flash' ? "/boot/$name" : "/mnt/$dev/$name");
$text[$i] .= '<span id="device_'.$i.'" class="hand" onclick="'.$menu.'(\''.$root.'\','.$i.')" oncontextmenu="'.$menu.'(\''.$root.'\','.$i.');return false">'.compress($dev,11,0).'</span></span>';
}
$i++;
@@ -141,8 +141,8 @@ $path = unscript($_GET['path']);
$fmt = "%F {$display['time']}";
$dirs = $files = [];
$total = $objs = 0;
[$null,$root,$main,$next,$rest] = my_explode('/',$dir,5);
$user = $root=='mnt' && in_array($main,['user','user0']);
[$null,$root,$main,$next,$rest] = my_explode('/', $dir, 5);
$user = $root=='mnt' && in_array($main, ['user','user0']);
$lock = $root=='mnt' ? ($main ?: '---') : ($root=='boot' ? _('flash') : '---');
$ishare = $root=='mnt' && (!$main || !$next || ($main=='rootshare' && !$rest));
$folder = $lock=='---' ? _('DEVICE') : ($ishare ? _('SHARE') : _('FOLDER'));
@@ -154,13 +154,14 @@ if ($user ) {
}
$stat = popen("shopt -s dotglob;stat -L -c'%F|%U|%A|%s|%Y|%n' ".escapeshellarg($dir)."/* 2>/dev/null",'r');
while (($row = fgets($stat))!==false) {
while (($row = fgets($stat)) !== false) {
[$type,$owner,$perm,$size,$time,$name] = explode('|',rtrim($row,"\n"),6);
$dev = explode('/',$name,5);
$devs = explode(',',$user ? $set[basename($name)]??$shares[$dev[3]]['cachePool']??'' : $lock);
$dev = explode('/', $name, 5);
$devs = explode(',', $user ? $set[basename($name)] ?? $shares[$dev[3]]['cachePool'] ?? '' : $lock);
$objs++;
$text = [];
if ($type[0]=='d') {
if ($type[0] == 'd') {
$text[] = '<tr><td><i id="check_'.$objs.'" class="fa fa-fw fa-square-o" onclick="selectOne(this.id)"></i></td>';
$text[] = '<td data=""><i class="fa fa-folder-o"></i></td>';
$text[] = '<td><a id="name_'.$objs.'" oncontextmenu="folderContextMenu(this.id,\'right\');return false" href="/'.$path.'?dir='.rawurlencode(htmlspecialchars($name)).'">'.htmlspecialchars(basename($name)).'</a></td>';
@@ -172,8 +173,8 @@ while (($row = fgets($stat))!==false) {
$text[] = '<td><i id="row_'.$objs.'" data="'.escapeQuote($name).'" type="d" class="fa fa-plus-square-o" onclick="folderContextMenu(this.id,\'both\')" oncontextmenu="folderContextMenu(this.id,\'both\');return false">...</i></td></tr>';
$dirs[] = gzdeflate(implode($text));
} else {
$ext = strtolower(pathinfo($name,PATHINFO_EXTENSION));
$tag = count($devs)>1 ? 'warning' : '';
$ext = strtolower(pathinfo($name, PATHINFO_EXTENSION));
$tag = count($devs) > 1 ? 'warning' : '';
$text[] = '<tr><td><i id="check_'.$objs.'" class="fa fa-fw fa-square-o" onclick="selectOne(this.id)"></i></td>';
$text[] = '<td class="ext" data="'.$ext.'"><i class="'.icon_class($ext).'"></i></td>';
$text[] = '<td id="name_'.$objs.'" class="'.$tag.'" onclick="fileEdit(this.id)" oncontextmenu="fileContextMenu(this.id,\'right\');return false">'.htmlspecialchars(basename($name)).'</td>';
@@ -189,6 +190,7 @@ while (($row = fgets($stat))!==false) {
}
pclose($stat);
if ($link = parent_link()) echo '<tbody class="tablesorter-infoOnly"><tr><td></td><td><i class="fa fa-folder-open-o"></i></td><td>',$link,'</td><td colspan="6"></td></tr></tbody>';
echo write($dirs),write($files),'<tfoot><tr><td></td><td></td><td colspan="7">',add($objs,'object'),': ',add($dirs,'director','y','ies'),', ',add($files,'file'),' (',my_scale($total,$unit),' ',$unit,' ',_('total'),')</td></tr></tfoot>';
?>

View File

@@ -115,6 +115,7 @@ if ($_POST['vms']) {
}
echo "<tr title='' class='updated'><td>";
$running = 0;
$pci_device_changes = comparePCIData();
foreach ($vms as $vm) {
$res = $lv->get_domain_by_name($vm);
$uuid = libvirt_domain_get_uuid_string($res);
@@ -155,8 +156,15 @@ if ($_POST['vms']) {
$log = (is_file("/var/log/libvirt/qemu/$vm.log") ? "libvirt/qemu/$vm.log" : '');
if (!isset($domain_cfg["CONSOLE"])) $vmrcconsole = "web"; else $vmrcconsole = $domain_cfg["CONSOLE"];
if (!isset($domain_cfg["RDPOPT"])) $vmrcconsole .= ";no"; else $vmrcconsole .= ";".$domain_cfg["RDPOPT"];
$WebUI = html_entity_decode($arrConfig["template"]["webui"]);
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm), addslashes($uuid), addslashes($template), $state, addslashes($vmrcurl), strtoupper($vmrcprotocol), addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)));
$WebUI = html_entity_decode($arrConfig["template"]["webui"]??"");
$vmpciids = $lv->domain_get_vm_pciids($vm);
$pcierror = false;
foreach($vmpciids as $pciid => $pcidetail) {
if (isset($pci_device_changes["0000:".$pciid])) {
$pcierror = true;
}
}
$menu = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm), addslashes($uuid), addslashes($template), $state, addslashes($vmrcurl), strtoupper($vmrcprotocol), addslashes($log),addslashes($fstype), $vmrcconsole,false,addslashes(str_replace('"',"'",$WebUI)),$pcierror);
$icon = $lv->domain_get_icon_url($res);
switch ($state) {
case 'running':
@@ -178,7 +186,9 @@ if ($_POST['vms']) {
break;
}
$image = substr($icon,-4)=='.png' ? "<img src='$icon' class='img'>" : (substr($icon,0,5)=='icon-' ? "<i class='$icon img'></i>" : "<i class='fa fa-$icon img'></i>");
echo "<span class='outer solid vms $status'><span id='vm-$uuid' $menu class='hand'>$image</span><span class='inner'>$vm<br><i class='fa fa-$shape $status $color'></i><span class='state'>"._($status)."</span></span></span>";
echo "<span class='outer solid vms $status'><span id='vm-$uuid' $menu class='hand'>$image</span><span class='inner'>$vm";
if ($pcierror) echo "<i class=\"fa fa-warning fa-fw orange-text\" title=\""._('PCI Changed')."\n"._('Start disabled')."\"></i>";
echo "<br><i class='fa fa-$shape $status $color'></i><span class='state'>"._($status)."</span></span></span>";
if ($state == "running") {
#Build VM Usage array.
$menuusage = sprintf("onclick=\"addVMContext('%s','%s','%s','%s','%s','%s','%s','%s','%s','%s','%s')\"", addslashes($vm), addslashes($uuid), addslashes($template), $state, addslashes($vmrcurl), strtoupper($vmrcprotocol), addslashes($log),addslashes($fstype), $vmrcconsole,true,addslashes(str_replace('"',"'",$WebUI)));

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,485 @@
<?php
/**
* This code was originally in DefaultPageLayout.php
* It has been moved here to help make DefaultPageLayout.php more maintainable.
* Please reference DefaultPageLayout.php for any historical git blame information.
*/
?>
<script>
function parseINI(msg) {
var regex = {
section: /^\s*\[\s*\"*([^\]]*)\s*\"*\]\s*$/,
param: /^\s*([^=]+?)\s*=\s*\"*(.*?)\s*\"*$/,
comment: /^\s*;.*$/
};
var value = {};
var lines = msg.split(/[\r\n]+/);
var section = null;
lines.forEach(function(line) {
if (regex.comment.test(line)) {
return;
} else if (regex.param.test(line)) {
var match = line.match(regex.param);
if (section) {
value[section][match[1]] = match[2];
} else {
value[match[1]] = match[2];
}
} else if (regex.section.test(line)) {
var match = line.match(regex.section);
value[match[1]] = {};
section = match[1];
} else if (line.length==0 && section) {
section = null;
};
});
return value;
}
// unraid animated logo
var unraid_logo = '<?readfile("$docroot/webGui/images/animated-logo.svg")?>';
var defaultPage = new NchanSubscriber('/sub/session,var<?=$entity?",notify":""?>',{subscriber:'websocket', reconnectTimeout:5000});
defaultPage.on('message', function(msg,meta) {
switch (meta.id.channel()) {
case 0:
// stale session, force login
if (csrf_token != msg) location.replace('/');
break;
case 1:
// message field in footer
var ini = parseINI(msg);
switch (ini['fsState']) {
case 'Stopped' : var status = "<span class='red strong'><i class='fa fa-stop-circle'></i> <?=_('Array Stopped')?></span>"; break;
case 'Started' : var status = "<span class='green strong'><i class='fa fa-play-circle'></i> <?=_('Array Started')?></span>"; break;
case 'Formatting': var status = "<span class='green strong'><i class='fa fa-play-circle'></i> <?=_('Array Started')?></span>&bullet;<span class='orange strong tour'><?=_('Formatting device(s)')?></span>"; break;
default : var status = "<span class='orange strong'><i class='fa fa-pause-circle'></i> "+_('Array '+ini['fsState'])+"</span>";
}
if (ini['mdResyncPos'] > 0) {
var resync = ini['mdResyncAction'].split(/\s+/);
switch (resync[0]) {
case 'recon': var action = resync[1]=='P' ? "<?=_('Parity-Sync')?>" : "<?=_('Data-Rebuild')?>"; break;
case 'check': var action = resync.length>1 ? "<?=_('Parity-Check')?>" : "<?=_('Read-Check')?>"; break;
case 'clear': var action = "<?=_('Disk-Clear')?>"; break;
default : var action = '';
}
action += " "+(ini['mdResyncPos']/(ini['mdResyncSize']/100+1)).toFixed(1)+" %";
status += "&bullet;<span class='orange strong tour'>"+action.replace('.','<?=_var($display,'number','.,')[0]?>');
if (ini['mdResyncDt']==0) status += " &bullet; <?=_('Paused')?>";
status += "</span>";
}
if (ini['fsProgress']) status += "&bullet;<span class='blue strong tour'>"+_(ini['fsProgress'])+"</span>";
$('#statusbar').html(status);
break;
case 2:
// notifications
var bell1 = 0, bell2 = 0, bell3 = 0;
$.each($.parseJSON(msg), function(i, notify){
switch (notify.importance) {
case 'alert' : bell1++; break;
case 'warning': bell2++; break;
case 'normal' : bell3++; break;
}
<?if ($notify['display']==0):?>
if (notify.show) {
$.jGrowl(notify.subject+'<br>'+notify.description,{
group: notify.importance,
header: notify.event+': '+notify.timestamp,
theme: notify.file,
beforeOpen: function(e,m,o){if ($('div.jGrowl-notification').hasClass(notify.file)) return(false);},
afterOpen: function(e,m,o){if (notify.link) $(e).css('cursor','pointer');},
click: function(e,m,o){if (notify.link) location.replace(notify.link);},
close: function(e,m,o){$.post('/webGui/include/Notify.php',{cmd:'hide',file:"<?=$notify['path'].'/unread/'?>"+notify.file,csrf_token:csrf_token}<?if ($notify['life']==0):?>,function(){$.post('/webGui/include/Notify.php',{cmd:'archive',file:notify.file,csrf_token:csrf_token});}<?endif;?>);}
});
}
<?endif;?>
});
$('#bell').removeClass('red-orb yellow-orb green-orb').prop('title',"<?=_('Alerts')?> ["+bell1+']\n'+"<?=_('Warnings')?> ["+bell2+']\n'+"<?=_('Notices')?> ["+bell3+']');
if (bell1) $('#bell').addClass('red-orb'); else
if (bell2) $('#bell').addClass('yellow-orb'); else
if (bell3) $('#bell').addClass('green-orb');
break;
}
});
<?if ($wlan0):?>
function wlanSettings() {
$.cookie('one','tab<?=count(glob("$docroot/webGui/Eth*.page"))?>');
window.location = '/Settings/NetworkSettings';
}
var nchan_wlan0 = new NchanSubscriber('/sub/wlan0',{subscriber:'websocket', reconnectTimeout:5000});
nchan_wlan0.on('message', function(msg) {
var wlan = JSON.parse(msg);
$('#wlan0').removeClass().addClass(wlan.color).attr('title',wlan.title);
});
nchan_wlan0.start();
<?endif;?>
var nchan_plugins = new NchanSubscriber('/sub/plugins',{subscriber:'websocket', reconnectTimeout:5000});
nchan_plugins.on('message', function(data) {
if (!data || openDone(data)) return;
var box = $('pre#swaltext');
const text = box.html().split('<br>');
if (data.slice(-1) == '\r') {
text[text.length-1] = data.slice(0,-1);
} else {
text.push(data.slice(0,-1));
}
box.html(text.join('<br>')).scrollTop(box[0].scrollHeight);
});
var nchan_docker = new NchanSubscriber('/sub/docker',{subscriber:'websocket', reconnectTimeout:5000});
nchan_docker.on('message', function(data) {
if (!data || openDone(data)) return;
var box = $('pre#swaltext');
data = data.split('\0');
switch (data[0]) {
case 'addLog':
var rows = document.getElementsByClassName('logLine');
if (rows.length) {
var row = rows[rows.length-1];
row.innerHTML += data[1]+'<br>';
}
break;
case 'progress':
var rows = document.getElementsByClassName('progress-'+data[1]);
if (rows.length) {
rows[rows.length-1].textContent = data[2];
}
break;
case 'addToID':
var rows = document.getElementById(data[1]);
if (rows === null) {
rows = document.getElementsByClassName('logLine');
if (rows.length) {
var row = rows[rows.length-1];
row.innerHTML += '<span id="'+data[1]+'">IMAGE ID ['+data[1]+']: <span class="content">'+data[2]+'</span><span class="progress-'+data[1]+'"></span>.</span><br>';
}
} else {
var rows_content = rows.getElementsByClassName('content');
if (!rows_content.length || rows_content[rows_content.length-1].textContent != data[2]) {
rows.innerHTML += '<span class="content">'+data[2]+'</span><span class="progress-'+data[1]+'"></span>.';
}
}
break;
case 'show_Wait':
progress_span[data[1]] = document.getElementById('wait-'+data[1]);
progress_dots[data[1]] = setInterval(function(){if (((progress_span[data[1]].innerHTML += '.').match(/\./g)||[]).length > 9) progress_span[data[1]].innerHTML = progress_span[data[1]].innerHTML.replace(/\.+$/,'');},500);
break;
case 'stop_Wait':
clearInterval(progress_dots[data[1]]);
progress_span[data[1]].innerHTML = '';
break;
default:
box.html(box.html()+data[0]);
break;
}
box.scrollTop(box[0].scrollHeight);
});
var nchan_vmaction = new NchanSubscriber('/sub/vmaction',{subscriber:'websocket', reconnectTimeout:5000});
nchan_vmaction.on('message', function(data) {
if (!data || openDone(data) || openError(data)) return;
var box = $('pre#swaltext');
data = data.split('\0');
switch (data[0]) {
case 'addLog':
var rows = document.getElementsByClassName('logLine');
if (rows.length) {
var row = rows[rows.length-1];
row.innerHTML += data[1]+'<br>';
}
break;
case 'progress':
var rows = document.getElementsByClassName('progress-'+data[1]);
if (rows.length) {
rows[rows.length-1].textContent = data[2];
}
break;
case 'addToID':
var rows = document.getElementById(data[1]);
if (rows === null) {
rows = document.getElementsByClassName('logLine');
if (rows.length) {
var row = rows[rows.length-1];
row.innerHTML += '<span id="'+data[1]+'">'+data[1]+': <span class="content">'+data[2]+'</span><span class="progress-'+data[1]+'"></span>.</span><br>';
}
} else {
var rows_content = rows.getElementsByClassName('content');
if (!rows_content.length || rows_content[rows_content.length-1].textContent != data[2]) {
rows.innerHTML += '<span class="content">'+data[2]+'</span><span class="progress-'+data[1]+'"></span>.';
}
}
break;
case 'show_Wait':
progress_span[data[1]] = document.getElementById('wait-'+data[1]);
progress_dots[data[1]] = setInterval(function(){if (((progress_span[data[1]].innerHTML += '.').match(/\./g)||[]).length > 9) progress_span[data[1]].innerHTML = progress_span[data[1]].innerHTML.replace(/\.+$/,'');},500);
break;
case 'stop_Wait':
clearInterval(progress_dots[data[1]]);
progress_span[data[1]].innerHTML = '';
break;
default:
box.html(box.html()+data[0]);
break;
}
box.scrollTop(box[0].scrollHeight);
});
const scrollDuration = 500;
$(window).scroll(function() {
if ($(this).scrollTop() > 0) {
$('.back_to_top').fadeIn(scrollDuration);
} else {
$('.back_to_top').fadeOut(scrollDuration);
}
<?if ($themeHelper->isTopNavTheme()):?>
var top = $('div#header').height()-1; // header height has 1 extra pixel to cover overlap
$('div#menu').css($(this).scrollTop() > top ? {position:'fixed',top:'0'} : {position:'absolute',top:top+'px'});
// banner
$('div.upgrade_notice').css($(this).scrollTop() > 24 ? {position:'fixed',top:'0'} : {position:'absolute',top:'24px'});
<?endif;?>
});
$('.move_to_end').click(function(event) {
event.preventDefault();
$('html,body').animate({scrollTop:$(document).height()},scrollDuration);
return false;
});
$('.back_to_top').click(function(event) {
event.preventDefault();
$('html,body').animate({scrollTop:0},scrollDuration);
return false;
});
<?if ($entity):?>
$.post('/webGui/include/Notify.php',{cmd:'init',csrf_token:csrf_token});
<?endif;?>
$(function() {
defaultPage.start();
$('div.spinner.fixed').html(unraid_logo);
setTimeout(function(){$('div.spinner').not('.fixed').each(function(){$(this).html(unraid_logo);});},500); // display animation if page loading takes longer than 0.5s
shortcut.add('F1',function(){HelpButton();});
<?if (_var($var,'regTy')=='unregistered'):?>
$('#licensetype').addClass('orange-text');
<?elseif (!in_array(_var($var,'regTy'),['Trial','Basic','Plus','Pro'])):?>
$('#licensetype').addClass('red-text');
<?endif;?>
$('input[value="<?=_("Apply")?>"],input[value="Apply"],input[name="cmdEditShare"],input[name="cmdUserEdit"]').prop('disabled',true);
$('form').find('select,input[type=text],input[type=number],input[type=password],input[type=checkbox],input[type=radio],input[type=file],textarea').not('.lock').each(function(){$(this).on('input change',function() {
var form = $(this).parentsUntil('form').parent();
form.find('input[value="<?=_("Apply")?>"],input[value="Apply"],input[name="cmdEditShare"],input[name="cmdUserEdit"]').not('input.lock').prop('disabled',false);
form.find('input[value="<?=_("Done")?>"],input[value="Done"]').not('input.lock').val("<?=_('Reset')?>").prop('onclick',null).off('click').click(function(){formHasUnsavedChanges=false;refresh(form.offset().top);});
});});
// add leave confirmation when form has changed without applying (opt-in function)
if ($('form.js-confirm-leave').length>0) {
$('form.js-confirm-leave').on('change',function(e){formHasUnsavedChanges=true;}).on('submit',function(e){formHasUnsavedChanges=false;});
$(window).on('beforeunload',function(e){if (formHasUnsavedChanges) return '';}); // note: the browser creates its own popup window and warning message
}
// form parser: add escapeQuotes protection
$('form').each(function(){
var action = $(this).prop('action').actionName();
if (action=='update.htm' || action=='update.php') {
var onsubmit = $(this).attr('onsubmit')||'';
$(this).attr('onsubmit','clearTimeout(timers.flashReport);escapeQuotes(this);'+onsubmit);
}
});
var top = ($.cookie('top')||0) - $('.tabs').offset().top - 75;
if (top>0) {$('html,body').scrollTop(top);}
$.removeCookie('top');
if ($.cookie('addAlert') != null) bannerAlert(addAlert.text,addAlert.cmd,addAlert.plg,addAlert.func);
<?if ($safemode):?>
showNotice("<?=_('System running in')?> <b><?=('safe mode')?></b>");
<?else:?>
<?if (!_var($notify,'system')):?>
addBannerWarning("<?=_('System notifications are')?> <b><?=_('disabled')?></b>. <?=_('Click')?> <a href='/Settings/Notifications'><?=_('here')?></a> <?=_('to change notification settings')?>.",true,true);
<?endif;?>
<?endif;?>
var opts = [];
context.settings({above:false});
opts.push({header:"<?=_('Notifications')?>"});
opts.push({text:"<?=_('View')?>",icon:'fa-folder-open-o',action:function(e){e.preventDefault();openNotifier();}});
opts.push({text:"<?=_('History')?>",icon:'fa-file-text-o',action:function(e){e.preventDefault();viewHistory();}});
opts.push({text:"<?=_('Acknowledge')?>",icon:'fa-check-square-o',action:function(e){e.preventDefault();closeNotifier();}});
context.attach('#board',opts);
if (location.pathname.search(/\/(AddVM|UpdateVM|AddContainer|UpdateContainer)/)==-1) {
$('blockquote.inline_help').each(function(i) {
$(this).attr('id','helpinfo'+i);
var pin = $(this).prev();
if (!pin.prop('nodeName')) pin = $(this).parent().prev();
while (pin.prop('nodeName') && pin.prop('nodeName').search(/(table|dl)/i)==-1) pin = pin.prev();
pin.find('tr:first,dt:last').each(function() {
var node = $(this);
var name = node.prop('nodeName').toLowerCase();
if (name=='dt') {
while (!node.html() || node.html().search(/(<input|<select|nbsp;)/i)>=0 || name!='dt') {
if (name=='dt' && node.is(':first-of-type')) break;
node = node.prev();
name = node.prop('nodeName').toLowerCase();
}
node.css('cursor','help').click(function(){$('#helpinfo'+i).toggle('slow');});
} else {
if (node.html() && (name!='tr' || node.children('td:first').html())) node.css('cursor','help').click(function(){$('#helpinfo'+i).toggle('slow');});
}
});
});
}
$('form').append($('<input>').attr({type:'hidden', name:'csrf_token', value:csrf_token}));
setInterval(function(){if ($(document).height() > $(window).height()) $('.move_to_end').fadeIn(scrollDuration); else $('.move_to_end').fadeOut(scrollDuration);},250);
});
var gui_pages_available = [];
<?
$gui_pages = glob("/usr/local/emhttp/plugins/*/*.page");
array_walk($gui_pages,function($value,$key){ ?>
gui_pages_available.push('<?=basename($value,".page")?>'); <?
});
?>
function isValidURL(url) {
try {
var ret = new URL(url);
return ret;
} catch (err) {
return false;
}
}
$('body').on('click','a,.ca_href', function(e) {
if ($(this).hasClass('ca_href')) {
var ca_href = true;
var href=$(this).attr('data-href');
var target=$(this).attr('data-target');
} else {
var ca_href = false;
var href = $(this).attr('href');
var target = $(this).attr('target');
}
if (href) {
href = href.trim();
// Sanitize href to prevent XSS
href = href.replace(/[<>"]/g, '');
if (href.match('https?://[^\.]*.(my)?unraid.net/') || href.indexOf('https://unraid.net/') == 0 || href == 'https://unraid.net' || href.indexOf('http://lime-technology.com') == 0) {
if (ca_href) window.open(href,target);
return;
}
if (href !== '#' && href.indexOf('javascript') !== 0) {
var dom = isValidURL(href);
if (dom == false) {
if (href.indexOf('/') == 0) return; // all internal links start with "/"
var baseURLpage = href.split('/');
if (gui_pages_available.includes(baseURLpage[0])) return;
}
if ($(this).hasClass('localURL')) return;
try {
var domainsAllowed = JSON.parse($.cookie('allowedDomains'));
} catch(e) {
var domainsAllowed = new Object();
}
$.cookie('allowedDomains',JSON.stringify(domainsAllowed),{expires:3650}); // rewrite cookie to further extend expiration by 400 days
if (domainsAllowed[dom.hostname]) return;
e.preventDefault();
swal({
title: "<?=_('External Link')?>",
text: "<span title='"+href+"'><?=_('Clicking OK will take you to a 3rd party website not associated with Lime Technology')?><br><br><b>"+href+"<br><br><input id='Link_Always_Allow' type='checkbox'></input><?=_('Always Allow')?> "+dom.hostname+"</span>",
html: true,
animation: 'none',
type: 'warning',
showCancelButton: true,
showConfirmButton: true,
cancelButtonText: "<?=_('Cancel')?>",
confirmButtonText: "<?=_('OK')?>"
},function(isConfirm) {
if (isConfirm) {
if ($('#Link_Always_Allow').is(':checked')) {
domainsAllowed[dom.hostname] = true;
$.cookie('allowedDomains',JSON.stringify(domainsAllowed),{expires:3650});
}
var popupOpen = window.open(href,target);
if (!popupOpen || popupOpen.closed || typeof popupOpen == 'undefined') {
var popupWarning = addBannerWarning("<?=_('Popup Blocked');?>");
setTimeout(function(){removeBannerWarning(popupWarning);},10000);
}
}
});
}
}
});
// Only include window focus/blur event handlers when live updates are disabled
// to prevent unnecessary page reloads when live updates are already handling data refreshes
// nchanPaused / blurTimer used elsewhere so need to always be defined
var nchanPaused = false;
var blurTimer = false;
<? if ( $display['liveUpdate'] == "no" ):?>
$(window).focus(function() {
nchanFocusStart();
});
// Stop nchan on loss of focus
$(window).blur(function() {
blurTimer = setTimeout(function(){
nchanFocusStop();
},30000);
});
document.addEventListener("visibilitychange", (event) => {
if (document.hidden) {
nchanFocusStop();
} else {
<? if (isset($myPage['Load']) && $myPage['Load'] > 0):?>
if ( dialogOpen() ) {
clearInterval(timers.reload);
setTimerReload();
nchanFocusStart();
} else {
window.location.reload();
}
<?else:?>
nchanFocusStart();
<?endif;?>
}
});
function nchanFocusStart() {
if ( blurTimer !== false ) {
clearTimeout(blurTimer);
blurTimer = false;
}
if (nchanPaused !== false ) {
removeBannerWarning(nchanPaused);
nchanPaused = false;
try {
pageFocusFunction();
} catch(error) {}
subscribers.forEach(function(e) {
e.start();
});
}
}
function nchanFocusStop(banner=true) {
if ( subscribers.length ) {
if ( nchanPaused === false ) {
var newsub = subscribers;
subscribers.forEach(function(e) {
try {
e.stop();
} catch(err) {
newsub.splice(newsub.indexOf(e,1));
}
});
subscribers = newsub;
if ( banner && subscribers.length ) {
nchanPaused = addBannerWarning("<?=_('Live Updates Paused');?>",false,true );
}
}
}
}
<?endif;?>
</script>

View File

@@ -0,0 +1,607 @@
<?php
/**
* This code was originally in DefaultPageLayout.php
* It has been moved here to help make DefaultPageLayout.php more maintainable.
* Please reference DefaultPageLayout.php for any historical git blame information.
*/
?>
<script>
String.prototype.actionName = function(){return this.split(/[\\/]/g).pop();}
String.prototype.channel = function(){return this.split(':')[1].split(',').findIndex((e)=>/\[\d\]/.test(e));}
NchanSubscriber.prototype.monitor = function(){subscribers.push(this);}
Shadowbox.init({skipSetup:true});
context.init();
// list of nchan subscribers to start/stop at focus change
var subscribers = [];
// server uptime
var uptime = <?=strtok(exec("cat /proc/uptime"),' ')?>;
var expiretime = <?=_var($var,'regTy')=='Trial'||strstr(_var($var,'regTy'),'expired')?_var($var,'regTm2'):0?>;
var before = new Date();
// page timer events
const timers = {};
timers.bannerWarning = null;
// tty window
var tty_window = null;
const addAlert = {};
addAlert.text = $.cookie('addAlert-text');
addAlert.cmd = $.cookie('addAlert-cmd');
addAlert.plg = $.cookie('addAlert-plg');
addAlert.func = $.cookie('addAlert-func');
// current csrf_token
var csrf_token = "<?=_var($var,'csrf_token')?>";
// form has unsaved changes indicator
var formHasUnsavedChanges = false;
// docker progess indicators
var progress_dots = [], progress_span = [];
function pauseEvents(id) {
$.each(timers, function(i,timer){
if (!id || i==id) clearTimeout(timer);
});
}
function resumeEvents(id,delay) {
var startDelay = delay||50;
$.each(timers, function(i,timer) {
if (!id || i==id) timers[i] = setTimeout(i+'()', startDelay);
startDelay += 50;
});
}
function plus(value,single,plural,last) {
return value>0 ? (value+' '+(value==1?single:plural)+(last?'':', ')) : '';
}
function updateTime() {
var now = new Date();
var days = parseInt(uptime/86400);
var hour = parseInt(uptime/3600%24);
var mins = parseInt(uptime/60%60);
$('span.uptime').html(((days|hour|mins)?plus(days,"<?=_('day')?>","<?=_('days')?>",(hour|mins)==0)+plus(hour,"<?=_('hour')?>","<?=_('hours')?>",mins==0)+plus(mins,"<?=_('minute')?>","<?=_('minutes')?>",true):"<?=_('less than a minute')?>"));
uptime += Math.round((now.getTime() - before.getTime())/1000);
before = now;
if (expiretime > 0) {
var remainingtime = expiretime - now.getTime()/1000;
if (remainingtime > 0) {
days = parseInt(remainingtime/86400);
hour = parseInt(remainingtime/3600%24);
mins = parseInt(remainingtime/60%60);
if (days) {
$('#licenseexpire').html(plus(days,"<?=_('day')?>","<?=_('days')?>",true)+" <?=_('remaining')?>");
} else if (hour) {
$('#licenseexpire').html(plus(hour,"<?=_('hour')?>","<?=_('hours')?>",true)+" <?=_('remaining')?>").addClass('orange-text');
} else if (mins) {
$('#licenseexpire').html(plus(mins,"<?=_('minute')?>","<?=_('minutes')?>",true)+" <?=_('remaining')?>").addClass('red-text');
} else {
$('#licenseexpire').html("<?=_('less than a minute remaining')?>").addClass('red-text');
}
} else {
$('#licenseexpire').addClass('red-text');
}
}
setTimeout(updateTime,1000);
}
function refresh(top) {
if (typeof top === 'undefined') {
for (var i=0,element; element=document.querySelectorAll('input,button,select')[i]; i++) {element.disabled = true;}
for (var i=0,link; link=document.getElementsByTagName('a')[i]; i++) { link.style.color = "gray"; } //fake disable
location.reload();
} else {
$.cookie('top',top);
location.reload();
}
}
function initab(page) {
$.removeCookie('one');
$.removeCookie('tab');
if (page != null) location.replace(page);
}
function settab(tab) {
<?switch ($myPage['name']):?>
<?case'Main':?>
$.cookie('tab',tab);
<?if (_var($var,'fsState')=='Started'):?>
$.cookie('one','tab1');
<?endif;?>
<?break;?>
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
$.cookie('one',tab);
<?break;?>
<?default:?>
$.cookie('one',tab);
<?endswitch;?>
}
function done(key) {
var url = location.pathname.split('/');
var path = '/'+url[1];
if (key) for (var i=2; i<url.length; i++) if (url[i]==key) break; else path += '/'+url[i];
$.removeCookie('one');
location.replace(path);
}
function chkDelete(form, button) {
button.value = form.confirmDelete.checked ? "<?=_('Delete')?>" : "<?=_('Apply')?>";
button.disabled = false;
}
function makeWindow(name,height,width) {
var top = (screen.height-height)/2;
if (top < 0) {top = 0; height = screen.availHeight;}
var left = (screen.width-width)/2;
if (left < 0) {left = 0; width = screen.availWidth;}
return window.open('',name,'resizeable=yes,scrollbars=yes,height='+height+',width='+width+',top='+top+',left='+left);
}
function openBox(cmd,title,height,width,load,func,id) {
// open shadowbox window (run in foreground)
// included for legacy purposes, replaced by openPlugin
var uri = cmd.split('?');
var run = uri[0].substr(-4)=='.php' ? cmd+(uri[1]?'&':'?')+'done=<?=urlencode(_("Done"))?>' : '/logging.htm?cmd='+cmd+'&csrf_token='+csrf_token+'&done=<?=urlencode(_("Done"))?>';
var options = load ? (func ? {modal:true,onClose:function(){setTimeout(func+'('+'"'+(id||'')+'")');}} : {modal:true,onClose:function(){location.reload();}}) : {modal:false};
Shadowbox.open({content:run, player:'iframe', title:title, height:Math.min(screen.availHeight,800), width:Math.min(screen.availWidth,1200), options:options});
}
function openWindow(cmd,title,height,width) {
// open regular window (run in background)
// included for legacy purposes, replaced by openTerminal
var window_name = title.replace(/ /g,"_");
var form_html = '<form action="/logging.htm" method="post" target="'+window_name+'">'+'<input type="hidden" name="csrf_token" value="'+csrf_token+'">'+'<input type="hidden" name="title" value="'+title+'">';
var vars = cmd.split('&');
form_html += '<input type="hidden" name="cmd" value="'+vars[0]+'">';
for (var i = 1; i < vars.length; i++) {
var pair = vars[i].split('=');
form_html += '<input type="hidden" name="'+pair[0]+'" value="'+pair[1]+'">';
}
form_html += '</form>';
var form = $(form_html);
$('body').append(form);
makeWindow(window_name,height,width);
form.submit();
}
function openTerminal(tag,name,more) {
if (/MSIE|Edge/.test(navigator.userAgent)) {
swal({title:"_(Unsupported Feature)_",text:"_(Sorry, this feature is not supported by MSIE/Edge)_.<br>_(Please try a different browser)_",type:'error',html:true,animation:'none',confirmButtonText:"_(Ok)_"});
return;
}
// open terminal window (run in background)
name = name.replace(/[ #]/g,"_");
tty_window = makeWindow(name+(more=='.log'?more:''),Math.min(screen.availHeight,800),Math.min(screen.availWidth,1200));
var socket = ['ttyd','syslog'].includes(tag) ? '/webterminal/'+tag+'/' : '/logterminal/'+name+(more=='.log'?more:'')+'/';
$.get('/webGui/include/OpenTerminal.php',{tag:tag,name:name,more:more},function(){setTimeout(function(){tty_window.location=socket; tty_window.focus();},200);});
}
function bannerAlert(text,cmd,plg,func,start) {
$.post('/webGui/include/StartCommand.php',{cmd:cmd,pid:1},function(pid) {
if (pid == 0) {
if ($(".upgrade_notice").hasClass('done') || timers.bannerAlert == null) {
forcedBanner = false;
if ($.cookie('addAlert') != null) {
removeBannerWarning($.cookie('addAlert'));
$.removeCookie('addAlert');
}
$(".upgrade_notice").removeClass('alert done');
timers.callback = null;
if (plg != null) {
if ($.cookie('addAlert-page') == null || $.cookie('addAlert-page') == '<?=$task?>') {
setTimeout((func||'loadlist')+'("'+plg+'")',250);
} else if ('Plugins' == '<?=$task?>') {
setTimeout(refresh);
}
}
$.removeCookie('addAlert-page');
} else {
$(".upgrade_notice").removeClass('alert').addClass('done');
timers.bannerAlert = null;
setTimeout(function(){bannerAlert(text,cmd,plg,func,start);},1000);
}
} else {
$.cookie('addAlert',addBannerWarning(text,true,true,true));
$.cookie('addAlert-text',text);
$.cookie('addAlert-cmd',cmd);
$.cookie('addAlert-plg',plg);
$.cookie('addAlert-func',func);
if ($.cookie('addAlert-page') == null) $.cookie('addAlert-page','<?=$task?>');
timers.bannerAlert = setTimeout(function(){bannerAlert(text,cmd,plg,func,start);},1000);
if (start==1 && timers.callback==null && plg!=null) timers.callback = setTimeout((func||'loadlist')+'("'+plg+'")',250);
}
});
}
function openPlugin(cmd,title,plg,func,start=0,button=0) {
// start = 0 : run command only when not already running (default)
// start = 1 : run command unconditionally
// button = 0 : show CLOSE button (default)
// button = 1 : hide CLOSE button
nchan_plugins.start();
$.post('/webGui/include/StartCommand.php',{cmd:cmd+' nchan',start:start},function(pid) {
if (pid==0) {
nchan_plugins.stop();
$('div.spinner.fixed').hide();
$(".upgrade_notice").addClass('alert');
return;
}
swal({title:title,text:"<pre id='swaltext'></pre><hr>",html:true,animation:'none',showConfirmButton:button==0,confirmButtonText:"<?=_('Close')?>"},function(close){
nchan_plugins.stop();
$('div.spinner.fixed').hide();
$('.sweet-alert').hide('fast').removeClass('nchan');
setTimeout(function(){bannerAlert("<?=_('Attention - operation continues in background')?> ["+pid.toString().padStart(8,'0')+"]<i class='fa fa-bomb fa-fw abortOps' title=\"<?=_('Abort background process')?>\" onclick='abortOperation("+pid+")'></i>",cmd,plg,func,start);});
});
$('.sweet-alert').addClass('nchan');
$('button.confirm').prop('disabled',button!=0);
});
}
function openDocker(cmd,title,plg,func,start=0,button=0) {
// start = 0 : run command only when not already running (default)
// start = 1 : run command unconditionally
// button = 0 : hide CLOSE button (default)
// button = 1 : show CLOSE button
nchan_docker.start();
$.post('/webGui/include/StartCommand.php',{cmd:cmd,start:start},function(pid) {
if (pid==0) {
nchan_docker.stop();
$('div.spinner.fixed').hide();
$(".upgrade_notice").addClass('alert');
return;
}
swal({title:title,text:"<pre id='swaltext'></pre><hr>",html:true,animation:'none',showConfirmButton:button!=0,confirmButtonText:"<?=_('Close')?>"},function(close){
nchan_docker.stop();
$('div.spinner.fixed').hide();
$('.sweet-alert').hide('fast').removeClass('nchan');
setTimeout(function(){bannerAlert("<?=_('Attention - operation continues in background')?> ["+pid.toString().padStart(8,'0')+"]<i class='fa fa-bomb fa-fw abortOps' title=\"<?=_('Abort background process')?>\" onclick='abortOperation("+pid+")'></i>",cmd,plg,func,start);});
});
$('.sweet-alert').addClass('nchan');
$('button.confirm').prop('disabled',button==0);
});
}
function openVMAction(cmd,title,plg,func,start=0,button=0) {
// start = 0 : run command only when not already running (default)
// start = 1 : run command unconditionally
// button = 0 : hide CLOSE button (default)
// button = 1 : show CLOSE button
nchan_vmaction.start();
$.post('/webGui/include/StartCommand.php',{cmd:cmd,start:start},function(pid) {
if (pid==0) {
nchan_vmaction.stop();
$('div.spinner.fixed').hide();
$(".upgrade_notice").addClass('alert');
return;
}
swal({title:title,text:"<pre id='swaltext'></pre><hr>",html:true,animation:'none',showConfirmButton:button!=0,confirmButtonText:"<?=_('Close')?>"},function(close){
nchan_vmaction.stop();
$('div.spinner.fixed').hide();
$('.sweet-alert').hide('fast').removeClass('nchan');
setTimeout(function(){bannerAlert("<?=_('Attention - operation continues in background')?> ["+pid.toString().padStart(8,'0')+"]<i class='fa fa-bomb fa-fw abortOps' title=\"<?=_('Abort background process')?>\" onclick='abortOperation("+pid+")'></i>",cmd,plg,func,start);});
});
$('.sweet-alert').addClass('nchan');
$('button.confirm').prop('disabled',button==0);
});
}
function abortOperation(pid) {
swal({title:"<?=_('Abort background operation')?>",text:"<?=_('This may leave an unknown state')?>",html:true,animation:'none',type:'warning',showCancelButton:true,confirmButtonText:"<?=_('Proceed')?>",cancelButtonText:"<?=_('Cancel')?>"},function(){
$.post('/webGui/include/StartCommand.php',{kill:pid},function() {
clearTimeout(timers.bannerAlert);
timers.bannerAlert = null;
timers.callback = null;
forcedBanner = false;
removeBannerWarning($.cookie('addAlert'));
$.removeCookie('addAlert');
$(".upgrade_notice").removeClass('alert done').hide();
});
});
}
function openChanges(cmd,title,nchan,button=0) {
$('div.spinner.fixed').show();
// button = 0 : hide CLOSE button (default)
// button = 1 : show CLOSE button
// nchan argument is not used, exists for backward compatibility
$.post('/webGui/include/StartCommand.php',{cmd:cmd,start:2},function(data) {
$('div.spinner.fixed').hide();
swal({title:title,text:"<pre id='swalbody'></pre><hr>",html:true,animation:'none',showConfirmButton:button!=0,confirmButtonText:"<?=_('Close')?>"},function(close){
$('.sweet-alert').hide('fast').removeClass('nchan');
if ($('#submit_button').length > 0) $('#submit_button').remove();
});
$('.sweet-alert').addClass('nchan');
$('pre#swalbody').html(data);
$('button.confirm').text("<?=_('Done')?>").prop('disabled',false).show();
});
}
function openAlert(cmd,title,func) {
$.post('/webGui/include/StartCommand.php',{cmd:cmd,start:2},function(data) {
$('div.spinner.fixed').hide();
swal({title:title,text:"<pre id='swalbody'></pre><hr>",html:true,animation:'none',showCancelButton:true,closeOnConfirm:false,confirmButtonText:"<?=_('Proceed')?>",cancelButtonText:"<?=_('Cancel')?>"},function(proceed){
if (proceed) setTimeout(func+'()');
});
$('.sweet-alert').addClass('nchan');
$('pre#swalbody').html(data);
});
}
function openDone(data) {
if (data == '_DONE_') {
$('div.spinner.fixed').hide();
$('button.confirm').text("<?=_('Done')?>").prop('disabled',false).show();
if (typeof ca_done_override !== 'undefined') {
if (ca_done_override == true) {
$("button.confirm").trigger("click");
ca_done_override = false;
}
}
return true;
}
return false;
}
function openError(data) {
if (data == '_ERROR_') {
$('div.spinner.fixed').hide();
$('button.confirm').text("<?=_('Error')?>").prop('disabled',false).show();
return true;
}
return false;
}
function showStatus(name,plugin,job) {
$.post('/webGui/include/ProcessStatus.php',{name:name,plugin:plugin,job:job},function(status){$(".tabs").append(status);});
}
function showFooter(data, id) {
if (id !== undefined) $('#'+id).remove();
$('#copyright').prepend(data);
}
function showNotice(data) {
$('#user-notice').html(data.replace(/<a>(.*)<\/a>/,"<a href='/Plugins'>$1</a>"));
}
function escapeQuotes(form) {
$(form).find('input[type=text]').each(function(){$(this).val($(this).val().replace(/"/g,'\\"'));});
}
// Banner warning system
var bannerWarnings = [];
var currentBannerWarning = 0;
var osUpgradeWarning = false;
var forcedBanner = false;
function addBannerWarning(text, warning=true, noDismiss=false, forced=false) {
var cookieText = text.replace(/[^a-z0-9]/gi,'');
if ($.cookie(cookieText) == "true") return false;
if (warning) text = "<i class='fa fa-warning fa-fw' style='float:initial'></i> "+text;
if (bannerWarnings.indexOf(text) < 0) {
if (forced) {
var arrayEntry = 0; bannerWarnings = []; clearTimeout(timers.bannerWarning); timers.bannerWarning = null; forcedBanner = true;
} else {
var arrayEntry = bannerWarnings.push("placeholder") - 1;
}
if (!noDismiss) text += "<a class='bannerDismiss' onclick='dismissBannerWarning("+arrayEntry+",&quot;"+cookieText+"&quot;)'></a>";
bannerWarnings[arrayEntry] = text;
} else {
return bannerWarnings.indexOf(text);
}
if (timers.bannerWarning==null) showBannerWarnings();
return arrayEntry;
}
function dismissBannerWarning(entry,cookieText) {
$.cookie(cookieText,"true",{expires:30}); // reset after 1 month
removeBannerWarning(entry);
}
function removeBannerWarning(entry) {
if (forcedBanner) return;
bannerWarnings[entry] = false;
clearTimeout(timers.bannerWarning);
showBannerWarnings();
}
function bannerFilterArray(array) {
var newArray = [];
array.filter(function(value,index,arr) {
if (value) newArray.push(value);
});
return newArray;
}
function showBannerWarnings() {
var allWarnings = bannerFilterArray(Object.values(bannerWarnings));
if (allWarnings.length == 0) {
$(".upgrade_notice").hide();
timers.bannerWarning = null;
return;
}
if (currentBannerWarning >= allWarnings.length) currentBannerWarning = 0;
$(".upgrade_notice").show().html(allWarnings[currentBannerWarning]);
currentBannerWarning++;
timers.bannerWarning = setTimeout(showBannerWarnings,3000);
}
function addRebootNotice(message="<?=_('You must reboot for changes to take effect')?>") {
addBannerWarning("<i class='fa fa-warning' style='float:initial;'></i> "+message,false,true);
$.post("/plugins/dynamix.plugin.manager/scripts/PluginAPI.php",{action:'addRebootNotice',message:message});
}
function removeRebootNotice(message="<?=_('You must reboot for changes to take effect')?>") {
var bannerIndex = bannerWarnings.indexOf("<i class='fa fa-warning' style='float:initial;'></i> "+message);
if (bannerIndex < 0) return;
removeBannerWarning(bannerIndex);
$.post("/plugins/dynamix.plugin.manager/scripts/PluginAPI.php",{action:'removeRebootNotice',message:message});
}
function showUpgradeChanges() { /** @note can likely be removed, not used in webgui or api repos */
openChanges("showchanges /tmp/plugins/unRAIDServer.txt","<?=_('Release Notes')?>");
}
function showUpgrade(text,noDismiss=false) { /** @note can likely be removed, not used in webgui or api repos */
if ($.cookie('os_upgrade')==null) {
if (osUpgradeWarning) removeBannerWarning(osUpgradeWarning);
osUpgradeWarning = addBannerWarning(text.replace(/<a>(.+?)<\/a>/,"<a href='#' onclick='openUpgrade()'>$1</a>").replace(/<b>(.*)<\/b>/,"<a href='#' onclick='document.rebootNow.submit()'>$1</a>"),false,noDismiss);
}
}
function hideUpgrade(set) { /** @note can likely be removed, not used in webgui or api repos */
removeBannerWarning(osUpgradeWarning);
if (set)
$.cookie('os_upgrade','true');
else
$.removeCookie('os_upgrade');
}
function confirmUpgrade(confirm) {
if (confirm) {
swal({title:"<?=_('Update')?> Unraid OS",text:"<?=_('Do you want to update to the new version')?>?",type:'warning',html:true,animation:'none',showCancelButton:true,closeOnConfirm:false,confirmButtonText:"<?=_('Proceed')?>",cancelButtonText:"<?=_('Cancel')?>"},function(){
openPlugin("plugin update unRAIDServer.plg","<?=_('Update')?> Unraid OS");
});
} else {
openPlugin("plugin update unRAIDServer.plg","<?=_('Update')?> Unraid OS");
}
}
function openUpgrade() {
hideUpgrade();
$.get('/plugins/dynamix.plugin.manager/include/ShowPlugins.php',{cmd:'alert'},function(data) {
if (data==0) {
// no alert message - proceed with upgrade
confirmUpgrade(true);
} else {
// show alert message and ask for confirmation
openAlert("showchanges <?=$alerts?>","<?=_('Alert Message')?>",'confirmUpgrade');
}
});
}
function digits(number) {
if (number < 10) return 'one';
if (number < 100) return 'two';
return 'three';
}
function openNotifier() {
$.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) {
$.each($.parseJSON(msg), function(i, notify){
$.jGrowl(notify.subject+'<br>'+notify.description,{
group: notify.importance,
header: notify.event+': '+notify.timestamp,
theme: notify.file,
sticky: true,
beforeOpen: function(e,m,o){if ($('div.jGrowl-notification').hasClass(notify.file)) return(false);},
afterOpen: function(e,m,o){if (notify.link) $(e).css('cursor','pointer');},
click: function(e,m,o){if (notify.link) location.replace(notify.link);},
close: function(e,m,o){$.post('/webGui/include/Notify.php',{cmd:'archive',file:notify.file,csrf_token:csrf_token});}
});
});
});
}
function closeNotifier() {
$.post('/webGui/include/Notify.php',{cmd:'get',csrf_token:csrf_token},function(msg) {
$.each($.parseJSON(msg), function(i, notify){
$.post('/webGui/include/Notify.php',{cmd:'archive',file:notify.file,csrf_token:csrf_token});
});
$('div.jGrowl').find('div.jGrowl-close').trigger('click');
});
}
function viewHistory() {
location.replace('/Tools/NotificationsArchive');
}
function flashReport() {
$.post('/webGui/include/Report.php',{cmd:'config'},function(check){
if (check>0) addBannerWarning("<?=_('Your flash drive is corrupted or offline').'. '._('Post your diagnostics in the forum for help').'.'?> <a target='_blank' href='https://docs.unraid.net/go/changing-the-flash-device/'><?=_('See also here')?></a>");
});
}
$(function() {
let tab;
<?switch ($myPage['name']):?>
<?case'Main':?>
tab = $.cookie('tab')||'tab1';
<?break;?>
<?case'Cache':case'Data':case'Device':case'Flash':case'Parity':?>
tab = $.cookie('one')||'tab1';
<?break;?>
<?default:?>
tab = $.cookie('one')||'tab1';
<?endswitch;?>
/* Check if the tab is 'tab0' */
if (tab === 'tab0') {
/* Set tab to the last available tab based on input[name$="tabs"] length */
tab = 'tab' + $('input[name$="tabs"]').length;
} else if ($('#' + tab).length === 0) {
/* If the tab element does not exist, initialize a tab and set to 'tab1' */
initab();
tab = 'tab1';
}
$('#'+tab).attr('checked', true);
updateTime();
$.jGrowl.defaults.closeTemplate = '<i class="fa fa-close"></i>';
$.jGrowl.defaults.closerTemplate = '<?=$notify['position'][0]=='b' ? '<div class="bottom">':'<div class="top">'?>[ <?=_("close all notifications")?> ]</div>';
$.jGrowl.defaults.position = '<?=$notify['position']?>';
$.jGrowl.defaults.theme = '';
$.jGrowl.defaults.themeState = '';
$.jGrowl.defaults.pool = 10;
<?if ($notify['life'] > 0):?>
$.jGrowl.defaults.life = <?=$notify['life']*1000?>;
<?else:?>
$.jGrowl.defaults.sticky = true;
<?endif;?>
Shadowbox.setup('a.sb-enable', {modal:true});
// add any pre-existing reboot notices
$.post('/webGui/include/Report.php',{cmd:'notice'},function(notices){
notices = notices.split('\n');
for (var i=0,notice; notice=notices[i]; i++) addBannerWarning("<i class='fa fa-warning' style='float:initial;'></i> "+notice,false,true);
});
// check for flash offline / corrupted (delayed).
timers.flashReport = setTimeout(flashReport,6000);
});
var mobiles=['ipad','iphone','ipod','android'];
var device=navigator.platform.toLowerCase();
for (var i=0,mobile; mobile=mobiles[i]; i++) {
if (device.indexOf(mobile)>=0) {$('#footer').css('position','static'); break;}
}
$.ajaxPrefilter(function(s, orig, xhr){
if (s.type.toLowerCase() == "post" && !s.crossDomain) {
s.data = s.data || "";
s.data += s.data?"&":"";
s.data += "csrf_token="+csrf_token;
}
});
<?if (isset($myPage['Load']) && $myPage['Load'] > 0):?>
// Reload page every X minutes during extended viewing?
function setTimerReload() {
timers.reload = setInterval(function(){
if (nchanPaused === false && ! dialogOpen() ) {
location.reload();
}
},<?=$myPage['Load'] * 60000?>);
}
$(document).click(function(e) {
clearInterval(timers.reload);
setTimerReload();
});
function dialogOpen() {
return ($('.sweet-alert').is(':visible') || $('.swal-overlay--show-modal').is(':visible') );
}
setTimerReload();
<?endif;?>
</script>

View File

@@ -0,0 +1,13 @@
<div id="header" class="<?=$display['banner']?>">
<div class="logo">
<a href="https://unraid.net" target="_blank">
<?readfile("$docroot/webGui/images/UN-logotype-gradient.svg")?>
</a>
<unraid-i18n-host>
<unraid-header-os-version></unraid-header-os-version>
</unraid-i18n-host>
</div>
<?include "$docroot/plugins/dynamix.my.servers/include/myservers2.php"?>
</div>

View File

@@ -0,0 +1,100 @@
<?
/**
* Main content template for the Unraid web interface.
* Handles the rendering of tabs and page content.
*/
$defaultIcon = "<i class=\"icon-app PanelIcon\"></i>";
// Helper function to process icon
function process_icon($icon, $docroot, $root) {
global $defaultIcon;
if (substr($icon, -4) == '.png') {
if (file_exists("$docroot/$root/images/$icon")) {
return "<img src=\"/$root/images/$icon\" class=\"PanelImg\">";
} elseif (file_exists("$docroot/$root/$icon")) {
return "<img src=\"/$root/$icon\" class=\"PanelImg\">";
}
return $defaultIcon;
} elseif (substr($icon, 0, 5) == 'icon-') {
return "<i class=\"$icon PanelIcon\"></i>";
} elseif ($icon[0] != '<') {
if (substr($icon, 0, 3) != 'fa-') {
$icon = "fa-$icon";
}
return "<i class=\"fa $icon PanelIcon\"></i>";
}
return $icon;
}
$tab = 1;
// even if DisplaySettings is not enabled for tabs, pages with Tabs="true" will use tabs
$display['tabs'] = isset($myPage['Tabs']) ? (strtolower($myPage['Tabs']) == 'true' ? 0 : 1) : 1;
$tabbed = $display['tabs'] == 0 && count($pages) > 1;
?>
<div id="displaybox">
<div class="tabs">
<? foreach ($pages as $page):
$close = false;
if (isset($page['Title'])):
$title = htmlspecialchars($page['Title']) ?? '';
if ($tabbed): ?>
<div class="tab">
<input type="radio" id="tab<?= $tab ?>" name="tabs" onclick="settab(this.id)">
<label for="tab<?= $tab ?>">
<?= tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
</label>
<div class="content">
<? $close = true;
else:
if ($tab == 1): ?>
<div class="tab">
<input type="radio" id="tab<?= $tab ?>" name="tabs">
<div class="content shift">
<? endif; ?>
<div class="title">
<span class="left">
<?= tab_title($title, $page['root'], _var($page, 'Tag', false)) ?>
</span>
</div>
<? endif;
$tab++;
endif;
// Handle menu type pages
if (isset($page['Type']) && $page['Type'] == 'menu'):
$pgs = find_pages($page['name']);
foreach ($pgs as $pg):
// Set title variable with proper escaping (suppress errors)
@$title = htmlspecialchars($pg['Title']);
$icon = _var($pg, 'Icon', $defaultIcon);
$icon = process_icon($icon, $docroot, $pg['root']); ?>
<div class="Panel">
<a href="/<?= $path ?>/<?= $pg['name'] ?>" onclick="$.cookie('one','tab1')">
<span><?= $icon ?></span>
<div class="PanelText"><?= _($title) ?></div>
</a>
</div>
<? endforeach;
endif;
// Annotate with HTML comment
annotate($page['file']);
// Create page content
if (empty($page['Markdown']) || $page['Markdown'] == 'true'):
eval('?>'.Markdown(parse_text($page['text'])));
else:
eval('?>'.parse_text($page['text']));
endif;
if ($close): ?>
</div><!-- /.content -->
</div><!-- /.tab -->
<? endif;
endforeach; ?>
</div><!-- /.tabs -->
</div><!-- /#displaybox -->
<?
// Clean up variables
unset($pages, $page, $pgs, $pg, $icon, $nchan, $running, $start, $stop, $row, $script, $opt, $nchan_run);
?>

View File

@@ -0,0 +1,24 @@
<?
/**
* Global elements are elements that are used on all pages.
* Describe the purpose of each element here.
*
* - upgrade_notice: Display a notice to the user that they need to upgrade their browser.
* - move_to_end: Move to the end of the page.
* - back_to_top: Move to the top of the page.
* - spinner: Display the Unraid loader icon.
* - rebootNow: Global form to POST to /webGui/include/Boot.php?cmd=reboot to trigger a system reboot.
* - progressFrame: Display a frame to show the progress of the reboot.
*/
?>
<div class="upgrade_notice" style="display:none"></div>
<a href="#" class="move_to_end" title="<?=_('Move To End')?>"><i class="fa fa-arrow-circle-down"></i></a>
<a href="#" class="back_to_top" title="<?=_('Back To Top')?>"><i class="fa fa-arrow-circle-up"></i></a>
<div class="spinner fixed"></div>
<form name="rebootNow" method="POST" action="/webGui/include/Boot.php"><input type="hidden" name="cmd" value="reboot"></form>
<iframe id="progressFrame" name="progressFrame" frameborder="0"></iframe>

Some files were not shown because too many files have changed in this diff Show More