734 lines
27 KiB
HTML
734 lines
27 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" data-bs-theme="auto">
|
|
|
|
<head>
|
|
<%- header %>
|
|
|
|
<script type="text/javascript" src="/assets/js/qrcode.min.js"></script>
|
|
<style scoped type="text/css">
|
|
.content-container {
|
|
padding-top: 2em;
|
|
}
|
|
|
|
.pin-tab-bar {
|
|
margin-bottom: 2em !important;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body id="app" v-cloak>
|
|
<Navbar></Navbar>
|
|
<div id="content" class="container content-container d-flex flex-column align-items-center">
|
|
<ul class="nav nav-pills pin-tab-bar justify-content-center">
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{active: currentTab !== '#PIN'}" href="#OTP" @click.prevent="switchTab('OTP')">{{ $t('pin.otp_pairing') }}</a>
|
|
</li>
|
|
<li class="nav-item">
|
|
<a class="nav-link" :class="{active: currentTab === '#PIN'}" href="#PIN" @click.prevent="switchTab('PIN')">{{ $t('pin.pin_pairing') }}</a>
|
|
</li>
|
|
</ul>
|
|
<form v-if="currentTab === '#PIN'" class="form d-flex flex-column align-items-center" id="form" @submit.prevent="registerDevice">
|
|
<div class="card flex-column d-flex p-4 mb-4">
|
|
<input type="text" pattern="\d*" :placeholder="`${$t('navbar.pin')}`" autofocus id="pin-input" class="form-control mt-2" required />
|
|
<input type="text" :placeholder="`${$t('pin.device_name')}`" id="name-input" class="form-control my-4" />
|
|
<button class="btn btn-primary">{{ $t('pin.send') }}</button>
|
|
</div>
|
|
<div id="status"></div>
|
|
</form>
|
|
<form v-else class="form d-flex flex-column align-items-center" @submit.prevent="requestOTP">
|
|
<div class="card flex-column d-flex p-4 mb-4">
|
|
<div v-show="editingHost || (otp && hostAddr)" id="qrRef"></div>
|
|
<p v-if="editingHost || (otp && hostAddr)" class="text-center text-secondary"><a class="text-secondary" :href="deepLink">art://{{ hostAddr }}:{{ hostPort }}</a> <i class="fas fa-fw fa-pen-to-square" @click="editHost"></i></p>
|
|
<h1 class="mb-4 text-center">{{ otp && otp || '????' }}</h1>
|
|
<div v-if="editingHost" class="d-flex flex-column align-items-stretch">
|
|
<input type="text" placeholder="HOST" v-model="hostAddr" autofocus class="form-control mt-2" />
|
|
<input type="text" placeholder="PORT" v-model="hostPort" class="form-control my-4" />
|
|
<button class="btn btn-primary" :disabled="!this.canSaveHost" @click.prevent="saveHost">{{ $t('_common.save') }}</button>
|
|
</div>
|
|
<div v-else class="d-flex flex-column align-items-stretch">
|
|
<input type="text" pattern="[0-9a-zA-Z]{4,}" :placeholder="`${$t('pin.otp_passphrase')}`" v-model="passphrase" required autofocus class="form-control mt-2" />
|
|
<input type="text" :placeholder="`${$t('pin.device_name')}`" v-model="deviceName" class="form-control my-4" />
|
|
<button class="btn btn-primary">{{ $t('pin.generate_pin') }}</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="otpMessage" class="alert" :class="['alert-' + otpStatus]">{{ otpMessage }}</div>
|
|
<div class="alert alert-info">{{ $t('pin.otp_msg') }}</div>
|
|
</form>
|
|
<div class="alert alert-warning">
|
|
<b>{{ $t('_common.warning') }}</b> {{ $t('pin.warning_msg') }}
|
|
</div>
|
|
<!-- Manage Clients -->
|
|
<div class="card my-4 align-self-stretch">
|
|
<div class="card-body">
|
|
<div class="p-2">
|
|
<div class="d-flex justify-content-end align-items-center">
|
|
<h2 id="unpair" class="me-auto">{{ $t('pin.device_management') }}</h2>
|
|
<button class="btn btn-danger" :disabled="unpairAllPressed" @click="unpairAll">
|
|
{{ $t('pin.unpair_all') }}
|
|
</button>
|
|
</div>
|
|
<br />
|
|
<p class="mb-0">{{ $t('pin.device_management_desc') }}</p>
|
|
<p class="mb-0">{{ $t('pin.device_management_warning') }} <a href="https://github.com/ClassicOldSong/Apollo/wiki/Permission-System" target="_blank">{{ $t('_common.learn_more') }}</a></p>
|
|
<div id="apply-alert" class="alert alert-success d-flex align-items-center mt-3" :style="{ 'display': (showApplyMessage ? 'flex !important': 'none !important') }">
|
|
<div class="me-2"><b>{{ $t('_common.success') }}</b> {{ $t('pin.unpair_single_success') }}</div>
|
|
<button class="btn btn-success ms-auto apply" @click="clickedApplyBanner">{{ $t('_common.dismiss') }}</button>
|
|
</div>
|
|
<div class="alert alert-success mt-3" v-if="unpairAllStatus === true">
|
|
{{ $t('pin.unpair_all_success') }}
|
|
</div>
|
|
<div class="alert alert-danger mt-3" v-if="unpairAllStatus === false">
|
|
{{ $t('pin.unpair_all_error') }}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<ul id="client-list" class="list-group list-group-flush list-group-item-light" v-if="clients && clients.length > 0">
|
|
<template v-for="client in clients" class="list-group-item d-flex align-items-center">
|
|
<div v-if="client.editing" class="list-group-item d-flex align-items-stretch flex-column">
|
|
<div class="d-flex align-items-center">
|
|
<div class="p-2 flex-grow-1 d-flex align-items-center">
|
|
<span class="badge" :class="client.editPerm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
|
|
[ {{permToStr(client.editPerm)}} ]
|
|
</span>
|
|
|
|
<input v-model="client.editName" @keyup.enter="saveClient(client)" class="form-control flex-grow-1" type="text" :placeholder="$t('pin.device_name')">
|
|
</div>
|
|
<div class="me-2 btn btn-success" @click="saveClient(client)"><i class="fas fa-check"></i></div>
|
|
<div class="me-2 btn btn-secondary" @click="cancelEdit(client)"><i class="fas fa-times"></i></div>
|
|
</div>
|
|
<div class="align-items-top d-flex flex-row justify-content-center">
|
|
<div v-for="group in permissionGroups" class="d-flex flex-column mx-2">
|
|
<div class="mx-2">{{ group.name }}:</div>
|
|
<button v-for="perm in group.permissions" class="my-1 btn btn-sm" :disabled="isSuppressed(client.editPerm, perm.name, perm.suppressed_by)" :class="(isSuppressed(client.editPerm, perm.name, perm.suppressed_by) || checkPermission(client.editPerm, perm.name)) ? 'btn-success' : 'btn-outline-secondary'" @click="togglePermission(client, perm.name)">
|
|
{{ $t(`permissions.${perm.name}`) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Enable legacy ordering -->
|
|
<Checkbox class="mb-3"
|
|
id="enable_legacy_ordering"
|
|
label="pin.enable_legacy_ordering"
|
|
desc="pin.enable_legacy_ordering_desc"
|
|
v-model="client.editEnableLegacyOrdering"
|
|
default="true"
|
|
></Checkbox>
|
|
|
|
<!-- Always Use Virtual Display -->
|
|
<Checkbox class="mb-3"
|
|
id="always_use_virtual_display"
|
|
label="pin.always_use_virtual_display"
|
|
desc="pin.always_use_virtual_display_desc"
|
|
v-model="client.editAlwaysUseVirtualDisplay"
|
|
default="false"
|
|
v-if="platform === 'windows'"
|
|
></Checkbox>
|
|
<!-- Display Mode Override -->
|
|
<div class="mb-3 mt-2">
|
|
<label for="display_mode_override" class="form-label">{{ $t('pin.display_mode_override') }}</label>
|
|
<input
|
|
type="text"
|
|
class="form-control"
|
|
id="display_mode_override"
|
|
v-model="client.editDisplayMode"
|
|
placeholder="1920x1080x59.94"
|
|
@input="validateModeOverride"
|
|
/>
|
|
<div class="form-text">{{ $t('pin.display_mode_override_desc') }} <a href="https://github.com/ClassicOldSong/Apollo/wiki/Display-Mode-Override" target="_blank">{{ $t('_common.learn_more') }}</a></div>
|
|
</div>
|
|
|
|
<!-- Allow client commands -->
|
|
<Checkbox class="mb-3"
|
|
id="allow_client_commands"
|
|
label="pin.allow_client_commands"
|
|
desc="pin.allow_client_commands_desc"
|
|
v-model="client.editAllowClientCommands"
|
|
default="true"
|
|
></Checkbox>
|
|
|
|
<!-- connect/disconnect commands -->
|
|
<div class="mb-3 mt-2 d-flex flex-column" v-for="cmdType in ['do', 'undo']" v-if="client.editAllowClientCommands">
|
|
<label class="mb-0 orm-label">{{ $t(`pin.client_${cmdType}_cmd`) }}</label>
|
|
<div class="form-text">{{ $t(`pin.client_${cmdType}_cmd_desc`) }} <a href="https://github.com/ClassicOldSong/Apollo/wiki/Client-Commands" target="_blank">{{ $t('_common.learn_more') }}</a></div>
|
|
<table class="mt-2 table" v-if="client[`edit_${cmdType}`].length > 0">
|
|
<thead>
|
|
<tr>
|
|
<th style="width: 80%"><i class="fas fa-terminal"></i> {{ $t('_common.cmd_val') }}</th>
|
|
<th style="min-width: 10em; max-width: 12em;" v-if="platform === 'windows'">
|
|
<i class="fas fa-shield-alt"></i> {{ $t('_common.run_as') }}
|
|
</th>
|
|
<th style="min-width: 110px;"></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<tr v-for="(c, i) in client[`edit_${cmdType}`]">
|
|
<td>
|
|
<input type="text" class="form-control monospace" v-model="c.cmd" />
|
|
</td>
|
|
<td v-if="platform === 'windows'">
|
|
<div class="form-check">
|
|
<input type="checkbox" class="form-check-input" :id="`client-${cmdType}-cmd-admin-${i}`" v-model="c.elevated"/>
|
|
<label :for="`client-${cmdType}-cmd-admin-${i}`" class="form-check-label">{{ $t('_common.elevated') }}</label>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<button class="btn btn-danger me-2" @click="removeCmd(client[`edit_${cmdType}`], i)">
|
|
<i class="fas fa-trash"></i>
|
|
</button>
|
|
<button class="btn btn-success" @click="addCmd(client[`edit_${cmdType}`], i)">
|
|
<i class="fas fa-plus"></i>
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
<button class="ms-0 mt-2 btn btn-success" style="margin: 0 auto" @click="addCmd(client[`edit_${cmdType}`], -1)">
|
|
+ {{ $t('config.add') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div v-else class="list-group-item d-flex align-items-center">
|
|
<div class="p-2 flex-grow-1 d-flex align-items-center">
|
|
<span class="badge" :class="client.perm >= 0x04000000 ? 'bg-danger' : 'bg-primary'">
|
|
[ {{permToStr(client.perm)}} ]
|
|
</span>
|
|
|
|
<span class="me-2">{{client.name != "" ? client.name : $t('pin.unpair_single_unknown')}}</span>
|
|
</div>
|
|
<div v-if="client.connected" class="me-2 btn btn-warning" @click="disconnectClient(client.uuid)"><i class="fas fa-link-slash"></i></div>
|
|
<div class="me-2 btn btn-primary" @click="editClient(client)"><i class="fas fa-edit"></i></div>
|
|
<div class="me-2 btn btn-danger" @click="unpairSingle(client.uuid)"><i class="fas fa-trash"></i></div>
|
|
</div>
|
|
</template>
|
|
</ul>
|
|
<ul v-else class="list-group list-group-flush list-group-item-light">
|
|
<div class="list-group-item p-3 text-center"><em>{{ $t('pin.unpair_single_no_devices') }}</em></div>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</body>
|
|
|
|
<script type="module">
|
|
import { createApp } from 'vue'
|
|
import { initApp } from './init'
|
|
import Navbar from './Navbar.vue'
|
|
import Checkbox from './Checkbox.vue'
|
|
|
|
let resetOTPTimeout = null;
|
|
const qrContainer = document.createElement('div');
|
|
qrContainer.className = "mb-2 p-2 bg-white"
|
|
const qrCode = new QRCode(qrContainer);
|
|
|
|
const updateQR = (url) => {
|
|
qrCode.clear()
|
|
qrCode.makeCode(url)
|
|
|
|
const refContainer = document.querySelector('#qrRef');
|
|
if (refContainer) refContainer.appendChild(qrContainer);
|
|
}
|
|
|
|
let hostInfoCache = JSON.parse(sessionStorage.getItem('hostInfo'));
|
|
let hostManuallySet = false;
|
|
|
|
if (hostInfoCache) hostManuallySet = true;
|
|
|
|
const saveHostCache = ({hostAddr, hostPort}, manual) => {
|
|
hostInfoCache = {hostAddr, hostPort}
|
|
if (manual) {
|
|
sessionStorage.setItem('hostInfo', JSON.stringify(hostInfoCache))
|
|
hostManuallySet = true;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Permissions:
|
|
enum class PERM: uint32_t {
|
|
_reserved = 1,
|
|
|
|
_input = _reserved << 8, // Input permission group
|
|
input_controller = _input << 0, // Allow controller input
|
|
input_touch = _input << 1, // Allow touch input
|
|
input_pen = _input << 2, // Allow pen input
|
|
input_mouse = _input << 3, // Allow mouse input
|
|
input_kbd = _input << 4, // Allow keyboard input
|
|
_all_inputs = input_controller | input_touch | input_pen | input_mouse | input_kbd,
|
|
|
|
_operation = _input << 8, // Operation permission group
|
|
clipboard_set = _operation << 0, // Allow set clipboard from client
|
|
clipboard_read = _operation << 1, // Allow read clipboard from host
|
|
file_upload = _operation << 2, // Allow upload files to host
|
|
file_dwnload = _operation << 3, // Allow download files from host
|
|
server_cmd = _operation << 4, // Allow execute server cmd
|
|
_all_opeiations = clipboard_set | clipboard_read | file_upload | file_dwnload | server_cmd,
|
|
|
|
_action = _operation << 8, // Action permission group
|
|
list = _action << 0, // Allow list apps
|
|
view = _action << 1, // Allow view streams
|
|
launch = _action << 2, // Allow launch apps
|
|
_allow_view = view | launch, // Launch contains view permission
|
|
_all_actions = list | view | launch,
|
|
|
|
_default = view | list, // Default permissions for new clients
|
|
_no = 0, // No permissions are granted
|
|
_all = _all_inputs | _all_opeiations | _all_actions, // All current permissions
|
|
};
|
|
*/
|
|
|
|
const permissionMapping = {
|
|
// Input permission group
|
|
input_controller: 0x00000100,
|
|
input_touch: 0x00000200,
|
|
input_pen: 0x00000400,
|
|
input_mouse: 0x00000800,
|
|
input_kbd: 0x00001000,
|
|
_all_inputs: 0x00001F00,
|
|
|
|
// Operation permission group
|
|
clipboard_set: 0x00010000,
|
|
clipboard_read: 0x00020000,
|
|
file_upload: 0x00040000,
|
|
file_dwnload: 0x00080000,
|
|
server_cmd: 0x00100000,
|
|
_all_operations: 0x001F0000,
|
|
|
|
// Action permission group
|
|
list: 0x01000000,
|
|
view: 0x02000000,
|
|
launch: 0x04000000,
|
|
_allow_view: 0x06000000,
|
|
_all_actions: 0x07000000,
|
|
|
|
// Special permissions
|
|
_default: 0x03000000,
|
|
_no: 0x00000000,
|
|
_all: 0x071F1F00
|
|
};
|
|
|
|
const permissionGroups = [
|
|
{ name: 'Action', permissions: [
|
|
{
|
|
name: 'list',
|
|
suppressed_by: ['view', 'launch']
|
|
}, {
|
|
name: 'view',
|
|
suppressed_by: ['launch']
|
|
}, {
|
|
name: 'launch',
|
|
suppressed_by: []
|
|
}
|
|
] },
|
|
{ name: 'Operation', permissions: [
|
|
{
|
|
name: 'clipboard_set',
|
|
suppressed_by: []
|
|
},
|
|
{
|
|
name: 'clipboard_read',
|
|
suppressed_by: []
|
|
},
|
|
{
|
|
name: 'server_cmd',
|
|
suppressed_by: []
|
|
}
|
|
] },
|
|
{ name: 'Input', permissions: [
|
|
{
|
|
name: 'input_controller',
|
|
suppressed_by: []
|
|
}, {
|
|
name: 'input_touch',
|
|
suppressed_by: []
|
|
}, {
|
|
name: 'input_pen',
|
|
suppressed_by: []
|
|
}, {
|
|
name: 'input_mouse',
|
|
suppressed_by: []
|
|
}, {
|
|
name: 'input_kbd',
|
|
suppressed_by: []
|
|
}
|
|
] },
|
|
];
|
|
|
|
let currentEditingClient = null;
|
|
|
|
const data = () => {
|
|
return {
|
|
platform: '',
|
|
editingHost: false,
|
|
currentTab: location.hash || '#OTP',
|
|
otp: '',
|
|
passphrase: '',
|
|
otpMessage: '',
|
|
otpStatus: 'warning',
|
|
deviceName: '',
|
|
hostAddr: '',
|
|
hostPort: '',
|
|
hostName: '',
|
|
permissionGroups,
|
|
clients: [],
|
|
showApplyMessage: false,
|
|
unpairAllPressed: false,
|
|
unpairAllStatus: null
|
|
}
|
|
}
|
|
|
|
const cmdTpl = {
|
|
cmd: '',
|
|
elevated: 'false'
|
|
}
|
|
|
|
let app = createApp({
|
|
components: {
|
|
Navbar,
|
|
Checkbox
|
|
},
|
|
inject: ['i18n'],
|
|
data,
|
|
computed: {
|
|
deepLink() {
|
|
return encodeURI(`art://${this.hostAddr}:${this.hostPort}?pin=${this.otp}&passphrase=${this.passphrase}&name=${this.hostName}`);
|
|
},
|
|
canSaveHost() {
|
|
return !!(this.hostAddr && this.hostPort);
|
|
}
|
|
},
|
|
created() {
|
|
this.refreshClients();
|
|
},
|
|
methods: {
|
|
switchTab(currentTab) {
|
|
location.hash = currentTab;
|
|
const clients = this.clients;
|
|
Object.assign(this, data(), { clients });
|
|
hostInfoCache = null;
|
|
clearTimeout(resetOTPTimeout);
|
|
},
|
|
editHost() {
|
|
this.editingHost = !this.editingHost;
|
|
Object.assign(this, hostInfoCache);
|
|
},
|
|
saveHost() {
|
|
if (!this.canSaveHost) return;
|
|
updateQR(this.deepLink);
|
|
this.editingHost = false;
|
|
saveHostCache(this, true);
|
|
},
|
|
registerDevice(e) {
|
|
let pin = document.querySelector("#pin-input").value;
|
|
let name = document.querySelector("#name-input").value;
|
|
document.querySelector("#status").innerHTML = "";
|
|
let b = JSON.stringify({pin: pin, name: name});
|
|
fetch("./api/pin", {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
method: 'POST',
|
|
body: b
|
|
})
|
|
.then((response) => response.json())
|
|
.then((response) => {
|
|
if (response.status === true) {
|
|
document.querySelector(
|
|
"#status"
|
|
).innerHTML = `<div class="alert alert-success" role="alert">${this.i18n.t('pin.pair_success')}</div>`;
|
|
document.querySelector("#pin-input").value = "";
|
|
document.querySelector("#name-input").value = "";
|
|
|
|
setTimeout(() => this.refreshClients(), 1000);
|
|
|
|
alert(this.i18n.t('pin.pair_success_check_perm'));
|
|
} else {
|
|
document.querySelector(
|
|
"#status"
|
|
).innerHTML = `<div class="alert alert-danger" role="alert">${this.i18n.t('pin.pair_failure')}</div>`;
|
|
}
|
|
});
|
|
},
|
|
requestOTP() {
|
|
if (this.editingHost) return;
|
|
|
|
fetch("./api/otp", {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
method: 'POST',
|
|
body: JSON.stringify({ passphrase: this.passphrase, deviceName: this.deviceName })
|
|
})
|
|
.then(resp => resp.json())
|
|
.then(resp => {
|
|
if (!resp.status) {
|
|
this.otpMessage = resp.message
|
|
this.otpStatus = 'danger'
|
|
return
|
|
}
|
|
|
|
this.otp = resp.otp
|
|
this.hostName = resp.name
|
|
this.otpStatus = 'success'
|
|
this.otpMessage = this.i18n.t('pin.otp_success')
|
|
|
|
const isLocalHost = ['localhost', '127.0.0.1', '[::1]'].indexOf(location.hostname) >= 0
|
|
|
|
if (hostManuallySet) {
|
|
Object.assign(this, hostInfoCache);
|
|
} else {
|
|
this.hostAddr = resp.ip
|
|
this.hostPort = parseInt(location.port, 10) - 1
|
|
|
|
if (!isLocalHost) {
|
|
this.hostAddr = location.hostname
|
|
}
|
|
|
|
saveHostCache(this);
|
|
}
|
|
|
|
if (this.hostAddr) {
|
|
updateQR(this.deepLink);
|
|
|
|
if (resetOTPTimeout !== null) clearTimeout(resetOTPTimeout)
|
|
resetOTPTimeout = setTimeout(() => {
|
|
Object.assign(this, data(), {
|
|
otp: this.i18n.t('pin.otp_expired'),
|
|
otpMessage: this.i18n.t('pin.otp_expired_msg')
|
|
})
|
|
resetOTPTimeout = null
|
|
}, 3 * 60 * 1000)
|
|
|
|
if (!isLocalHost) {
|
|
setTimeout(() => {
|
|
if (window.confirm(this.i18n.t('pin.otp_pair_now'))) {
|
|
window.open(this.deepLink);
|
|
}
|
|
}, 0)
|
|
}
|
|
}
|
|
})
|
|
},
|
|
clickedApplyBanner() {
|
|
this.showApplyMessage = false;
|
|
},
|
|
addCmd(arr, idx) {
|
|
const newCmd = Object.assign({}, cmdTpl);
|
|
if (idx < 0) {
|
|
arr.push(newCmd);
|
|
} else {
|
|
arr.splice(idx + 1, 0, newCmd);
|
|
}
|
|
},
|
|
removeCmd(arr, idx) {
|
|
arr.splice(idx, 1);
|
|
},
|
|
editClient(client) {
|
|
if (currentEditingClient) {
|
|
this.cancelEdit(currentEditingClient);
|
|
}
|
|
client.editing = true;
|
|
client.editPerm = client.perm;
|
|
client.editName = client.name;
|
|
client.editAllowClientCommands = client.allow_client_commands;
|
|
client.editEnableLegacyOrdering = client.enable_legacy_ordering;
|
|
client.editAlwaysUseVirtualDisplay = client.always_use_virtual_display;
|
|
client.editDisplayMode = client.display_mode;
|
|
client.edit_do = JSON.parse(JSON.stringify(client.do || []));
|
|
client.edit_undo = JSON.parse(JSON.stringify(client.undo || []));
|
|
currentEditingClient = client;
|
|
|
|
console.log(client.do, client.undo)
|
|
},
|
|
cancelEdit(client) {
|
|
currentEditingClient = null;
|
|
client.editing = false;
|
|
client.editPerm = client.perm;
|
|
client.editName = client.name;
|
|
client.editDisplayMode = client.display_mode;
|
|
client.editAllowClientCommands = client.allow_client_commands;
|
|
client.editEnableLegacyOrdering = client.enable_legacy_ordering;
|
|
client.editAlwaysUseVirtualDisplay = client.always_use_virtual_display;
|
|
},
|
|
saveClient(client) {
|
|
client.editing = false;
|
|
currentEditingClient = null;
|
|
const editedClient = {
|
|
uuid: client.uuid,
|
|
name: client.editName,
|
|
display_mode: client.editDisplayMode.trim(),
|
|
allow_client_commands: client.editAllowClientCommands,
|
|
enable_legacy_ordering: client.editEnableLegacyOrdering,
|
|
always_use_virtual_display: client.editAlwaysUseVirtualDisplay,
|
|
perm: client.editPerm & permissionMapping._all,
|
|
do: client.edit_do.reduce((filtered, {cmd: _cmd, elevated}) => {
|
|
const cmd = _cmd.trim()
|
|
if (cmd) {
|
|
filtered.push({
|
|
cmd,
|
|
elevated
|
|
})
|
|
}
|
|
return filtered
|
|
}, []),
|
|
undo: client.edit_undo.reduce((filtered, {cmd: _cmd, elevated}) => {
|
|
const cmd = _cmd.trim()
|
|
if (cmd) {
|
|
filtered.push({
|
|
cmd,
|
|
elevated
|
|
})
|
|
}
|
|
return filtered
|
|
}, [])
|
|
}
|
|
fetch("./api/clients/update", {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
method: 'POST',
|
|
body: JSON.stringify(editedClient)
|
|
})
|
|
.then(resp => resp.json())
|
|
.then(resp => {
|
|
if (resp.status) {
|
|
this.refreshClients();
|
|
} else {
|
|
throw new Error(resp.message);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
alert(this.i18n.t('pin.save_client_error') + err);
|
|
})
|
|
.finally(() => {
|
|
setTimeout(() => {
|
|
this.refreshClients();
|
|
}, 1000);
|
|
});
|
|
},
|
|
permToStr(perm) {
|
|
const permSegments = [];
|
|
permSegments.push((perm >> 24) & 0xFF);
|
|
permSegments.push((perm >> 16) & 0xFF);
|
|
permSegments.push((perm >> 8) & 0xFF);
|
|
return permSegments.map(seg => seg.toString(16).toUpperCase().padStart(2, '0')).join(' ');
|
|
},
|
|
checkPermission(perm, permission) {
|
|
return (perm & permissionMapping[permission]) !== 0;
|
|
},
|
|
isSuppressed(perm, permission, suppressed_by) {
|
|
for (const suppressed of suppressed_by) {
|
|
if (this.checkPermission(perm, suppressed)) {
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
},
|
|
togglePermission(client, permission) {
|
|
client.editPerm ^= permissionMapping[permission];
|
|
},
|
|
validateModeOverride(event) {
|
|
const value = event.target.value.trim();
|
|
if (value && !value.match(/^\d+x\d+x\d+(\.\d+)?$/)) {
|
|
event.target.setCustomValidity(this.i18n.t('pin.display_mode_override_error'));
|
|
} else {
|
|
event.target.setCustomValidity('');
|
|
}
|
|
|
|
event.target.reportValidity();
|
|
},
|
|
disconnectClient(uuid) {
|
|
fetch("./api/clients/disconnect", {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
method: 'POST',
|
|
body: JSON.stringify({ uuid })
|
|
}).finally(() => {
|
|
setTimeout(() => {
|
|
this.refreshClients();
|
|
}, 1000);
|
|
});
|
|
},
|
|
unpairAll() {
|
|
this.unpairAllPressed = true;
|
|
fetch("./api/clients/unpair-all", {
|
|
credentials: 'include',
|
|
method: 'POST'
|
|
})
|
|
.then((r) => r.json())
|
|
.then((r) => {
|
|
this.unpairAllPressed = false;
|
|
this.unpairAllStatus = r.status;
|
|
setTimeout(() => {
|
|
this.unpairAllStatus = null;
|
|
}, 5000);
|
|
this.refreshClients();
|
|
});
|
|
},
|
|
unpairSingle(uuid) {
|
|
fetch("./api/clients/unpair", {
|
|
credentials: 'include',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
method: 'POST',
|
|
body: JSON.stringify({ uuid })
|
|
}).then(() => {
|
|
this.showApplyMessage = true;
|
|
this.refreshClients();
|
|
});
|
|
},
|
|
refreshClients() {
|
|
if (currentEditingClient) {
|
|
this.cancelEdit(currentEditingClient);
|
|
}
|
|
fetch("./api/clients/list", { credentials: 'include' })
|
|
.then((response) => response.json())
|
|
.then((response) => {
|
|
if (response.status && response.named_certs && response.named_certs.length) {
|
|
this.platform = response.platform
|
|
this.clients = response.named_certs.map(({
|
|
name,
|
|
uuid,
|
|
display_mode,
|
|
perm,
|
|
connected,
|
|
do: _do,
|
|
undo,
|
|
allow_client_commands,
|
|
always_use_virtual_display,
|
|
enable_legacy_ordering
|
|
}) => {
|
|
const permInt = parseInt(perm, 10);
|
|
return {
|
|
name,
|
|
uuid,
|
|
display_mode,
|
|
perm: permInt,
|
|
connected,
|
|
editing: false,
|
|
do: _do,
|
|
undo,
|
|
allow_client_commands,
|
|
enable_legacy_ordering,
|
|
always_use_virtual_display
|
|
}
|
|
})
|
|
currentEditingClient = null;
|
|
} else {
|
|
this.clients = [];
|
|
}
|
|
})
|
|
.catch(e => {
|
|
console.error(e)
|
|
location.reload();
|
|
});
|
|
},
|
|
}
|
|
});
|
|
|
|
initApp(app);
|
|
</script>
|