tabby-rdp 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +75 -0
- package/README.md +41 -11
- package/TROUBLESHOOTING.md +82 -0
- package/dist/accountRevisions.js +46 -0
- package/dist/accounts.js +5 -1
- package/dist/connectionStatus.js +21 -4
- package/dist/desktop.service.js +296 -96
- package/dist/desktopForm.js +5 -4
- package/dist/diskStorage.js +212 -0
- package/dist/fileTransfer.js +6 -53
- package/dist/help.js +10 -62
- package/dist/hostCompat.js +86 -0
- package/dist/index.js +5 -2
- package/dist/ironrdp.js +26 -0
- package/dist/keyboard.js +12 -1
- package/dist/packageChangeDialog.js +186 -0
- package/dist/rdcleanpath.js +17 -4
- package/dist/rdpFile.js +13 -4
- package/dist/rdpProfile.js +44 -14
- package/dist/releaseSettings.js +72 -0
- package/dist/releases.js +411 -0
- package/dist/settingsPage.js +110 -115
- package/dist/signin.js +73 -10
- package/dist/targets.js +9 -1
- package/dist/updates.js +77 -89
- package/docs/desk.md +157 -0
- package/package.json +11 -2
- package/vendor/SHA256SUMS +1 -0
- package/vendor/ironrdp-factory.js +3237 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PackageChangeDialog = void 0;
|
|
4
|
+
const STYLE = `
|
|
5
|
+
.trd-package-dialog { width: min(460px, calc(100vw - 40px)); padding: 24px; border-radius: 8px; outline: none;
|
|
6
|
+
border: 1px solid rgba(128,128,128,.35); background: var(--bs-body-bg, #1b1b1b); color: var(--bs-body-color, #ddd);
|
|
7
|
+
box-shadow: 0 16px 60px rgba(0,0,0,.45); }
|
|
8
|
+
.trd-package-dialog::backdrop { background: rgba(0,0,0,.55); }
|
|
9
|
+
.trd-package-dialog h4 { font-size: 18px; margin: 0 0 16px; }
|
|
10
|
+
.trd-package-dialog p { white-space: pre-line; overflow-wrap: anywhere; }
|
|
11
|
+
.trd-package-dialog [data-detail] { font-size: 13px; opacity: .8; }
|
|
12
|
+
.trd-package-dialog p:empty { display: none; }
|
|
13
|
+
.trd-package-dialog progress { display: block; width: 100%; height: 8px; margin: 18px 0 8px; accent-color: #589cf0; }
|
|
14
|
+
.trd-package-dialog [hidden] { display: none !important; }
|
|
15
|
+
.trd-package-dialog [data-elapsed] { font-size: 12px; opacity: .7; }
|
|
16
|
+
.trd-package-dialog [data-phase=error] { color: var(--bs-danger, #f08080); }
|
|
17
|
+
.trd-package-dialog [data-phase=success] { color: var(--bs-success, #79c99b); }
|
|
18
|
+
.trd-package-dialog .trd-package-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 22px; }
|
|
19
|
+
`;
|
|
20
|
+
/** One modal remains open from confirmation through the installer's verified result. */
|
|
21
|
+
class PackageChangeDialog {
|
|
22
|
+
constructor() {
|
|
23
|
+
this.dialog = null;
|
|
24
|
+
this.previousFocus = null;
|
|
25
|
+
this.started = 0;
|
|
26
|
+
}
|
|
27
|
+
update(state) {
|
|
28
|
+
if (state.phase === 'cancelled') {
|
|
29
|
+
this.close();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.open();
|
|
33
|
+
this.field('[data-title]').textContent = state.action === 'install'
|
|
34
|
+
? `Install tabby-rdp ${state.version}` : 'Uninstall tabby-rdp';
|
|
35
|
+
const result = this.field('[data-result]');
|
|
36
|
+
result.dataset.phase = state.phase;
|
|
37
|
+
result.setAttribute('role', state.phase === 'error' ? 'alert' : 'status');
|
|
38
|
+
const complete = state.phase === 'success' || state.phase === 'error';
|
|
39
|
+
this.field('progress').hidden = complete;
|
|
40
|
+
this.field('[data-elapsed]').hidden = complete;
|
|
41
|
+
this.field('[data-cancel]').hidden = true;
|
|
42
|
+
this.field('[data-confirm]').hidden = true;
|
|
43
|
+
this.field('[data-close]').hidden = !complete;
|
|
44
|
+
this.field('[data-detail]').textContent = '';
|
|
45
|
+
let message = state.message ?? '';
|
|
46
|
+
if (state.phase === 'preparing') {
|
|
47
|
+
message = state.action === 'install' ? 'Checking compatibility…' : 'Checking installation…';
|
|
48
|
+
}
|
|
49
|
+
if (state.phase === 'installing') {
|
|
50
|
+
message = `Installing tabby-rdp ${state.version}…`;
|
|
51
|
+
}
|
|
52
|
+
if (state.phase === 'uninstalling') {
|
|
53
|
+
message = 'Uninstalling tabby-rdp…';
|
|
54
|
+
}
|
|
55
|
+
if (state.phase === 'verifying') {
|
|
56
|
+
message = state.action === 'install' ? 'Verifying installation…' : 'Verifying removal…';
|
|
57
|
+
}
|
|
58
|
+
if (state.phase === 'success') {
|
|
59
|
+
this.field('[data-title]').textContent = state.action === 'install' ? 'Installation complete' : 'Uninstall complete';
|
|
60
|
+
if (!state.message) {
|
|
61
|
+
message = state.action === 'install' ? `tabby-rdp ${state.version} is installed.` : 'tabby-rdp is uninstalled.';
|
|
62
|
+
this.field('[data-detail]').textContent = 'Close remote desktops, then restart Tabby.';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (state.phase === 'error') {
|
|
66
|
+
this.field('[data-title]').textContent = state.action === 'install' ? 'Installation failed' : 'Uninstall failed';
|
|
67
|
+
}
|
|
68
|
+
this.field('[data-message]').textContent = message;
|
|
69
|
+
if (complete) {
|
|
70
|
+
this.stopTimer();
|
|
71
|
+
this.field('[data-close]').focus();
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
this.dialog.focus();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
confirm(message, detail, action) {
|
|
78
|
+
this.open();
|
|
79
|
+
this.stopTimer();
|
|
80
|
+
this.field('[data-title]').textContent = message;
|
|
81
|
+
this.field('[data-message]').textContent = detail;
|
|
82
|
+
this.field('[data-detail]').textContent = '';
|
|
83
|
+
this.field('progress').hidden = true;
|
|
84
|
+
this.field('[data-elapsed]').hidden = true;
|
|
85
|
+
this.field('[data-close]').hidden = true;
|
|
86
|
+
const cancel = this.field('[data-cancel]');
|
|
87
|
+
const accept = this.field('[data-confirm]');
|
|
88
|
+
cancel.hidden = false;
|
|
89
|
+
accept.hidden = false;
|
|
90
|
+
accept.textContent = action;
|
|
91
|
+
this.field('[data-result]').dataset.phase = 'confirming';
|
|
92
|
+
cancel.focus();
|
|
93
|
+
return new Promise(resolve => { this.confirmation = resolve; });
|
|
94
|
+
}
|
|
95
|
+
field(selector) { return this.dialog.querySelector(selector); }
|
|
96
|
+
open() {
|
|
97
|
+
if (this.dialog) {
|
|
98
|
+
if (!this.dialog.open) {
|
|
99
|
+
this.dialog.showModal();
|
|
100
|
+
}
|
|
101
|
+
if (!this.timer) {
|
|
102
|
+
this.startTimer();
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!document.getElementById('trd-package-dialog-style')) {
|
|
107
|
+
const style = document.createElement('style');
|
|
108
|
+
style.id = 'trd-package-dialog-style';
|
|
109
|
+
style.textContent = STYLE;
|
|
110
|
+
document.head.appendChild(style);
|
|
111
|
+
}
|
|
112
|
+
this.previousFocus = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
113
|
+
const dialog = document.createElement('dialog');
|
|
114
|
+
this.dialog = dialog;
|
|
115
|
+
dialog.className = 'trd-package-dialog';
|
|
116
|
+
dialog.setAttribute('aria-labelledby', 'trd-package-dialog-title');
|
|
117
|
+
dialog.setAttribute('tabindex', '-1');
|
|
118
|
+
dialog.setAttribute('closedby', 'none');
|
|
119
|
+
dialog.innerHTML = `<h4 id="trd-package-dialog-title" data-title></h4>
|
|
120
|
+
<div data-result role="status" aria-live="polite"><p data-message></p><p data-detail></p></div>
|
|
121
|
+
<progress aria-label="Package change progress"></progress><div data-elapsed></div>
|
|
122
|
+
<div class="trd-package-actions"><button class="btn btn-secondary" data-cancel hidden>Cancel</button>
|
|
123
|
+
<button class="btn btn-primary" data-confirm hidden></button><button class="btn btn-primary" data-close hidden>Close</button></div>`;
|
|
124
|
+
this.field('[data-confirm]').addEventListener('click', () => this.decide(true));
|
|
125
|
+
this.field('[data-cancel]').addEventListener('click', () => this.decide(false));
|
|
126
|
+
this.field('[data-close]').addEventListener('click', () => this.close());
|
|
127
|
+
dialog.addEventListener('cancel', event => {
|
|
128
|
+
event.preventDefault();
|
|
129
|
+
this.dismiss();
|
|
130
|
+
});
|
|
131
|
+
// Keep package-dialog keystrokes out of Tabby's terminal and shortcut handlers.
|
|
132
|
+
dialog.addEventListener('keydown', event => {
|
|
133
|
+
event.stopPropagation();
|
|
134
|
+
if (event.key === 'Escape') {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
this.dismiss();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
dialog.addEventListener('keyup', event => event.stopPropagation());
|
|
140
|
+
document.body.appendChild(dialog);
|
|
141
|
+
dialog.showModal();
|
|
142
|
+
this.startTimer();
|
|
143
|
+
}
|
|
144
|
+
decide(accepted) {
|
|
145
|
+
const resolve = this.confirmation;
|
|
146
|
+
if (!resolve) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
this.confirmation = undefined;
|
|
150
|
+
this.field('[data-confirm]').hidden = true;
|
|
151
|
+
this.field('[data-cancel]').hidden = true;
|
|
152
|
+
resolve(accepted);
|
|
153
|
+
if (!accepted) {
|
|
154
|
+
this.close();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
dismiss() {
|
|
158
|
+
if (this.confirmation) {
|
|
159
|
+
this.decide(false);
|
|
160
|
+
}
|
|
161
|
+
else if (!this.field('[data-close]').hidden) {
|
|
162
|
+
this.close();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
startTimer() {
|
|
166
|
+
this.started = Date.now();
|
|
167
|
+
const tick = () => {
|
|
168
|
+
const seconds = Math.floor((Date.now() - this.started) / 1000);
|
|
169
|
+
this.field('[data-elapsed]').textContent = `Elapsed ${Math.floor(seconds / 60)}:${String(seconds % 60).padStart(2, '0')}`;
|
|
170
|
+
};
|
|
171
|
+
tick();
|
|
172
|
+
this.timer = setInterval(tick, 1000);
|
|
173
|
+
}
|
|
174
|
+
stopTimer() { clearInterval(this.timer); this.timer = undefined; }
|
|
175
|
+
close() {
|
|
176
|
+
this.stopTimer();
|
|
177
|
+
this.dialog?.close();
|
|
178
|
+
this.dialog?.remove();
|
|
179
|
+
this.dialog = null;
|
|
180
|
+
if (this.previousFocus?.isConnected) {
|
|
181
|
+
this.previousFocus.focus();
|
|
182
|
+
}
|
|
183
|
+
this.previousFocus = null;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
exports.PackageChangeDialog = PackageChangeDialog;
|
package/dist/rdcleanpath.js
CHANGED
|
@@ -527,6 +527,8 @@ async function handshake(openUpstream, destination, pcb, x224Request, legacyTls,
|
|
|
527
527
|
}
|
|
528
528
|
const raw = await openUpstream(destination, signal);
|
|
529
529
|
track(raw);
|
|
530
|
+
// What this handshake opened, in order, for destroyAll if it fails.
|
|
531
|
+
const opened = [raw];
|
|
530
532
|
try {
|
|
531
533
|
if (gone()) {
|
|
532
534
|
throw new Error('the client left');
|
|
@@ -556,6 +558,7 @@ async function handshake(openUpstream, destination, pcb, x224Request, legacyTls,
|
|
|
556
558
|
...name ? { host: name, ca: (0, authorities_1.certificateAuthorities)(), ...(0, net_1.isIP)(name) ? {} : { servername: name } } : {},
|
|
557
559
|
...legacyTls ? KEY_ENCIPHERMENT_ONLY_TLS : {}, ...options.tls,
|
|
558
560
|
});
|
|
561
|
+
opened.push(upstream);
|
|
559
562
|
track(upstream);
|
|
560
563
|
// An error after the one the handshake below waits for (the stream under it failing as the socket is destroyed,
|
|
561
564
|
// which a TLS socket over a stream passes on as its own) would have no listener until the relay is up, and an
|
|
@@ -571,10 +574,22 @@ async function handshake(openUpstream, destination, pcb, x224Request, legacyTls,
|
|
|
571
574
|
return { raw, upstream, x224 };
|
|
572
575
|
}
|
|
573
576
|
catch (e) {
|
|
574
|
-
|
|
577
|
+
destroyAll(opened);
|
|
575
578
|
throw e;
|
|
576
579
|
}
|
|
577
580
|
}
|
|
581
|
+
/**
|
|
582
|
+
* Destroys streams opened one over the other (the stream to the server, then TLS over it), the last opened first. TLS
|
|
583
|
+
* over a socket works inside the socket's own handle (Node's TLSWrap), and a socket destroyed under TLS still using it
|
|
584
|
+
* frees what TLS then reads: right after the handshake, where a certificate refused at once ends the connection, that
|
|
585
|
+
* crashed Tabby's window (a segmentation fault, in Node as in Electron). TLS destroyed first is done with the socket
|
|
586
|
+
* before the socket goes. (Over a stream of JavaScript, an SSH channel or a gateway's tunnel, the order did no harm.)
|
|
587
|
+
*/
|
|
588
|
+
function destroyAll(streams) {
|
|
589
|
+
for (const stream of [...streams].reverse()) {
|
|
590
|
+
stream.destroy();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
578
593
|
/** Whether `given` is the secret, compared in a time that doesn't tell how much of it was right. */
|
|
579
594
|
function sameSecret(given, secret) {
|
|
580
595
|
const bytes = Buffer.from(given);
|
|
@@ -666,9 +681,7 @@ async function startRDCleanPathProxy(openUpstream, checkCertificate, log = () =>
|
|
|
666
681
|
closed = true;
|
|
667
682
|
leaving.abort();
|
|
668
683
|
clearTimeout(deadline);
|
|
669
|
-
|
|
670
|
-
stream.destroy();
|
|
671
|
-
}
|
|
684
|
+
destroyAll(streams);
|
|
672
685
|
// What was handed to this connection went with it: the next starts with all the room.
|
|
673
686
|
if (flowClient === ws) {
|
|
674
687
|
flowClient = null;
|
package/dist/rdpFile.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.MICROPHONE_NOTE = void 0;
|
|
3
|
+
exports.MAX_RDP_FILE = exports.MICROPHONE_NOTE = void 0;
|
|
4
4
|
exports.parseRdpFile = parseRdpFile;
|
|
5
5
|
/**
|
|
6
6
|
* The note for a file that asks for the microphone: not applied while the setting is off. With it on, every desktop
|
|
@@ -53,6 +53,11 @@ function decode(data) {
|
|
|
53
53
|
}
|
|
54
54
|
return b.toString('utf8');
|
|
55
55
|
}
|
|
56
|
+
/**
|
|
57
|
+
* The most an .rdp file may be: one Remote Desktop Connection saves is a few KB, a signed one a few more. A larger one is
|
|
58
|
+
* refused before it is read: what it holds would be parsed on the thread that runs Tabby's window.
|
|
59
|
+
*/
|
|
60
|
+
exports.MAX_RDP_FILE = 1024 * 1024;
|
|
56
61
|
/** The connection a .rdp file describes, or null without a usable `full address`. */
|
|
57
62
|
function parseRdpFile(data) {
|
|
58
63
|
const settings = new Map();
|
|
@@ -60,9 +65,13 @@ function parseRdpFile(data) {
|
|
|
60
65
|
// twice, differently, gets the narrower of the two rather than whichever line comes last.
|
|
61
66
|
let clipboardOff = false;
|
|
62
67
|
for (const line of decode(data).split(/\r?\n/)) {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
// The name up to the first colon, trimmed after: a pattern with room for spaces on both sides of the name (as
|
|
69
|
+
// `\s*([^:]+?)\s*:` had) tries every way of splitting a line of spaces without a colon before it fails, in time
|
|
70
|
+
// that grows with the cube of its length, and a line of a few thousand spaces held up Tabby's window for seconds
|
|
71
|
+
// (security review, finding 2). This one takes a line's length, whatever it holds.
|
|
72
|
+
const m = /^([^:]*):\s*([sib])\s*:(.*)$/i.exec(line);
|
|
73
|
+
const key = m?.[1].trim().toLowerCase();
|
|
74
|
+
if (m && key) {
|
|
66
75
|
const value = m[3].trim();
|
|
67
76
|
settings.set(key, value);
|
|
68
77
|
clipboardOff ||= key === 'redirectclipboard' && /^[+-]?\d+$/.test(value) && Number(value) === 0;
|
package/dist/rdpProfile.js
CHANGED
|
@@ -124,7 +124,7 @@ exports.RDPTabRecovery = RDPTabRecovery = __decorate([
|
|
|
124
124
|
// ---- profile settings --------------------------------------------------------------------------
|
|
125
125
|
/**
|
|
126
126
|
* The profile's own fields in Tabby's profile editor (name, group, icon and color are Tabby's). Plain DOM like the
|
|
127
|
-
* rest of the plugin
|
|
127
|
+
* rest of the plugin. Address and gateway edits stay in the form until Save validates both of them.
|
|
128
128
|
*/
|
|
129
129
|
let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
130
130
|
constructor(element, injector) {
|
|
@@ -188,6 +188,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
188
188
|
Applies on the next connection.</div>
|
|
189
189
|
</div>`;
|
|
190
190
|
const field = (name) => root.querySelector(`[name="${name}"]`);
|
|
191
|
+
this.connectionInputs = { address: field('address'), gateway: field('gateway') };
|
|
191
192
|
field('address').value = o.host ? (0, desktopForm_1.formatAddress)(o.host, o.port || 3389) : '';
|
|
192
193
|
field('kind').value = o.kind === 'gnome' || o.kind === 'xrdp' ? o.kind : 'windows';
|
|
193
194
|
field('username').value = o.username ?? '';
|
|
@@ -196,10 +197,6 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
196
197
|
const text = field('address').value;
|
|
197
198
|
const address = (0, desktopForm_1.parseAddress)(text);
|
|
198
199
|
root.querySelector('[data-for=address]').textContent = text.trim() && !address ? 'This doesn\'t look like an address.' : '';
|
|
199
|
-
if (address) {
|
|
200
|
-
o.host = address.host;
|
|
201
|
-
o.port = address.port;
|
|
202
|
-
}
|
|
203
200
|
});
|
|
204
201
|
field('kind').addEventListener('change', () => {
|
|
205
202
|
const kind = field('kind').value;
|
|
@@ -227,12 +224,12 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
227
224
|
ownAccount();
|
|
228
225
|
field('username').addEventListener('input', () => { o.username = field('username').value.trim(); });
|
|
229
226
|
field('domain').addEventListener('input', () => { o.domain = field('domain').value.trim(); });
|
|
230
|
-
// The gateway, and its account
|
|
227
|
+
// The gateway, and its separate account or sign-in prompt.
|
|
231
228
|
const gateway = field('gateway');
|
|
232
229
|
const gatewayAccount = field('gatewayAccount');
|
|
233
230
|
gateway.value = o.gateway ?? '';
|
|
234
|
-
gatewayAccount.append(new Option('The same as the desktop\'s', ''), ...accounts.map(a => new Option(label(a), a.id)));
|
|
235
|
-
if (o.gatewayAccount && !accounts.some(a => a.id === o.gatewayAccount)) {
|
|
231
|
+
gatewayAccount.append(new Option('The same as the desktop\'s', ''), new Option('Ask for a gateway account when connecting', accounts_1.ASK_GATEWAY_ACCOUNT), ...accounts.map(a => new Option(label(a), a.id)));
|
|
232
|
+
if (o.gatewayAccount && o.gatewayAccount !== accounts_1.ASK_GATEWAY_ACCOUNT && !accounts.some(a => a.id === o.gatewayAccount)) {
|
|
236
233
|
gatewayAccount.append(new Option('(a saved account that no longer exists)', o.gatewayAccount));
|
|
237
234
|
}
|
|
238
235
|
gatewayAccount.value = o.gatewayAccount ?? '';
|
|
@@ -241,10 +238,7 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
241
238
|
root.querySelector('[data-for=gateway]').textContent = text && !(0, gateway_1.parseGateway)(text) ? 'A name or address, with :port if it isn\'t 443.' : '';
|
|
242
239
|
root.querySelector('[data-gateway-account]').hidden = !text || gatewayAccount.options.length < 2;
|
|
243
240
|
};
|
|
244
|
-
gateway.addEventListener('input',
|
|
245
|
-
o.gateway = gateway.value.trim();
|
|
246
|
-
gatewayChanged();
|
|
247
|
-
});
|
|
241
|
+
gateway.addEventListener('input', gatewayChanged);
|
|
248
242
|
gatewayAccount.addEventListener('change', () => { o.gatewayAccount = gatewayAccount.value; });
|
|
249
243
|
gatewayChanged();
|
|
250
244
|
// Its own clipboard sharing, or the setting's (named, as it is now).
|
|
@@ -265,10 +259,45 @@ let RDPProfileSettingsComponent = class RDPProfileSettingsComponent {
|
|
|
265
259
|
}
|
|
266
260
|
save() {
|
|
267
261
|
const o = this.profile.options;
|
|
262
|
+
if (this.connectionInputs) {
|
|
263
|
+
const { address: addressInput, gateway: gatewayInput } = this.connectionInputs;
|
|
264
|
+
const addressText = addressInput.value.trim();
|
|
265
|
+
const address = (0, desktopForm_1.parseAddress)(addressText);
|
|
266
|
+
const gatewayText = gatewayInput.value.trim();
|
|
267
|
+
// Empty addresses are useful in group/type defaults. They clear the destination instead of retaining it.
|
|
268
|
+
const addressError = addressText && !address ? 'This doesn\'t look like an address.' : '';
|
|
269
|
+
const gatewayError = gatewayText && !(0, gateway_1.parseGateway)(gatewayText) ? 'A name or address, with :port if it isn\'t 443.' : '';
|
|
270
|
+
const root = this.element.nativeElement;
|
|
271
|
+
root.querySelector('[data-for=address]').textContent = addressError;
|
|
272
|
+
root.querySelector('[data-for=gateway]').textContent = gatewayError;
|
|
273
|
+
if (addressError || gatewayError) {
|
|
274
|
+
;
|
|
275
|
+
(addressError ? addressInput : gatewayInput).focus();
|
|
276
|
+
// Tabby invokes this synchronous hook before cleaning the proxy and closing the modal. Throwing
|
|
277
|
+
// stops that Save operation; a return value would be ignored by the host.
|
|
278
|
+
throw new Error(`Cannot save this RDP profile: ${addressError || gatewayError}`);
|
|
279
|
+
}
|
|
280
|
+
// Commit the two connection fields together only after both drafts pass validation.
|
|
281
|
+
o.host = address?.host ?? '';
|
|
282
|
+
// A blank destination carries no port edit: keep custom/inherited ports in group and type defaults.
|
|
283
|
+
if (address) {
|
|
284
|
+
o.port = address.port;
|
|
285
|
+
}
|
|
286
|
+
o.gateway = gatewayText;
|
|
287
|
+
}
|
|
268
288
|
const { id, account, via, gateway } = this.before;
|
|
269
289
|
if (!id) {
|
|
270
290
|
return;
|
|
271
291
|
}
|
|
292
|
+
if (!o.host) {
|
|
293
|
+
// A saved profile's address cleared: the desktop it led to is gone from it, as when the profile is removed
|
|
294
|
+
// (see RDPProfilesService.deleteProfile). Not for the type's or a group's defaults, which have no id of
|
|
295
|
+
// their own (an empty address is useful there): profiles that name that address keep what is saved for it.
|
|
296
|
+
if (!via && this.profile.id) {
|
|
297
|
+
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop(id, true, `"${this.profile.name}"`).catch(() => null);
|
|
298
|
+
}
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
272
301
|
if ((o.via ?? '') !== via) {
|
|
273
302
|
// Reached another way, it is another desktop. A direct one's saved passwords (through any gateway, the ones
|
|
274
303
|
// it had before included) and certificate were its own; one behind a host shares them with that host's
|
|
@@ -374,8 +403,9 @@ let RDPProfilesService = class RDPProfilesService extends tabby_core_1.QuickConn
|
|
|
374
403
|
}
|
|
375
404
|
deleteProfile(profile) {
|
|
376
405
|
// A direct desktop's saved passwords (through any gateway: one it went through before too) and certificate are
|
|
377
|
-
// its own; one behind a host shares them with that host's desktop at the same address.
|
|
378
|
-
|
|
406
|
+
// its own; one behind a host shares them with that host's desktop at the same address. A profile without an
|
|
407
|
+
// address of its own leads to no desktop of its own: desktopIdOf would name 127.0.0.1:3389 for it.
|
|
408
|
+
if (!profile.options.via && (0, desktops_1.configText)(profile.options.host)) {
|
|
379
409
|
this.injector.get(desktop_service_1.RemoteDesktopService).forgetDesktop((0, desktops_1.desktopIdOf)(profile.options), true, `"${profile.name}"`).catch(() => null);
|
|
380
410
|
}
|
|
381
411
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.renderReleaseSettings = renderReleaseSettings;
|
|
4
|
+
const help_1 = require("./help");
|
|
5
|
+
/** Rebuilt from manager state; every package mutation stays behind the manager's confirmation and validation. */
|
|
6
|
+
function renderReleaseSettings(root, releases, actions) {
|
|
7
|
+
const oldSelection = root.dataset.releaseChannel === releases.state.channel ? root.querySelector('[data-version]')?.value : undefined;
|
|
8
|
+
root.dataset.releaseChannel = releases.state.channel;
|
|
9
|
+
const selected = releases.versions.some(p => p.version === oldSelection) ? oldSelection : releases.recommended ?? '';
|
|
10
|
+
const blocked = releases.busy || releases.checking || !!releases.pending || releases.removed;
|
|
11
|
+
const disable = (condition) => condition ? 'disabled' : '';
|
|
12
|
+
const reason = selected ? releases.reason(selected) : null;
|
|
13
|
+
const stable = releases.catalog?.stable;
|
|
14
|
+
const previous = releases.state.previous;
|
|
15
|
+
const status = releases.checking ? 'Checking…' : releases.checkedAt !== null ? `Last checked ${new Date(releases.checkedAt).toLocaleString()}.` : 'Not checked';
|
|
16
|
+
root.innerHTML = `
|
|
17
|
+
<div class="trd-release-summary">
|
|
18
|
+
<div><span class="trd-sub">Running version</span><h4>tabby-rdp ${(0, help_1.esc)(releases.running)}</h4></div>
|
|
19
|
+
<button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.running)}">Release notes</button>
|
|
20
|
+
</div>
|
|
21
|
+
${releases.pending ? `<div class="trd-release-notice" role="status"><b>Restart required to use ${(0, help_1.esc)(releases.pending)}.</b> Close remote desktops and restart Tabby.</div>` : ''}
|
|
22
|
+
${releases.removed ? '<div class="trd-release-notice" role="status"><b>Plugin uninstalled.</b> Close remote desktops and restart Tabby.</div>' : ''}
|
|
23
|
+
${releases.notice ? `<p role="status">${(0, help_1.esc)(releases.notice)}</p>` : ''}
|
|
24
|
+
${releases.storageWarning ? `<p role="alert">${(0, help_1.esc)(releases.storageWarning)}</p>` : ''}
|
|
25
|
+
<div class="form-line">
|
|
26
|
+
<div class="header"><label class="title" for="trd-release-channel">Release channel</label><div class="description">Preview includes beta releases and release candidates.</div></div>
|
|
27
|
+
<select id="trd-release-channel" class="form-control" data-channel ${disable(blocked)}><option value="stable" ${releases.state.channel === 'stable' ? 'selected' : ''}>Stable</option><option value="preview" ${releases.state.channel === 'preview' ? 'selected' : ''}>Preview</option></select>
|
|
28
|
+
</div>
|
|
29
|
+
<label class="trd-release-pause"><input type="checkbox" data-pause ${disable(releases.busy)} ${!releases.state.paused ? 'checked' : ''}> Check for updates automatically</label>
|
|
30
|
+
<div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-check ${disable(releases.checking || releases.busy)}>Check now</button><span class="trd-sub" role="status">${(0, help_1.esc)(status)}</span></div>
|
|
31
|
+
${releases.error ? `<p class="trd-release-error" role="alert">${(0, help_1.esc)(releases.error)}${releases.catalog ? ' Showing the last successful check.' : ''}</p>` : ''}
|
|
32
|
+
${releases.state.channel === 'preview' && releases.catalog && !releases.catalog.preview ? '<p>No preview releases available.</p>' : ''}
|
|
33
|
+
${releases.recommended ? `<p>Latest: <b>${(0, help_1.esc)(releases.recommended)}</b> <button class="btn btn-link btn-sm" data-release-notes="${(0, help_1.esc)(releases.recommended)}">Release notes</button></p>` : ''}
|
|
34
|
+
<h5><label for="trd-release-version">Version</label></h5>
|
|
35
|
+
<div class="trd-release-actions">
|
|
36
|
+
<select id="trd-release-version" class="form-control" data-version ${disable(blocked || !releases.versions.length)}>
|
|
37
|
+
${!releases.versions.length ? '<option>No versions loaded</option>' : releases.versions.map(p => {
|
|
38
|
+
const why = releases.reason(p.version);
|
|
39
|
+
return `<option value="${(0, help_1.esc)(p.version)}" ${p.version === selected ? 'selected' : ''} ${disable(!!why)}>${(0, help_1.esc)(p.version)}${why ? ` — ${(0, help_1.esc)(why)}` : ''}</option>`;
|
|
40
|
+
}).join('')}
|
|
41
|
+
</select>
|
|
42
|
+
<button class="btn btn-primary btn-sm" data-install ${disable(blocked || !!releases.error || !selected || !!reason || selected === releases.running)}>Install selected version…</button>
|
|
43
|
+
<button class="btn btn-link btn-sm" data-selected-notes ${disable(!selected)}>Release notes</button>
|
|
44
|
+
</div>
|
|
45
|
+
<p class="trd-sub" data-version-reason ${reason ? '' : 'hidden'}>${(0, help_1.esc)(reason ?? '')}</p>
|
|
46
|
+
<div class="trd-release-actions">
|
|
47
|
+
${stable && stable !== releases.running ? `<button class="btn btn-secondary btn-sm" data-stable ${disable(blocked || !!releases.error || !!releases.reason(stable))}>Return to stable ${(0, help_1.esc)(stable)}…</button>` : ''}
|
|
48
|
+
<button class="btn btn-secondary btn-sm" data-restore ${disable(blocked || !!releases.error || !previous || !!releases.reason(previous) || previous === releases.running)}>Restore previous version${previous ? ` (${(0, help_1.esc)(previous)})` : ''}…</button>
|
|
49
|
+
</div>
|
|
50
|
+
${!previous ? '<p class="trd-sub">No previous version saved.</p>' : releases.reason(previous) ? `<p class="trd-sub">Previous version unavailable: ${(0, help_1.esc)(releases.reason(previous))}</p>` : ''}
|
|
51
|
+
<h5>Disable or uninstall</h5>
|
|
52
|
+
<p class="trd-sub">Saved connections, settings, passwords and remote setup are kept.</p>
|
|
53
|
+
<div class="trd-release-actions"><button class="btn btn-secondary btn-sm" data-plugins>Manage in Tabby…</button><button class="btn btn-outline-danger btn-sm" data-uninstall ${disable(blocked)}>Uninstall plugin…</button><a href="#" data-recovery>Cleanup instructions</a></div>`;
|
|
54
|
+
root.querySelector('[data-channel]').addEventListener('change', e => {
|
|
55
|
+
void releases.setChannel(e.target.value);
|
|
56
|
+
});
|
|
57
|
+
root.querySelector('[data-pause]').addEventListener('change', e => { void releases.setPaused(!e.target.checked); });
|
|
58
|
+
root.querySelector('[data-check]').addEventListener('click', () => { void releases.refreshCatalog(true); });
|
|
59
|
+
root.querySelector('[data-version]').addEventListener('change', () => renderReleaseSettings(root, releases, actions));
|
|
60
|
+
root.querySelector('[data-install]').addEventListener('click', () => { void releases.install(selected); });
|
|
61
|
+
root.querySelector('[data-stable]')?.addEventListener('click', () => { if (stable) {
|
|
62
|
+
void releases.install(stable, true);
|
|
63
|
+
} });
|
|
64
|
+
root.querySelector('[data-restore]').addEventListener('click', () => { if (previous) {
|
|
65
|
+
void releases.install(previous);
|
|
66
|
+
} });
|
|
67
|
+
root.querySelector('[data-uninstall]').addEventListener('click', () => { void releases.uninstall(); });
|
|
68
|
+
root.querySelector('[data-plugins]').addEventListener('click', actions.plugins);
|
|
69
|
+
root.querySelector('[data-recovery]').addEventListener('click', e => { e.preventDefault(); actions.link(`${help_1.TROUBLESHOOTING_URL}#recovery`); });
|
|
70
|
+
root.querySelector('[data-selected-notes]').addEventListener('click', () => actions.notes(selected));
|
|
71
|
+
root.querySelectorAll('[data-release-notes]').forEach(el => el.addEventListener('click', () => actions.notes(el.dataset.releaseNotes)));
|
|
72
|
+
}
|