tabby-rdp 0.3.4 → 0.4.0
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 +38 -0
- package/README.md +27 -4
- package/dist/accountForm.js +89 -0
- package/dist/accountForm.js.map +1 -0
- package/dist/accounts.js +39 -0
- package/dist/accounts.js.map +1 -0
- package/dist/desktop.service.js +200 -16
- package/dist/desktop.service.js.map +1 -1
- package/dist/desktopForm.js +38 -2
- package/dist/desktopForm.js.map +1 -1
- package/dist/desktops.js +1 -0
- package/dist/desktops.js.map +1 -1
- package/dist/help.js +15 -1
- package/dist/help.js.map +1 -1
- package/dist/index.js +24 -5
- package/dist/index.js.map +1 -1
- package/dist/keyboard.js +3 -3
- package/dist/keyboard.js.map +1 -1
- package/dist/rdcleanpath.js +1 -3
- package/dist/rdcleanpath.js.map +1 -1
- package/dist/rdpProfile.js +33 -5
- package/dist/rdpProfile.js.map +1 -1
- package/dist/settingsPage.js +537 -56
- package/dist/settingsPage.js.map +1 -1
- package/dist/signin.js +140 -21
- package/dist/signin.js.map +1 -1
- package/dist/ui.js +5 -2
- package/dist/ui.js.map +1 -1
- package/dist/updates.js +9 -0
- package/dist/updates.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
**New**
|
|
6
|
+
- **Saved accounts** ([#11](https://github.com/meanaverage/tabby-rdp/issues/11)): a user name and domain under a
|
|
7
|
+
name, with the password kept once, for desktops that share an account. Chosen in a remote desktop profile's
|
|
8
|
+
**Account** field or in the form for a desktop behind an SSH host (**New account…** there adds one), or for a whole
|
|
9
|
+
profile group through the group's defaults. Listed, added, edited and removed in **Settings › Remote Desktop ›
|
|
10
|
+
Accounts**, which names the desktops using each account. A password a server refuses is asked for again and saved
|
|
11
|
+
for every desktop; **Sign in again…** asks anew.
|
|
12
|
+
- **Passwords in Tabby's Vault** when it is enabled, as Tabby keeps SSH passwords: encrypted, and carried by config
|
|
13
|
+
sync. The system keychain as before otherwise, and still read for what was saved before the Vault was turned on.
|
|
14
|
+
- **Configurable shortcuts** ([#15](https://github.com/meanaverage/tabby-rdp/issues/15)): view only, a screenshot,
|
|
15
|
+
Ctrl+Alt+Del, typing into all desktops of a tab, pasting to all of them, the connection status and Disconnect join
|
|
16
|
+
the desktop/console switch as Tabby hotkeys, set on the settings page in the look of Tabby's Hotkeys page (or
|
|
17
|
+
there). A key another hotkey already has is refused, and collisions made elsewhere are shown.
|
|
18
|
+
- **The settings page in tabs** — Getting started, Settings, Overlay, Desktops, Accounts, Troubleshooting — as
|
|
19
|
+
Tabby's Profiles & connections page is, with the settings grouped (Picture, Sound, Keyboard, SSH hosts, Updates)
|
|
20
|
+
and the detail behind ⓘ tooltips. The Desktops tab lists remote desktop profiles as well as desktops behind SSH
|
|
21
|
+
hosts, and adds, edits and removes both; profiles the plugin makes go into a "Remote desktops" group.
|
|
22
|
+
- **A clearer reason when a connection fails** before it is up ("10.3.0.5:3389 did not answer"), in place of
|
|
23
|
+
IronRDP's "general error (code 1)".
|
|
24
|
+
- The **name overlay preview** says which desktop resolution it stands for, and lets you pick one
|
|
25
|
+
([#16](https://github.com/meanaverage/tabby-rdp/issues/16)).
|
|
26
|
+
|
|
27
|
+
**Fixed**
|
|
28
|
+
- **The window could not be moved by its tab bar while a dialog was open**
|
|
29
|
+
([#19](https://github.com/meanaverage/tabby-rdp/issues/19)), the plugin's and Tabby's alike. Each dialog keeps a
|
|
30
|
+
strip the height of the tab bar draggable. Submitted to Tabby as well.
|
|
31
|
+
- **A stale update notice** in a Tabby left open across several releases
|
|
32
|
+
([#17](https://github.com/meanaverage/tabby-rdp/issues/17)): opening the settings page with an update known asks
|
|
33
|
+
npm again.
|
|
34
|
+
- Opening the settings page no longer prompts for the Vault passphrase.
|
|
35
|
+
|
|
36
|
+
**Docs**
|
|
37
|
+
- README: saved accounts, shortcuts, and where passwords are kept. Tabby's Vault page labels the plugin's secrets
|
|
38
|
+
generically until Tabby ships [#11765](https://github.com/Eugeny/tabby/pull/11765)
|
|
39
|
+
([#18](https://github.com/meanaverage/tabby-rdp/issues/18)).
|
|
40
|
+
|
|
3
41
|
## 0.3.4
|
|
4
42
|
|
|
5
43
|
**Fixed**
|
package/README.md
CHANGED
|
@@ -40,8 +40,8 @@ nothing is opened on the network, and nothing has to be installed on the remote
|
|
|
40
40
|
- **`desk`:** type `desk` in the console and the same shell, with its history and running programs, moves into a
|
|
41
41
|
terminal on the desktop.
|
|
42
42
|
|
|
43
|
-
Everything is in **Settings › Remote Desktop
|
|
44
|
-
the desktops and certificates the plugin keeps, and troubleshooting.
|
|
43
|
+
Everything is in **Settings › Remote Desktop**, in tabs: getting started and the shortcuts, the settings, the name
|
|
44
|
+
overlay, the desktops and certificates the plugin keeps, saved accounts, and troubleshooting.
|
|
45
45
|
|
|
46
46
|
The remote desktop itself is [IronRDP](https://github.com/Devolutions/IronRDP)'s web client, running inside Tabby.
|
|
47
47
|
Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([below](#built-on-ironrdp)).
|
|
@@ -53,6 +53,7 @@ Making it work with GNOME took fixes in IronRDP, which we contribute upstream ([
|
|
|
53
53
|
- [Features](#features)
|
|
54
54
|
- [Windows and other desktops behind a host](#windows-and-other-desktops-behind-a-host)
|
|
55
55
|
- [Remote desktop profiles](#remote-desktop-profiles)
|
|
56
|
+
- [Saved accounts](#saved-accounts)
|
|
56
57
|
- [Several desktops in one tab](#several-desktops-in-one-tab)
|
|
57
58
|
- [VMs on a host](#vms-on-a-host)
|
|
58
59
|
- [desk: the console on the desktop](#desk-the-console-on-the-desktop)
|
|
@@ -120,7 +121,8 @@ connecting again.
|
|
|
120
121
|
| **Screenshots** | **Save a screenshot** in the desktop's menus saves the remote screen at its full resolution as a PNG in Downloads, and copies it to the clipboard. |
|
|
121
122
|
| **Connection status** | Optionally, a small line in the corner of the desktop with the throughput each way, frames per second, the SSH round trip, and how it is connected (desktop, host, resolution, graphics mode, sharpness). |
|
|
122
123
|
| **Reconnecting** | After sleep or a network change, the desktop reconnects by itself once the connection is back. |
|
|
123
|
-
| **Sign-in** | GNOME desktops need none: the plugin manages their credentials. Windows and xrdp desktops ask for the account once and can remember it in the system keychain; **Sign in again…** in the menu replaces it. |
|
|
124
|
+
| **Sign-in** | GNOME desktops need none: the plugin manages their credentials. Windows and xrdp desktops ask for the account once and can remember it, in Tabby's Vault when that is on and in the system keychain otherwise; **Sign in again…** in the menu replaces it. Desktops that share an account can use a [saved account](#saved-accounts), whose password is kept once. |
|
|
125
|
+
| **Shortcuts** | Switching between the desktop and the console, view only, a screenshot, Ctrl+Alt+Del, typing into or pasting to all desktops of a tab, the connection status and Disconnect each have a hotkey, set on the settings page or in Tabby's Hotkeys. They work while the desktop has the keyboard; a key another hotkey has is refused. |
|
|
124
126
|
| **Certificates** | GNOME desktops accept only the certificate the plugin made for them. Windows and xrdp desktops remember theirs on first use and ask before accepting a different one. |
|
|
125
127
|
|
|
126
128
|
<p align="center"><img src="docs/images/files.png" width="760" alt="A file copied on the remote desktop, offered for saving"></p>
|
|
@@ -217,6 +219,25 @@ A direct connection is a plain TCP connection from this computer, encrypted with
|
|
|
217
219
|
server's certificate is remembered on the first connection and checked on every later one, as for desktops behind a
|
|
218
220
|
host (above).
|
|
219
221
|
|
|
222
|
+
## Saved accounts
|
|
223
|
+
|
|
224
|
+
Several desktops often share one account: a domain admin, a lab user. A saved account keeps that user name (and
|
|
225
|
+
domain) under a name, with its password stored once, so when the password changes it is entered once for all of them.
|
|
226
|
+
|
|
227
|
+
**Settings › Remote Desktop › Accounts** lists them, with the desktops that use each one (click a name to open its
|
|
228
|
+
settings), and adds, edits and removes them. A desktop picks its account in its profile's **Account** field, or in the
|
|
229
|
+
form that adds a desktop behind an SSH host; **New account…** there adds one on the spot. A profile group's defaults
|
|
230
|
+
for **Remote desktop (RDP)** can pick an account for every profile in the group (**Profiles & connections**, the group's
|
|
231
|
+
menu), which is how to give a whole group of servers one sign-in.
|
|
232
|
+
|
|
233
|
+
Signing in with a saved account that has no password yet, or whose password a server refused, asks for it, and the
|
|
234
|
+
form saves what you enter as the account's new password for every desktop. A refused password is not dropped by
|
|
235
|
+
itself: one server refusing it doesn't make it wrong for the others. **Sign in again…** on such a desktop asks anew.
|
|
236
|
+
|
|
237
|
+
The accounts' names and user names are in Tabby's config (`remoteDesktop.accounts`); the passwords are in Tabby's
|
|
238
|
+
Vault when it is enabled (encrypted, and carried by Tabby's config sync), else in the system keychain. Removing an
|
|
239
|
+
account removes its password and sets its desktops back to asking.
|
|
240
|
+
|
|
220
241
|
## Several desktops in one tab
|
|
221
242
|
|
|
222
243
|
Split a tab (Tabby's **Split** in the tab's menu, or drag one tab onto another) and each pane can show a desktop: one
|
|
@@ -285,12 +306,14 @@ They are stored in Tabby's config under `remoteDesktop`:
|
|
|
285
306
|
| Microphone (`microphone`) | Off | Send your microphone while an app on the remote desktop records. Applies on the next connection. |
|
|
286
307
|
| Show connection status (`connectionStatus`) | Off | The indicator in the desktop's corner; fades when the pointer comes near. |
|
|
287
308
|
| Mac shortcuts (`macShortcuts`) | On | macOS. Off: ⌘ is the Windows key. |
|
|
309
|
+
| Shortcuts (`hotkeys.remote-desktop-*`) | ⌘⇧G / Ctrl+Shift+G for the switch; the rest unbound | Tabby's hotkeys: the switch between desktop and console, view only, screenshot, Ctrl+Alt+Del, type into all, paste to all, connection status, disconnect. Changed on the settings page (Getting started › Shortcuts) or in Settings › Hotkeys. |
|
|
310
|
+
| Saved accounts (`accounts`) | None | `[{ id, name, username, domain }]`; see [Saved accounts](#saved-accounts). The passwords are not in the config. |
|
|
288
311
|
| Bring the console along with `desk` (`desk`) | Off | Installs `desk` and a login line on each machine you open a desktop on; applies on the next connection there. |
|
|
289
312
|
| Session backend (`sessionBackend`) | `native` | For `desk`: `native` (trd-pty) or `tmux`. Config file only. |
|
|
290
313
|
| Find virtual machines on SSH hosts (`discoverVMs`) | On | Lists a host's libvirt VMs with a desktop in its menu ([VMs on a host](#vms-on-a-host)). |
|
|
291
314
|
| Shut down VMs it started (`shutDownIdle`) | Never | 5, 15 or 60: minutes without a desktop open after which a VM the plugin started is shut down again ([VMs on a host](#vms-on-a-host)). |
|
|
292
315
|
| Tell me about new versions (`checkUpdates`) | On | Once a day, asks npm for the latest tabby-rdp (nothing else is sent), and says so in a note, the menus and the settings page when there's a newer one: Tabby itself shows plugin upgrades only on its Plugins page. |
|
|
293
|
-
| Desktop name overlay (`osd`) | When it helps | `show` (`auto`, `always`, `off`), `font`, `size`, `position`, `color` (empty: white) and `seconds`; the settings page previews it. |
|
|
316
|
+
| Desktop name overlay (`osd`) | When it helps | `show` (`auto`, `always`, `off`), `font`, `size`, `position`, `color` (empty: white) and `seconds`; the settings page previews it, at a resolution you pick. |
|
|
294
317
|
|
|
295
318
|
## Requirements
|
|
296
319
|
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.STYLE = exports.NEW_ACCOUNT = void 0;
|
|
4
|
+
exports.newAccountOption = newAccountOption;
|
|
5
|
+
exports.NEW_ACCOUNT = '__new__';
|
|
6
|
+
exports.STYLE = `
|
|
7
|
+
.trd-new-account { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px; padding: 10px; border-radius: 6px;
|
|
8
|
+
border: 1px solid rgba(128, 128, 128, 0.35); }
|
|
9
|
+
.trd-new-account .trd-new-account-error { grid-column: 1 / -1; color: #f08080; font-size: 12px; }
|
|
10
|
+
.trd-new-account .trd-new-account-error:empty { display: none; }
|
|
11
|
+
.trd-new-account .trd-new-account-buttons { grid-column: 1 / -1; display: flex; gap: 8px; justify-content: flex-end; }
|
|
12
|
+
`;
|
|
13
|
+
/**
|
|
14
|
+
* Wires an account `<select>` that ends with "New account…": choosing it opens the form under the select; "Add" hands
|
|
15
|
+
* the fields to `save`, which returns the saved account, and the select then offers and shows it. `onChange` is told
|
|
16
|
+
* the chosen id ('' for none) whenever it changes, the new account included.
|
|
17
|
+
*/
|
|
18
|
+
function newAccountOption(select, label, save, onChange) {
|
|
19
|
+
select.append(new Option('New account…', exports.NEW_ACCOUNT));
|
|
20
|
+
let last = select.value;
|
|
21
|
+
let form = null;
|
|
22
|
+
const close = () => {
|
|
23
|
+
form?.remove();
|
|
24
|
+
form = null;
|
|
25
|
+
};
|
|
26
|
+
select.addEventListener('change', () => {
|
|
27
|
+
if (select.value !== exports.NEW_ACCOUNT) {
|
|
28
|
+
close();
|
|
29
|
+
last = select.value;
|
|
30
|
+
onChange(select.value);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (form) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
form = document.createElement('form');
|
|
37
|
+
form.className = 'trd-new-account';
|
|
38
|
+
form.autocomplete = 'off';
|
|
39
|
+
form.innerHTML = `
|
|
40
|
+
<input class="form-control form-control-sm" name="name" placeholder="Name, e.g. Domain A admin" spellcheck="false">
|
|
41
|
+
<input class="form-control form-control-sm" name="username" placeholder="User name, or DOMAIN\\user" spellcheck="false">
|
|
42
|
+
<input class="form-control form-control-sm" name="domain" placeholder="Domain (optional)" spellcheck="false">
|
|
43
|
+
<input class="form-control form-control-sm" name="password" type="password" autocomplete="new-password" placeholder="Password (optional; asked on the first connection)">
|
|
44
|
+
<div class="trd-new-account-error"></div>
|
|
45
|
+
<div class="trd-new-account-buttons">
|
|
46
|
+
<button type="button" class="btn btn-secondary btn-sm" name="cancel">Cancel</button>
|
|
47
|
+
<button type="submit" class="btn btn-primary btn-sm">Add account</button>
|
|
48
|
+
</div>`;
|
|
49
|
+
const field = (name) => form.querySelector(`[name="${name}"]`);
|
|
50
|
+
const error = form.querySelector('.trd-new-account-error');
|
|
51
|
+
// Keys typed in it are its own (the forms around it act on Enter and Escape).
|
|
52
|
+
form.addEventListener('keydown', event => {
|
|
53
|
+
event.stopPropagation();
|
|
54
|
+
if (event.key === 'Escape') {
|
|
55
|
+
event.preventDefault();
|
|
56
|
+
field('cancel').click();
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
field('cancel').addEventListener('click', () => {
|
|
60
|
+
close();
|
|
61
|
+
select.value = last;
|
|
62
|
+
});
|
|
63
|
+
form.addEventListener('submit', async (event) => {
|
|
64
|
+
event.preventDefault();
|
|
65
|
+
event.stopPropagation();
|
|
66
|
+
const username = field('username').value.trim();
|
|
67
|
+
if (!username) {
|
|
68
|
+
error.textContent = 'Give it a user name.';
|
|
69
|
+
field('username').focus();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const account = await save({ name: field('name').value.trim(), username, domain: field('domain').value.trim(), password: field('password').value });
|
|
74
|
+
const option = new Option(label(account), account.id);
|
|
75
|
+
select.insertBefore(option, select.querySelector(`option[value="${exports.NEW_ACCOUNT}"]`));
|
|
76
|
+
select.value = account.id;
|
|
77
|
+
last = account.id;
|
|
78
|
+
close();
|
|
79
|
+
onChange(account.id);
|
|
80
|
+
}
|
|
81
|
+
catch (e) {
|
|
82
|
+
error.textContent = `Couldn't save it: ${e?.message ?? e}`;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
select.insertAdjacentElement('afterend', form);
|
|
86
|
+
field('name').focus();
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=accountForm.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accountForm.js","sourceRoot":"","sources":["../src/accountForm.ts"],"names":[],"mappings":"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"}
|
package/dist/accounts.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.accountsOf = accountsOf;
|
|
4
|
+
exports.newAccountId = newAccountId;
|
|
5
|
+
exports.signInName = signInName;
|
|
6
|
+
exports.accountKey = accountKey;
|
|
7
|
+
/** The usable entries of `remoteDesktop.accounts` (hand-written ones may lack fields). */
|
|
8
|
+
function accountsOf(store) {
|
|
9
|
+
const list = Array.isArray(store?.accounts) ? store.accounts : [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
return list.flatMap((a) => {
|
|
12
|
+
const id = typeof a?.id === 'string' ? a.id.trim() : '';
|
|
13
|
+
const username = typeof a?.username === 'string' ? a.username.trim() : '';
|
|
14
|
+
if (!/^[A-Za-z0-9_-]+$/.test(id) || !username || seen.has(id)) {
|
|
15
|
+
return [];
|
|
16
|
+
}
|
|
17
|
+
seen.add(id);
|
|
18
|
+
const domain = typeof a.domain === 'string' && a.domain.trim() ? a.domain.trim() : undefined;
|
|
19
|
+
return [{ id, name: typeof a.name === 'string' && a.name.trim() ? a.name.trim() : signInName({ username, domain }), username, ...domain ? { domain } : {} }];
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/** A new account's id: nothing in it but that it is unused. */
|
|
23
|
+
function newAccountId(taken) {
|
|
24
|
+
for (;;) {
|
|
25
|
+
const id = Math.random().toString(36).slice(2, 10).padEnd(8, '0');
|
|
26
|
+
if (!taken.some(a => a.id === id)) {
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/** What the account signs in as: DOMAIN\user, or the user name as typed when it names its domain itself. */
|
|
32
|
+
function signInName(account) {
|
|
33
|
+
return account.domain && !/[\\@]/.test(account.username) ? `${account.domain}\\${account.username}` : account.username;
|
|
34
|
+
}
|
|
35
|
+
/** Where the keychain keeps an account's password (next to the desktops' own entries, which are session keys). */
|
|
36
|
+
function accountKey(id) {
|
|
37
|
+
return `account#${id}`;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=accounts.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"accounts.js","sourceRoot":"","sources":["../src/accounts.ts"],"names":[],"mappings":";;AAoBA,gCAaC;AAGD,oCAOC;AAGD,gCAEC;AAGD,gCAEC;AAlCD,0FAA0F;AAC1F,SAAgB,UAAU,CAAE,KAAU;IAClC,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;IACjE,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAA;IAC9B,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,CAAM,EAAkB,EAAE;QAC3C,MAAM,EAAE,GAAG,OAAO,CAAC,EAAE,EAAE,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACvD,MAAM,QAAQ,GAAG,OAAO,CAAC,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QACzE,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,EAAE,CAAC,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC5D,OAAO,EAAE,CAAA;QACb,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAA;QACZ,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,MAAM,KAAK,QAAQ,IAAI,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAA;QAC5F,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;IAChK,CAAC,CAAC,CAAA;AACN,CAAC;AAED,+DAA+D;AAC/D,SAAgB,YAAY,CAAE,KAAqB;IAC/C,SAAS,CAAC;QACN,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,GAAG,CAAC,CAAA;QACjE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;YAChC,OAAO,EAAE,CAAA;QACb,CAAC;IACL,CAAC;AACL,CAAC;AAED,4GAA4G;AAC5G,SAAgB,UAAU,CAAE,OAAkD;IAC1E,OAAO,OAAO,CAAC,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,MAAM,KAAK,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAA;AAC1H,CAAC;AAED,kHAAkH;AAClH,SAAgB,UAAU,CAAE,EAAU;IAClC,OAAO,WAAW,EAAE,EAAE,CAAA;AAC1B,CAAC"}
|
package/dist/desktop.service.js
CHANGED
|
@@ -25,6 +25,8 @@ const audio_1 = require("./audio");
|
|
|
25
25
|
const connectionStatus_1 = require("./connectionStatus");
|
|
26
26
|
const microphone_1 = require("./microphone");
|
|
27
27
|
const desktopForm_1 = require("./desktopForm");
|
|
28
|
+
const accounts_1 = require("./accounts");
|
|
29
|
+
const accountForm_1 = require("./accountForm");
|
|
28
30
|
const fileTransfer_1 = require("./fileTransfer");
|
|
29
31
|
const desktops_1 = require("./desktops");
|
|
30
32
|
const help_1 = require("./help");
|
|
@@ -322,6 +324,11 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
322
324
|
* long as Tabby runs; a Windows host is found out on the first connect there, which costs a failed setup.
|
|
323
325
|
*/
|
|
324
326
|
this.ownDesktops = new Map();
|
|
327
|
+
/**
|
|
328
|
+
* Forgets the saved account of the pane's desktop and connects again, with the sign-in form. For xrdp above all:
|
|
329
|
+
* with a wrong password it shows its own login window rather than refusing, so the form never comes back.
|
|
330
|
+
*/
|
|
331
|
+
this.askAgain = new WeakSet();
|
|
325
332
|
/** VMs found on SSH hosts (see vms.ts), by target key: when, and as desktops. */
|
|
326
333
|
this.vms = new Map();
|
|
327
334
|
this.lastUsed = new Map();
|
|
@@ -389,14 +396,12 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
389
396
|
const spec = this.sessions.get(pane)?.spec;
|
|
390
397
|
return !!spec && this.signsIn(spec);
|
|
391
398
|
}
|
|
392
|
-
/**
|
|
393
|
-
* Forgets the saved account of the pane's desktop and connects again, with the sign-in form. For xrdp above all:
|
|
394
|
-
* with a wrong password it shows its own login window rather than refusing, so the form never comes back.
|
|
395
|
-
*/
|
|
396
399
|
async signInAgain(pane) {
|
|
397
400
|
const session = this.sessions.get(pane);
|
|
398
401
|
if (session && this.signsIn(session.spec)) {
|
|
399
402
|
await (0, signin_1.forgetCredentials)(session.key);
|
|
403
|
+
// With a saved account, the form asks for its password anew (the saved one stays until then).
|
|
404
|
+
this.askAgain.add(pane);
|
|
400
405
|
await this.reopen(pane, session.spec);
|
|
401
406
|
}
|
|
402
407
|
}
|
|
@@ -480,6 +485,106 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
480
485
|
desktopOf(pane) {
|
|
481
486
|
return this.sessions.get(pane)?.spec ?? null;
|
|
482
487
|
}
|
|
488
|
+
/** `remoteDesktop.accounts`: the saved accounts desktops can sign in with (see accounts.ts). */
|
|
489
|
+
accounts() {
|
|
490
|
+
return (0, accounts_1.accountsOf)(this.config.store.remoteDesktop);
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* The desktops that sign in with a saved account: configured ones behind SSH hosts, and RDP profiles (their own
|
|
494
|
+
* choice, or their profile group's default when they make none). Each comes with what opens its editor.
|
|
495
|
+
*/
|
|
496
|
+
accountUses(id) {
|
|
497
|
+
const desktops = this.configuredDesktops().flatMap((d, i) => d.account === id ? [{ name: String(d.name ?? (0, desktops_1.desktopIdOf)(d)), desktopIndex: i }] : []);
|
|
498
|
+
const groups = this.config.store.groups ?? [];
|
|
499
|
+
const inherited = (p) => p.options?.account === undefined && groups.find(g => g.id === p.group)?.defaults?.[targets_1.RDP_PROFILE_TYPE]?.options?.account === id;
|
|
500
|
+
const profiles = (this.config.store.profiles ?? [])
|
|
501
|
+
.filter((p) => p?.type === targets_1.RDP_PROFILE_TYPE && (p.options?.account === id || inherited(p)))
|
|
502
|
+
.map((p) => ({ name: String(p.name ?? p.options?.host ?? ''), profileId: p.id ? String(p.id) : undefined }));
|
|
503
|
+
return [...desktops, ...profiles];
|
|
504
|
+
}
|
|
505
|
+
/**
|
|
506
|
+
* Adds a saved account (no id) or changes one. `password`: the new one for the keychain; undefined keeps what is
|
|
507
|
+
* there. A new user name or domain without a new password drops the old password: it was for the old account.
|
|
508
|
+
* Returns the account's id, and the keychain's complaint if the password couldn't be saved.
|
|
509
|
+
*/
|
|
510
|
+
async saveAccount(account, password) {
|
|
511
|
+
const list = this.accounts();
|
|
512
|
+
const old = list.find(a => a.id === account.id);
|
|
513
|
+
const entry = {
|
|
514
|
+
id: old?.id ?? (0, accounts_1.newAccountId)(list),
|
|
515
|
+
name: account.name.trim() || (0, accounts_1.signInName)(account),
|
|
516
|
+
username: account.username.trim(),
|
|
517
|
+
...account.domain?.trim() ? { domain: account.domain.trim() } : {},
|
|
518
|
+
};
|
|
519
|
+
this.config.store.remoteDesktop.accounts = old ? list.map(a => a === old ? entry : a) : [...list, entry];
|
|
520
|
+
this.config.save();
|
|
521
|
+
this.changed$.next();
|
|
522
|
+
let keychainError;
|
|
523
|
+
if (password !== undefined && password !== '') {
|
|
524
|
+
await (0, signin_1.saveCredentials)((0, accounts_1.accountKey)(entry.id), { username: (0, accounts_1.signInName)(entry), password }).catch(e => { keychainError = String(e?.message ?? e); });
|
|
525
|
+
}
|
|
526
|
+
else if (old && (0, accounts_1.signInName)(old) !== (0, accounts_1.signInName)(entry)) {
|
|
527
|
+
await (0, signin_1.forgetCredentials)((0, accounts_1.accountKey)(entry.id));
|
|
528
|
+
}
|
|
529
|
+
return { id: entry.id, keychainError };
|
|
530
|
+
}
|
|
531
|
+
/** A desktop form's "New account…": saves it and hands it back for the form's list. */
|
|
532
|
+
async addAccountFromForm(input) {
|
|
533
|
+
const { id, keychainError } = await this.saveAccount({ name: input.name, username: input.username, domain: input.domain }, input.password);
|
|
534
|
+
if (keychainError) {
|
|
535
|
+
throw new Error(keychainError);
|
|
536
|
+
}
|
|
537
|
+
return this.accounts().find(a => a.id === id);
|
|
538
|
+
}
|
|
539
|
+
/** Whether a password is saved for the account; 'unknown' while the Vault is locked (it isn't unlocked for this). */
|
|
540
|
+
accountHasPassword(id) {
|
|
541
|
+
return (0, signin_1.hasCredentials)((0, accounts_1.accountKey)(id));
|
|
542
|
+
}
|
|
543
|
+
/** "Remove…" on a saved account: asks first, naming the desktops that use it. Resolves true when removed. */
|
|
544
|
+
async confirmRemoveAccount(id) {
|
|
545
|
+
const account = this.accounts().find(a => a.id === id);
|
|
546
|
+
if (!account) {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const uses = this.accountUses(id);
|
|
550
|
+
const { response } = await this.platform.showMessageBox({
|
|
551
|
+
type: 'warning',
|
|
552
|
+
message: `Remove the account "${account.name}"?`,
|
|
553
|
+
detail: `${(0, accounts_1.signInName)(account)}. Its saved password is removed too. ` + (uses.length
|
|
554
|
+
? `${uses.length === 1 ? 'One desktop uses' : `${uses.length} desktops use`} it and will ask for an account when connecting: ${uses.map(u => u.name).join(', ')}.`
|
|
555
|
+
: 'No desktop uses it.'),
|
|
556
|
+
buttons: ['Remove', 'Keep'],
|
|
557
|
+
defaultId: 1,
|
|
558
|
+
cancelId: 1,
|
|
559
|
+
});
|
|
560
|
+
if (response !== 0) {
|
|
561
|
+
return false;
|
|
562
|
+
}
|
|
563
|
+
await this.removeAccount(id);
|
|
564
|
+
return true;
|
|
565
|
+
}
|
|
566
|
+
/** Removes a saved account and its password; desktops that used it go back to asking. */
|
|
567
|
+
async removeAccount(id) {
|
|
568
|
+
const store = this.config.store.remoteDesktop;
|
|
569
|
+
store.accounts = this.accounts().filter(a => a.id !== id);
|
|
570
|
+
if (this.configuredDesktops().some(d => d.account === id)) {
|
|
571
|
+
store.desktops = this.configuredDesktops().map(d => {
|
|
572
|
+
if (d.account !== id) {
|
|
573
|
+
return d;
|
|
574
|
+
}
|
|
575
|
+
const { account: _account, ...rest } = d;
|
|
576
|
+
return rest;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
for (const profile of this.config.store.profiles ?? []) {
|
|
580
|
+
if (profile?.type === targets_1.RDP_PROFILE_TYPE && profile.options?.account === id) {
|
|
581
|
+
profile.options.account = '';
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
this.config.save();
|
|
585
|
+
this.changed$.next();
|
|
586
|
+
await (0, signin_1.forgetCredentials)((0, accounts_1.accountKey)(id));
|
|
587
|
+
}
|
|
483
588
|
/** `remoteDesktop.desktops`: the desktops configured behind SSH hosts. */
|
|
484
589
|
configuredDesktops() {
|
|
485
590
|
const list = this.config.store.remoteDesktop?.desktops;
|
|
@@ -500,6 +605,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
500
605
|
const entry = await (0, desktopForm_1.askDesktop)(pane.element.nativeElement, {
|
|
501
606
|
title: `Add a desktop reached through ${target.label}`,
|
|
502
607
|
action: 'Add and open',
|
|
608
|
+
accounts: this.accounts(),
|
|
609
|
+
addAccount: input => this.addAccountFromForm(input),
|
|
503
610
|
entry: { via: hostname, host: '127.0.0.1', port: 3389 },
|
|
504
611
|
check: e => this.addressTaken(e, -1),
|
|
505
612
|
});
|
|
@@ -524,20 +631,37 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
524
631
|
* sharpness along; a new user name drops the saved account, which was for the old one. Open sessions keep running.
|
|
525
632
|
*/
|
|
526
633
|
async editDesktop(pane, index) {
|
|
527
|
-
|
|
528
|
-
if (!old) {
|
|
634
|
+
if (!this.configuredDesktops()[index]) {
|
|
529
635
|
return;
|
|
530
636
|
}
|
|
531
637
|
if (this.isVisible(pane)) {
|
|
532
638
|
this.showConsole(pane);
|
|
533
639
|
}
|
|
534
|
-
|
|
535
|
-
|
|
640
|
+
await this.editDesktopIn(pane.element.nativeElement, index);
|
|
641
|
+
pane.frontend?.focus();
|
|
642
|
+
}
|
|
643
|
+
/** What a desktop's `via` can name: the SSH profiles' names and hosts (see viaMatches), for the form to suggest. */
|
|
644
|
+
async sshHosts() {
|
|
645
|
+
const profiles = await this.profiles.getProfiles().catch(() => []);
|
|
646
|
+
const names = profiles.filter((p) => p.type === 'ssh' && !p.isTemplate)
|
|
647
|
+
.flatMap((p) => [p.name?.replace(/\s*\(\.ssh\/config\)$/, ''), p.options?.host].filter(Boolean));
|
|
648
|
+
return [...new Set(names)].sort((a, b) => a.localeCompare(b));
|
|
649
|
+
}
|
|
650
|
+
/** "Edit a desktop" with the form over any element (a pane, or the settings page). */
|
|
651
|
+
async editDesktopIn(host, index, askVia = false) {
|
|
652
|
+
const old = this.configuredDesktops()[index];
|
|
653
|
+
if (!old) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
const entry = await (0, desktopForm_1.askDesktop)(host, {
|
|
657
|
+
title: `Edit ${old.name ?? (0, desktops_1.desktopIdOf)(old)}${askVia ? '' : ` (behind ${old.via})`}`,
|
|
536
658
|
action: 'Save',
|
|
659
|
+
accounts: this.accounts(),
|
|
660
|
+
addAccount: input => this.addAccountFromForm(input),
|
|
661
|
+
hosts: askVia ? await this.sshHosts() : undefined,
|
|
537
662
|
entry: old,
|
|
538
663
|
check: e => this.addressTaken(e, index),
|
|
539
664
|
});
|
|
540
|
-
pane.frontend?.focus();
|
|
541
665
|
// Compared with the entry as it is now: the config can change while the form shows.
|
|
542
666
|
const list = this.configuredDesktops();
|
|
543
667
|
if (!entry || list[index] !== old) {
|
|
@@ -547,7 +671,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
547
671
|
this.config.store.remoteDesktop.desktops = list;
|
|
548
672
|
this.config.save();
|
|
549
673
|
this.changed$.next();
|
|
550
|
-
const accountChanged = (old.username ?? '') !== (entry.username ?? '') || (old.domain ?? '') !== (entry.domain ?? '');
|
|
674
|
+
const accountChanged = (old.username ?? '') !== (entry.username ?? '') || (old.domain ?? '') !== (entry.domain ?? '') || (old.account ?? '') !== (entry.account ?? '');
|
|
551
675
|
await this.desktopEdited((0, desktops_1.desktopIdOf)(old), (0, desktops_1.desktopIdOf)(entry), accountChanged, false);
|
|
552
676
|
}
|
|
553
677
|
/**
|
|
@@ -610,6 +734,21 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
610
734
|
await this.importRdp(new Uint8Array(await file.arrayBuffer()), file.name);
|
|
611
735
|
}
|
|
612
736
|
}
|
|
737
|
+
/**
|
|
738
|
+
* The profile group for remote desktop profiles the plugin makes itself (an .rdp import, "New profile…" on the
|
|
739
|
+
* settings page): "Remote desktops", made the first time. Profiles made on Tabby's Profiles page go where the user
|
|
740
|
+
* puts them.
|
|
741
|
+
*/
|
|
742
|
+
async remoteDesktopGroup() {
|
|
743
|
+
const groups = await this.profiles.getProfileGroups({ includeNonUserGroup: false });
|
|
744
|
+
const existing = groups.find(g => g.name === 'Remote desktops');
|
|
745
|
+
if (existing?.id) {
|
|
746
|
+
return existing.id;
|
|
747
|
+
}
|
|
748
|
+
const group = { name: 'Remote desktops', profiles: [] };
|
|
749
|
+
await this.profiles.newProfileGroup(group, { genId: true });
|
|
750
|
+
return group.id;
|
|
751
|
+
}
|
|
613
752
|
/**
|
|
614
753
|
* Adds a "Remote desktop (RDP)" profile from a .rdp file's contents (address, user name, domain; see rdpFile.ts)
|
|
615
754
|
* and opens it; or opens the profile that already has that address and user. Returns the profile, or null.
|
|
@@ -631,6 +770,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
631
770
|
type: targets_1.RDP_PROFILE_TYPE,
|
|
632
771
|
name: fileName.replace(/^.*[\\/]/, '').replace(/\.rdp$/i, '') || parsed.host,
|
|
633
772
|
icon: 'fas fa-desktop',
|
|
773
|
+
group: await this.remoteDesktopGroup(),
|
|
634
774
|
options: { host: parsed.host, port: parsed.port, kind: 'windows', username: parsed.username ?? '', domain: parsed.domain ?? '', via: '' },
|
|
635
775
|
};
|
|
636
776
|
await this.profiles.newProfile(profile);
|
|
@@ -1437,23 +1577,34 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1437
1577
|
spec.port = endpoint.port;
|
|
1438
1578
|
spec.username ??= endpoint.username;
|
|
1439
1579
|
}
|
|
1440
|
-
|
|
1580
|
+
// A saved account (Settings): its user name, and its password from the keychain. One that has none yet, or that
|
|
1581
|
+
// this desktop just refused, is asked for here and saved for every desktop that uses the account.
|
|
1582
|
+
const account = spec.account ? this.accounts().find(a => a.id === spec.account) : undefined;
|
|
1583
|
+
if (spec.account && !account) {
|
|
1584
|
+
session.log.push('sign-in: its saved account no longer exists; asking');
|
|
1585
|
+
}
|
|
1586
|
+
const again = this.askAgain.delete(pane);
|
|
1587
|
+
const saved = retryError || again ? null : await (0, signin_1.loadCredentials)(account ? (0, accounts_1.accountKey)(account.id) : session.key);
|
|
1441
1588
|
if (saved) {
|
|
1442
|
-
|
|
1589
|
+
if (account) {
|
|
1590
|
+
session.log.push(`sign-in: the saved account "${account.name}"`);
|
|
1591
|
+
}
|
|
1592
|
+
return { host: spec.host, port: spec.port, credentials: account ? { username: (0, accounts_1.signInName)(account), password: saved.password } : saved, remember: false };
|
|
1443
1593
|
}
|
|
1444
1594
|
session.status('');
|
|
1445
1595
|
const asked = (0, signin_1.askCredentials)(session.overlay, {
|
|
1446
1596
|
// The host's own desktops (its own, or its xrdp besides GNOME) and direct ones need no "via".
|
|
1447
1597
|
title: spec.id === desktops_1.OWN_DESKTOP || target.direct || spec.kind === 'xrdp' && spec.host === '127.0.0.1' ? `Sign in to ${spec.name}` : `Sign in to ${spec.name} (via ${target.label})`,
|
|
1448
|
-
username: spec.username,
|
|
1598
|
+
username: account ? (0, accounts_1.signInName)(account) : spec.username,
|
|
1449
1599
|
error: retryError,
|
|
1450
1600
|
canRemember: true,
|
|
1601
|
+
account: account?.name,
|
|
1451
1602
|
}, session.disposed);
|
|
1452
1603
|
if (session.visible) {
|
|
1453
1604
|
setTimeout(() => session.visible && session.focusDesktop());
|
|
1454
1605
|
}
|
|
1455
1606
|
const entered = await asked;
|
|
1456
|
-
return entered && { host: spec.host, port: spec.port, credentials: entered, remember: entered.remember };
|
|
1607
|
+
return entered && { host: spec.host, port: spec.port, credentials: entered, remember: entered.remember, saveKey: account && (0, accounts_1.accountKey)(account.id) };
|
|
1457
1608
|
}
|
|
1458
1609
|
async connect(pane, target, spec, session) {
|
|
1459
1610
|
const alive = () => this.sessions.get(pane) === session;
|
|
@@ -1505,6 +1656,8 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1505
1656
|
return;
|
|
1506
1657
|
}
|
|
1507
1658
|
if (outcome.signInFailed && this.signsIn(spec) && attempt < 5 && alive()) {
|
|
1659
|
+
// The desktop's own saved account goes. A shared one stays until a new password is entered: this
|
|
1660
|
+
// desktop refusing it doesn't make it wrong for the others.
|
|
1508
1661
|
await (0, signin_1.forgetCredentials)(session.key);
|
|
1509
1662
|
retryError = outcome.signInFailed;
|
|
1510
1663
|
endpoint = null;
|
|
@@ -1848,7 +2001,7 @@ let RemoteDesktopService = RemoteDesktopService_1 = class RemoteDesktopService {
|
|
|
1848
2001
|
this.changed$.next();
|
|
1849
2002
|
this.reconnects.delete(pane); // connected again: a later drop starts over
|
|
1850
2003
|
if (endpoint.remember) {
|
|
1851
|
-
(0, signin_1.saveCredentials)(session.key, credentials).then(() => session.log.push(
|
|
2004
|
+
(0, signin_1.saveCredentials)(endpoint.saveKey ?? session.key, credentials, endpoint.saveLabel).then(() => session.log.push(`sign-in: saved ${endpoint.saveKey ? 'as the account\'s password ' : ''}in ${(0, signin_1.storeName)()}`), e => session.log.push(`sign-in: ${(0, signin_1.storeName)()}: ${e?.message ?? e}`));
|
|
1852
2005
|
}
|
|
1853
2006
|
if (session.visible) {
|
|
1854
2007
|
session.focusDesktop();
|
|
@@ -1901,7 +2054,38 @@ function installStyle() {
|
|
|
1901
2054
|
}
|
|
1902
2055
|
styleInstalled = true;
|
|
1903
2056
|
const style = document.createElement('style');
|
|
1904
|
-
style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE;
|
|
2057
|
+
style.textContent = STYLE + signin_1.STYLE + fileTransfer_1.STYLE + connectionStatus_1.STYLE + osd_1.OSD_STYLE + DRAGBAR_STYLE + accountForm_1.STYLE;
|
|
1905
2058
|
document.head.appendChild(style);
|
|
2059
|
+
keepWindowDraggable();
|
|
2060
|
+
}
|
|
2061
|
+
/** A strip the height of the tab bar that stays a window drag region over a dialog (the plugin's, and Tabby's). */
|
|
2062
|
+
const DRAGBAR_STYLE = `
|
|
2063
|
+
.trd-form-dragbar { display: none; }
|
|
2064
|
+
.modal > .trd-form-dragbar { display: block; position: fixed; left: 0; right: 0; top: 0; height: var(--tabs-height, 38px); -webkit-app-region: drag; }
|
|
2065
|
+
`;
|
|
2066
|
+
/**
|
|
2067
|
+
* Tabby's dialogs (ng-bootstrap modals) cover the whole window, tab bar included, with nothing marked as a drag
|
|
2068
|
+
* region, so the window can't be moved while one shows. Each gets a strip where the tab bar is; the dialog box itself
|
|
2069
|
+
* is below it. The plugin's own dialogs do the same (see settingsPage.ts).
|
|
2070
|
+
*/
|
|
2071
|
+
function keepWindowDraggable() {
|
|
2072
|
+
const strip = (modal) => {
|
|
2073
|
+
if (!modal.querySelector(':scope > .trd-form-dragbar')) {
|
|
2074
|
+
const bar = document.createElement('div');
|
|
2075
|
+
bar.className = 'trd-form-dragbar';
|
|
2076
|
+
modal.prepend(bar);
|
|
2077
|
+
}
|
|
2078
|
+
};
|
|
2079
|
+
// ng-bootstrap appends each dialog (ngb-modal-window.modal) to the body itself: only the body's children are watched.
|
|
2080
|
+
document.querySelectorAll('body > .modal').forEach(strip);
|
|
2081
|
+
new MutationObserver(records => {
|
|
2082
|
+
for (const record of records) {
|
|
2083
|
+
record.addedNodes.forEach(node => {
|
|
2084
|
+
if (node instanceof Element && node.classList.contains('modal')) {
|
|
2085
|
+
strip(node);
|
|
2086
|
+
}
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
}).observe(document.body, { childList: true });
|
|
1906
2090
|
}
|
|
1907
2091
|
//# sourceMappingURL=desktop.service.js.map
|