tabby-sftp-ui 0.1.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/LICENSE +22 -0
- package/README.md +129 -0
- package/dist/config.d.ts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +2624 -0
- package/dist/index.js.map +1 -0
- package/dist/local-transfers.d.ts +24 -0
- package/dist/sftp-context-menu.d.ts +10 -0
- package/dist/sftp-hotkey.d.ts +7 -0
- package/dist/sftp-manager-tab.component.d.ts +189 -0
- package/dist/sftp-terminal-decorator.d.ts +7 -0
- package/dist/sftp-toolbar-buttons.d.ts +6 -0
- package/dist/sftp-ui.service.d.ts +13 -0
- package/dist/sftp.service.d.ts +24 -0
- package/package.json +44 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2624 @@
|
|
|
1
|
+
(function webpackUniversalModuleDefinition(root, factory) {
|
|
2
|
+
if(typeof exports === 'object' && typeof module === 'object')
|
|
3
|
+
module.exports = factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs"));
|
|
4
|
+
else if(typeof define === 'function' && define.amd)
|
|
5
|
+
define(["@angular/common", "@angular/forms", "@angular/core", "tabby-core", "tabby-terminal", "path", "fs"], factory);
|
|
6
|
+
else {
|
|
7
|
+
var a = typeof exports === 'object' ? factory(require("@angular/common"), require("@angular/forms"), require("@angular/core"), require("tabby-core"), require("tabby-terminal"), require("path"), require("fs")) : factory(root["@angular/common"], root["@angular/forms"], root["@angular/core"], root["tabby-core"], root["tabby-terminal"], root["path"], root["fs"]);
|
|
8
|
+
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
|
|
9
|
+
}
|
|
10
|
+
})(global, (__WEBPACK_EXTERNAL_MODULE__angular_common__, __WEBPACK_EXTERNAL_MODULE__angular_forms__, __WEBPACK_EXTERNAL_MODULE__angular_core__, __WEBPACK_EXTERNAL_MODULE_tabby_core__, __WEBPACK_EXTERNAL_MODULE_tabby_terminal__, __WEBPACK_EXTERNAL_MODULE_path__, __WEBPACK_EXTERNAL_MODULE_fs__) => {
|
|
11
|
+
return /******/ (() => { // webpackBootstrap
|
|
12
|
+
/******/ "use strict";
|
|
13
|
+
/******/ var __webpack_modules__ = ({
|
|
14
|
+
|
|
15
|
+
/***/ "./src/local-transfers.ts"
|
|
16
|
+
/*!********************************!*\
|
|
17
|
+
!*** ./src/local-transfers.ts ***!
|
|
18
|
+
\********************************/
|
|
19
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
20
|
+
|
|
21
|
+
__webpack_require__.r(__webpack_exports__);
|
|
22
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
23
|
+
/* harmony export */ LocalPathFileDownload: () => (/* binding */ LocalPathFileDownload),
|
|
24
|
+
/* harmony export */ LocalPathFileUpload: () => (/* binding */ LocalPathFileUpload)
|
|
25
|
+
/* harmony export */ });
|
|
26
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! fs */ "fs");
|
|
27
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_0__);
|
|
28
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! path */ "path");
|
|
29
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_1__);
|
|
30
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
31
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_2__);
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class LocalPathFileUpload extends tabby_core__WEBPACK_IMPORTED_MODULE_2__.FileUpload {
|
|
36
|
+
constructor(filePath) {
|
|
37
|
+
super();
|
|
38
|
+
this.filePath = filePath;
|
|
39
|
+
this.fd = null;
|
|
40
|
+
this.position = 0;
|
|
41
|
+
}
|
|
42
|
+
getName() {
|
|
43
|
+
return path__WEBPACK_IMPORTED_MODULE_1__.basename(this.filePath);
|
|
44
|
+
}
|
|
45
|
+
getMode() {
|
|
46
|
+
return 0o644;
|
|
47
|
+
}
|
|
48
|
+
getSize() {
|
|
49
|
+
try {
|
|
50
|
+
return fs__WEBPACK_IMPORTED_MODULE_0__.statSync(this.filePath).size;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
async read() {
|
|
57
|
+
if (this.isCancelled()) {
|
|
58
|
+
return Buffer.alloc(0);
|
|
59
|
+
}
|
|
60
|
+
if (this.fd === null) {
|
|
61
|
+
this.fd = fs__WEBPACK_IMPORTED_MODULE_0__.openSync(this.filePath, 'r');
|
|
62
|
+
}
|
|
63
|
+
const buf = Buffer.allocUnsafe(256 * 1024);
|
|
64
|
+
const bytesRead = fs__WEBPACK_IMPORTED_MODULE_0__.readSync(this.fd, buf, 0, buf.length, this.position);
|
|
65
|
+
if (!bytesRead) {
|
|
66
|
+
return Buffer.alloc(0);
|
|
67
|
+
}
|
|
68
|
+
this.position += bytesRead;
|
|
69
|
+
this.increaseProgress(bytesRead);
|
|
70
|
+
return buf.subarray(0, bytesRead);
|
|
71
|
+
}
|
|
72
|
+
close() {
|
|
73
|
+
if (this.fd !== null) {
|
|
74
|
+
try {
|
|
75
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.closeSync(this.fd);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// ignore
|
|
79
|
+
}
|
|
80
|
+
this.fd = null;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
class LocalPathFileDownload extends tabby_core__WEBPACK_IMPORTED_MODULE_2__.FileDownload {
|
|
85
|
+
constructor(targetPath, mode, size) {
|
|
86
|
+
super();
|
|
87
|
+
this.targetPath = targetPath;
|
|
88
|
+
this.mode = mode;
|
|
89
|
+
this.size = size;
|
|
90
|
+
this.fd = null;
|
|
91
|
+
}
|
|
92
|
+
getName() {
|
|
93
|
+
return path__WEBPACK_IMPORTED_MODULE_1__.basename(this.targetPath);
|
|
94
|
+
}
|
|
95
|
+
getMode() {
|
|
96
|
+
return this.mode;
|
|
97
|
+
}
|
|
98
|
+
getSize() {
|
|
99
|
+
return this.size;
|
|
100
|
+
}
|
|
101
|
+
async write(buffer) {
|
|
102
|
+
if (this.isCancelled()) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (this.fd === null) {
|
|
106
|
+
this.fd = fs__WEBPACK_IMPORTED_MODULE_0__.openSync(this.targetPath, 'w');
|
|
107
|
+
}
|
|
108
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.writeSync(this.fd, buffer);
|
|
109
|
+
this.increaseProgress(buffer.length);
|
|
110
|
+
}
|
|
111
|
+
close() {
|
|
112
|
+
if (this.fd !== null) {
|
|
113
|
+
try {
|
|
114
|
+
fs__WEBPACK_IMPORTED_MODULE_0__.closeSync(this.fd);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// ignore
|
|
118
|
+
}
|
|
119
|
+
this.fd = null;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
/***/ },
|
|
126
|
+
|
|
127
|
+
/***/ "./src/sftp-context-menu.ts"
|
|
128
|
+
/*!**********************************!*\
|
|
129
|
+
!*** ./src/sftp-context-menu.ts ***!
|
|
130
|
+
\**********************************/
|
|
131
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
132
|
+
|
|
133
|
+
__webpack_require__.r(__webpack_exports__);
|
|
134
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
135
|
+
/* harmony export */ SftpContextMenuProvider: () => (/* binding */ SftpContextMenuProvider)
|
|
136
|
+
/* harmony export */ });
|
|
137
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
138
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
139
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
140
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
141
|
+
/* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
|
|
142
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
143
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
144
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
145
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
146
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
147
|
+
};
|
|
148
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
149
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
let SftpContextMenuProvider = class SftpContextMenuProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.TabContextMenuItemProvider {
|
|
155
|
+
constructor(sftpUi) {
|
|
156
|
+
super();
|
|
157
|
+
this.sftpUi = sftpUi;
|
|
158
|
+
}
|
|
159
|
+
async getItems() {
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
label: 'Open SFTP UI',
|
|
163
|
+
click: () => this.sftpUi.open(),
|
|
164
|
+
},
|
|
165
|
+
];
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
SftpContextMenuProvider = __decorate([
|
|
169
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)(),
|
|
170
|
+
__metadata("design:paramtypes", [_sftp_ui_service__WEBPACK_IMPORTED_MODULE_2__.SftpUiService])
|
|
171
|
+
], SftpContextMenuProvider);
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
/***/ },
|
|
176
|
+
|
|
177
|
+
/***/ "./src/sftp-hotkey.ts"
|
|
178
|
+
/*!****************************!*\
|
|
179
|
+
!*** ./src/sftp-hotkey.ts ***!
|
|
180
|
+
\****************************/
|
|
181
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
182
|
+
|
|
183
|
+
__webpack_require__.r(__webpack_exports__);
|
|
184
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
185
|
+
/* harmony export */ SftpUiHotkeyProvider: () => (/* binding */ SftpUiHotkeyProvider)
|
|
186
|
+
/* harmony export */ });
|
|
187
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
188
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
189
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
190
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
191
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
192
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
193
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
194
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
195
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
let SftpUiHotkeyProvider = class SftpUiHotkeyProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.HotkeyProvider {
|
|
200
|
+
async provide() {
|
|
201
|
+
return [
|
|
202
|
+
{ id: 'open-sftp-ui', name: 'Open SFTP UI' },
|
|
203
|
+
];
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
SftpUiHotkeyProvider = __decorate([
|
|
207
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
|
|
208
|
+
], SftpUiHotkeyProvider);
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
/***/ },
|
|
213
|
+
|
|
214
|
+
/***/ "./src/sftp-manager-tab.component.ts"
|
|
215
|
+
/*!*******************************************!*\
|
|
216
|
+
!*** ./src/sftp-manager-tab.component.ts ***!
|
|
217
|
+
\*******************************************/
|
|
218
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
219
|
+
|
|
220
|
+
__webpack_require__.r(__webpack_exports__);
|
|
221
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
222
|
+
/* harmony export */ SftpManagerTabComponent: () => (/* binding */ SftpManagerTabComponent)
|
|
223
|
+
/* harmony export */ });
|
|
224
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! path */ "path");
|
|
225
|
+
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);
|
|
226
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! os */ "os");
|
|
227
|
+
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(os__WEBPACK_IMPORTED_MODULE_1__);
|
|
228
|
+
/* harmony import */ var fs_promises__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! fs/promises */ "fs/promises");
|
|
229
|
+
/* harmony import */ var fs_promises__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs_promises__WEBPACK_IMPORTED_MODULE_2__);
|
|
230
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! fs */ "fs");
|
|
231
|
+
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_3__);
|
|
232
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
233
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_4__);
|
|
234
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
235
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_5__);
|
|
236
|
+
/* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
|
|
237
|
+
/* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
|
|
238
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
239
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
240
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
241
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
242
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
243
|
+
};
|
|
244
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
245
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_5__.BaseTabComponent {
|
|
256
|
+
constructor(injector, sftp, profilesService, app) {
|
|
257
|
+
// Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
|
|
258
|
+
// @ts-expect-error runtime-compatible super(injector)
|
|
259
|
+
super(injector);
|
|
260
|
+
this.sftp = sftp;
|
|
261
|
+
this.profilesService = profilesService;
|
|
262
|
+
this.app = app;
|
|
263
|
+
// injected from the SSH tab when opened via SFTP-UI button
|
|
264
|
+
this.sshSession = null;
|
|
265
|
+
this.profile = null;
|
|
266
|
+
this.connecting = false;
|
|
267
|
+
this.connected = false;
|
|
268
|
+
// legacy UI fields kept for now (not used when opened from SSH tab)
|
|
269
|
+
this.host = '';
|
|
270
|
+
this.port = 22;
|
|
271
|
+
this.username = '';
|
|
272
|
+
this.password = '';
|
|
273
|
+
this.localPath = os__WEBPACK_IMPORTED_MODULE_1__.homedir();
|
|
274
|
+
this.localEntries = [];
|
|
275
|
+
this.remotePath = '/';
|
|
276
|
+
this.remoteEntries = [];
|
|
277
|
+
this.sftpSession = null;
|
|
278
|
+
this.localDropActive = false;
|
|
279
|
+
this.remoteDropActive = false;
|
|
280
|
+
this.transfers = [];
|
|
281
|
+
this.transfersTimer = null;
|
|
282
|
+
this.localFilter = '';
|
|
283
|
+
this.remoteFilter = '';
|
|
284
|
+
this.remotePathInput = this.remotePath;
|
|
285
|
+
this.localPathInput = this.localPath;
|
|
286
|
+
this.localFolderSizeLoading = new Set();
|
|
287
|
+
this.remoteFolderSizeLoading = new Set();
|
|
288
|
+
this.localSortBy = 'name';
|
|
289
|
+
this.localSortAsc = true;
|
|
290
|
+
this.remoteSortBy = 'name';
|
|
291
|
+
this.remoteSortAsc = true;
|
|
292
|
+
this.localCache = null;
|
|
293
|
+
this.remoteCache = null;
|
|
294
|
+
this.showHiddenLocal = false;
|
|
295
|
+
this.showHiddenRemote = false;
|
|
296
|
+
this.selectedLocal = [];
|
|
297
|
+
this.selectedRemote = [];
|
|
298
|
+
this.localActionName = '';
|
|
299
|
+
this.localActionPerms = '';
|
|
300
|
+
this.remoteActionName = '';
|
|
301
|
+
this.remoteActionPerms = '';
|
|
302
|
+
this.localLastSelectedIndex = null;
|
|
303
|
+
this.remoteLastSelectedIndex = null;
|
|
304
|
+
this.deleteConfirmVisible = false;
|
|
305
|
+
this.deleteConfirmMode = null;
|
|
306
|
+
this.deleteConfirmText = '';
|
|
307
|
+
this.pendingLocalDelete = [];
|
|
308
|
+
this.pendingRemoteDelete = [];
|
|
309
|
+
this.openedRemoteFiles = new Map();
|
|
310
|
+
this.localPathPresets = [];
|
|
311
|
+
this.localFavorites = [];
|
|
312
|
+
this.recentProfiles = [];
|
|
313
|
+
this.localMenuVisible = false;
|
|
314
|
+
this.localMenuX = 0;
|
|
315
|
+
this.localMenuY = 0;
|
|
316
|
+
this.localMenuItems = [];
|
|
317
|
+
this.platform = injector.get(tabby_core__WEBPACK_IMPORTED_MODULE_5__.PlatformService);
|
|
318
|
+
// build local path presets (similar to Termius quick locations)
|
|
319
|
+
const home = os__WEBPACK_IMPORTED_MODULE_1__.homedir();
|
|
320
|
+
this.localPathPresets.push({ id: 'home', label: 'Home', path: home });
|
|
321
|
+
const desktop = path__WEBPACK_IMPORTED_MODULE_0__.join(home, 'Desktop');
|
|
322
|
+
const documents = path__WEBPACK_IMPORTED_MODULE_0__.join(home, 'Documents');
|
|
323
|
+
const downloads = path__WEBPACK_IMPORTED_MODULE_0__.join(home, 'Downloads');
|
|
324
|
+
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(desktop)) {
|
|
325
|
+
this.localPathPresets.push({ id: 'desktop', label: 'Desktop', path: desktop });
|
|
326
|
+
}
|
|
327
|
+
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(documents)) {
|
|
328
|
+
this.localPathPresets.push({ id: 'documents', label: 'Documents', path: documents });
|
|
329
|
+
}
|
|
330
|
+
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(downloads)) {
|
|
331
|
+
this.localPathPresets.push({ id: 'downloads', label: 'Downloads', path: downloads });
|
|
332
|
+
}
|
|
333
|
+
this.loadLocalFavorites();
|
|
334
|
+
void this.refreshLocal();
|
|
335
|
+
this.transfersTimer = window.setInterval(() => {
|
|
336
|
+
this.transfers = this.transfers.filter(t => !t.transfer.isComplete() && !t.transfer.isCancelled());
|
|
337
|
+
}, 1000);
|
|
338
|
+
}
|
|
339
|
+
ngOnInit() {
|
|
340
|
+
// If there's no live SSH session, this tab was likely restored across
|
|
341
|
+
// restart or opened in an invalid context. Close it immediately to avoid
|
|
342
|
+
// an empty, nameless SFTP tab lingering after restart.
|
|
343
|
+
if (!this.sshSession) {
|
|
344
|
+
try {
|
|
345
|
+
this.app.closeTab(this);
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
console.error('[SFTP-UI] Failed to close invalid SFTP tab', e);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
this.remotePathInput = this.remotePath;
|
|
353
|
+
this.localPathInput = this.localPath;
|
|
354
|
+
if (this.sshSession) {
|
|
355
|
+
void this.connect();
|
|
356
|
+
}
|
|
357
|
+
this.loadRecentProfiles();
|
|
358
|
+
}
|
|
359
|
+
async connect() {
|
|
360
|
+
if (this.connecting || this.connected) {
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (!this.sshSession) {
|
|
364
|
+
console.error('[SFTP-UI] No SSH session on current tab');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
this.connecting = true;
|
|
368
|
+
try {
|
|
369
|
+
this.sftpSession = await this.sftp.openFromSSHSession(this.sshSession);
|
|
370
|
+
this.connected = true;
|
|
371
|
+
this.remotePath = this.getDefaultRemotePath();
|
|
372
|
+
this.remotePathInput = this.remotePath;
|
|
373
|
+
await this.refreshRemote();
|
|
374
|
+
}
|
|
375
|
+
catch (e) {
|
|
376
|
+
console.error('[SFTP-UI] SFTP connection failed', e);
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
this.connecting = false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async disconnect() {
|
|
383
|
+
this.sftpSession = null;
|
|
384
|
+
this.connected = false;
|
|
385
|
+
this.remoteEntries = [];
|
|
386
|
+
}
|
|
387
|
+
canLocalUp() {
|
|
388
|
+
const parent = path__WEBPACK_IMPORTED_MODULE_0__.dirname(this.localPath);
|
|
389
|
+
return parent !== this.localPath;
|
|
390
|
+
}
|
|
391
|
+
localUp() {
|
|
392
|
+
const parent = path__WEBPACK_IMPORTED_MODULE_0__.dirname(this.localPath);
|
|
393
|
+
if (parent !== this.localPath) {
|
|
394
|
+
this.localPath = parent;
|
|
395
|
+
this.localPathInput = this.localPath;
|
|
396
|
+
void this.refreshLocal();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
remoteUp() {
|
|
400
|
+
if (!this.connected || this.remotePath === '/') {
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
const next = path__WEBPACK_IMPORTED_MODULE_0__.posix.dirname(this.remotePath);
|
|
404
|
+
this.remotePath = next === '.' ? '/' : next;
|
|
405
|
+
this.remotePathInput = this.remotePath;
|
|
406
|
+
void this.refreshRemote();
|
|
407
|
+
}
|
|
408
|
+
async refreshLocal() {
|
|
409
|
+
try {
|
|
410
|
+
const names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(this.localPath);
|
|
411
|
+
const entries = [];
|
|
412
|
+
for (const name of names) {
|
|
413
|
+
const fullPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, name);
|
|
414
|
+
try {
|
|
415
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(fullPath);
|
|
416
|
+
entries.push({
|
|
417
|
+
name,
|
|
418
|
+
fullPath,
|
|
419
|
+
isDirectory: st.isDirectory(),
|
|
420
|
+
size: st.size,
|
|
421
|
+
mtimeMs: st.mtimeMs,
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
catch {
|
|
425
|
+
// ignore entries that disappeared
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
this.localEntries = entries;
|
|
429
|
+
}
|
|
430
|
+
catch (e) {
|
|
431
|
+
console.error('[SFTP-UI] Local listing failed', e);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
async refreshRemote() {
|
|
435
|
+
if (!this.connected) {
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
try {
|
|
439
|
+
if (!this.sftpSession) {
|
|
440
|
+
throw new Error('Not connected');
|
|
441
|
+
}
|
|
442
|
+
this.remoteEntries = await this.sftpSession.readdir(this.remotePath);
|
|
443
|
+
}
|
|
444
|
+
catch (e) {
|
|
445
|
+
console.error('[SFTP-UI] Remote listing failed', e);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
openLocal(e) {
|
|
449
|
+
if (!e.isDirectory) {
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
this.localPath = e.fullPath;
|
|
453
|
+
this.localPathInput = this.localPath;
|
|
454
|
+
void this.refreshLocal();
|
|
455
|
+
}
|
|
456
|
+
openRemote(e) {
|
|
457
|
+
if (!this.connected) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
if (e.isDirectory) {
|
|
461
|
+
this.remotePath = e.fullPath;
|
|
462
|
+
this.remotePathInput = this.remotePath;
|
|
463
|
+
void this.refreshRemote();
|
|
464
|
+
}
|
|
465
|
+
else {
|
|
466
|
+
void this.openRemoteFile(e);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
onDragOver(ev) {
|
|
470
|
+
ev.preventDefault();
|
|
471
|
+
}
|
|
472
|
+
onLocalMouseDown(entry, event) {
|
|
473
|
+
if (event.button === 2) {
|
|
474
|
+
this.onLocalContextMenu(entry, event);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
onRemoteMouseDown(entry, event) {
|
|
478
|
+
if (event.button === 2) {
|
|
479
|
+
this.onRemoteContextMenu(entry, event);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
selectLocal(entry, event) {
|
|
483
|
+
const list = this.getFilteredLocalEntries();
|
|
484
|
+
const index = list.indexOf(entry);
|
|
485
|
+
if (index === -1) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const isCtrl = event.ctrlKey || event.metaKey;
|
|
489
|
+
const isShift = event.shiftKey;
|
|
490
|
+
if (isShift && this.localLastSelectedIndex != null) {
|
|
491
|
+
const [from, to] = this.localLastSelectedIndex < index
|
|
492
|
+
? [this.localLastSelectedIndex, index]
|
|
493
|
+
: [index, this.localLastSelectedIndex];
|
|
494
|
+
const range = list.slice(from, to + 1);
|
|
495
|
+
const set = new Set(this.selectedLocal);
|
|
496
|
+
for (const e of range) {
|
|
497
|
+
set.add(e);
|
|
498
|
+
}
|
|
499
|
+
this.selectedLocal = Array.from(set);
|
|
500
|
+
}
|
|
501
|
+
else if (isCtrl) {
|
|
502
|
+
const exists = this.selectedLocal.includes(entry);
|
|
503
|
+
if (exists) {
|
|
504
|
+
this.selectedLocal = this.selectedLocal.filter(e => e !== entry);
|
|
505
|
+
}
|
|
506
|
+
else {
|
|
507
|
+
this.selectedLocal = [...this.selectedLocal, entry];
|
|
508
|
+
}
|
|
509
|
+
this.localLastSelectedIndex = index;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
this.selectedLocal = [entry];
|
|
513
|
+
this.localLastSelectedIndex = index;
|
|
514
|
+
}
|
|
515
|
+
if (this.selectedLocal.length === 1) {
|
|
516
|
+
this.localActionName = this.selectedLocal[0].name;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
isLocalSelected(entry) {
|
|
520
|
+
return this.selectedLocal.includes(entry);
|
|
521
|
+
}
|
|
522
|
+
selectRemote(entry, event) {
|
|
523
|
+
const list = this.getFilteredRemoteEntries();
|
|
524
|
+
const index = list.indexOf(entry);
|
|
525
|
+
if (index === -1) {
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const isCtrl = event.ctrlKey || event.metaKey;
|
|
529
|
+
const isShift = event.shiftKey;
|
|
530
|
+
if (isShift && this.remoteLastSelectedIndex != null) {
|
|
531
|
+
const [from, to] = this.remoteLastSelectedIndex < index
|
|
532
|
+
? [this.remoteLastSelectedIndex, index]
|
|
533
|
+
: [index, this.remoteLastSelectedIndex];
|
|
534
|
+
const range = list.slice(from, to + 1);
|
|
535
|
+
const set = new Set(this.selectedRemote);
|
|
536
|
+
for (const e of range) {
|
|
537
|
+
set.add(e);
|
|
538
|
+
}
|
|
539
|
+
this.selectedRemote = Array.from(set);
|
|
540
|
+
}
|
|
541
|
+
else if (isCtrl) {
|
|
542
|
+
const exists = this.selectedRemote.includes(entry);
|
|
543
|
+
if (exists) {
|
|
544
|
+
this.selectedRemote = this.selectedRemote.filter(e => e !== entry);
|
|
545
|
+
}
|
|
546
|
+
else {
|
|
547
|
+
this.selectedRemote = [...this.selectedRemote, entry];
|
|
548
|
+
}
|
|
549
|
+
this.remoteLastSelectedIndex = index;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
this.selectedRemote = [entry];
|
|
553
|
+
this.remoteLastSelectedIndex = index;
|
|
554
|
+
}
|
|
555
|
+
if (this.selectedRemote.length === 1) {
|
|
556
|
+
this.remoteActionName = this.selectedRemote[0].name;
|
|
557
|
+
const currentPerms = (this.selectedRemote[0].mode & 0o777).toString(8);
|
|
558
|
+
this.remoteActionPerms = currentPerms;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
isRemoteSelected(entry) {
|
|
562
|
+
return this.selectedRemote.includes(entry);
|
|
563
|
+
}
|
|
564
|
+
setLocalSort(field) {
|
|
565
|
+
if (this.localSortBy === field) {
|
|
566
|
+
this.localSortAsc = !this.localSortAsc;
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
this.localSortBy = field;
|
|
570
|
+
this.localSortAsc = true;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
setRemoteSort(field) {
|
|
574
|
+
if (this.remoteSortBy === field) {
|
|
575
|
+
this.remoteSortAsc = !this.remoteSortAsc;
|
|
576
|
+
}
|
|
577
|
+
else {
|
|
578
|
+
this.remoteSortBy = field;
|
|
579
|
+
this.remoteSortAsc = true;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
onDragStartLocal(ev, e) {
|
|
583
|
+
const sources = this.selectedLocal.includes(e) && this.selectedLocal.length ? this.selectedLocal : [e];
|
|
584
|
+
const movePayload = sources.map(x => x.fullPath);
|
|
585
|
+
ev.dataTransfer?.setData('application/x-tabby-sftp-ui-local-move', JSON.stringify(movePayload));
|
|
586
|
+
// Existing cross-device drag (local -> remote) only for files
|
|
587
|
+
if (!e.isDirectory) {
|
|
588
|
+
const payload = { kind: 'local-file', fullPath: e.fullPath, name: e.name };
|
|
589
|
+
ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
|
|
590
|
+
ev.dataTransfer?.setData('text/plain', e.fullPath);
|
|
591
|
+
}
|
|
592
|
+
ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
|
|
593
|
+
}
|
|
594
|
+
onDragStartRemote(ev, item) {
|
|
595
|
+
if (!this.connected) {
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
const sources = this.selectedRemote.includes(item) && this.selectedRemote.length ? this.selectedRemote : [item];
|
|
599
|
+
const movePayload = sources.map(x => x.fullPath);
|
|
600
|
+
ev.dataTransfer?.setData('application/x-tabby-sftp-ui-remote-move', JSON.stringify(movePayload));
|
|
601
|
+
// Existing cross-device drag (remote -> local) only for files
|
|
602
|
+
if (!item.isDirectory) {
|
|
603
|
+
const payload = {
|
|
604
|
+
kind: 'remote-file',
|
|
605
|
+
remotePath: item.fullPath,
|
|
606
|
+
name: item.name,
|
|
607
|
+
size: item.size,
|
|
608
|
+
mode: item.mode,
|
|
609
|
+
};
|
|
610
|
+
ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
|
|
611
|
+
ev.dataTransfer?.setData('text/plain', item.fullPath);
|
|
612
|
+
}
|
|
613
|
+
ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
|
|
614
|
+
}
|
|
615
|
+
async onDropOnRemote(ev) {
|
|
616
|
+
ev.preventDefault();
|
|
617
|
+
this.remoteDropActive = false;
|
|
618
|
+
if (!this.connected) {
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
if (!this.sftpSession) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
625
|
+
if (!raw) {
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
let payload;
|
|
629
|
+
try {
|
|
630
|
+
payload = JSON.parse(raw);
|
|
631
|
+
}
|
|
632
|
+
catch {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
if (payload.kind !== 'local-file') {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
640
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(payload.fullPath);
|
|
641
|
+
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
642
|
+
await this.sftpSession.upload(targetRemotePath, upload);
|
|
643
|
+
await this.refreshRemote();
|
|
644
|
+
}
|
|
645
|
+
catch (e) {
|
|
646
|
+
console.error('[SFTP-UI] Upload failed', e);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
async onDropOnLocal(ev) {
|
|
650
|
+
ev.preventDefault();
|
|
651
|
+
this.localDropActive = false;
|
|
652
|
+
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
653
|
+
if (!raw) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
let payload;
|
|
657
|
+
try {
|
|
658
|
+
payload = JSON.parse(raw);
|
|
659
|
+
}
|
|
660
|
+
catch {
|
|
661
|
+
return;
|
|
662
|
+
}
|
|
663
|
+
if (payload.kind !== 'remote-file') {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
try {
|
|
667
|
+
const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
|
|
668
|
+
if (!this.sftpSession) {
|
|
669
|
+
throw new Error('Not connected');
|
|
670
|
+
}
|
|
671
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
|
|
672
|
+
this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
|
|
673
|
+
await this.sftpSession.download(payload.remotePath, dl);
|
|
674
|
+
await this.refreshLocal();
|
|
675
|
+
}
|
|
676
|
+
catch (e) {
|
|
677
|
+
console.error('[SFTP-UI] Download failed', e);
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
getFilteredLocalEntries() {
|
|
681
|
+
const entriesRef = this.localEntries;
|
|
682
|
+
const filter = this.localFilter;
|
|
683
|
+
const showHidden = this.showHiddenLocal;
|
|
684
|
+
const sortBy = this.localSortBy;
|
|
685
|
+
const asc = this.localSortAsc;
|
|
686
|
+
if (this.localCache &&
|
|
687
|
+
this.localCache.entriesRef === entriesRef &&
|
|
688
|
+
this.localCache.filter === filter &&
|
|
689
|
+
this.localCache.showHidden === showHidden &&
|
|
690
|
+
this.localCache.sortBy === sortBy &&
|
|
691
|
+
this.localCache.asc === asc) {
|
|
692
|
+
return this.localCache.result;
|
|
693
|
+
}
|
|
694
|
+
const term = filter.trim().toLowerCase();
|
|
695
|
+
let entries = entriesRef;
|
|
696
|
+
if (!showHidden) {
|
|
697
|
+
entries = entries.filter(e => !e.name.startsWith('.'));
|
|
698
|
+
}
|
|
699
|
+
if (term) {
|
|
700
|
+
entries = entries.filter(e => e.name.toLowerCase().includes(term));
|
|
701
|
+
}
|
|
702
|
+
const result = this.sortLocalEntries(entries.slice());
|
|
703
|
+
this.localCache = { entriesRef, filter, showHidden, sortBy, asc, result };
|
|
704
|
+
return result;
|
|
705
|
+
}
|
|
706
|
+
getFilteredRemoteEntries() {
|
|
707
|
+
const entriesRef = this.remoteEntries;
|
|
708
|
+
const filter = this.remoteFilter;
|
|
709
|
+
const showHidden = this.showHiddenRemote;
|
|
710
|
+
const sortBy = this.remoteSortBy;
|
|
711
|
+
const asc = this.remoteSortAsc;
|
|
712
|
+
if (this.remoteCache &&
|
|
713
|
+
this.remoteCache.entriesRef === entriesRef &&
|
|
714
|
+
this.remoteCache.filter === filter &&
|
|
715
|
+
this.remoteCache.showHidden === showHidden &&
|
|
716
|
+
this.remoteCache.sortBy === sortBy &&
|
|
717
|
+
this.remoteCache.asc === asc) {
|
|
718
|
+
return this.remoteCache.result;
|
|
719
|
+
}
|
|
720
|
+
const term = filter.trim().toLowerCase();
|
|
721
|
+
let entries = entriesRef;
|
|
722
|
+
if (!showHidden) {
|
|
723
|
+
entries = entries.filter(e => !e.name.startsWith('.'));
|
|
724
|
+
}
|
|
725
|
+
if (term) {
|
|
726
|
+
entries = entries.filter(e => e.name.toLowerCase().includes(term));
|
|
727
|
+
}
|
|
728
|
+
const result = this.sortRemoteEntries(entries.slice());
|
|
729
|
+
this.remoteCache = { entriesRef, filter, showHidden, sortBy, asc, result };
|
|
730
|
+
return result;
|
|
731
|
+
}
|
|
732
|
+
sortLocalEntries(entries) {
|
|
733
|
+
const dirFirst = (a, b) => Number(b.isDirectory) - Number(a.isDirectory);
|
|
734
|
+
const factor = this.localSortAsc ? 1 : -1;
|
|
735
|
+
const field = this.localSortBy;
|
|
736
|
+
return entries.sort((a, b) => {
|
|
737
|
+
const d = dirFirst(a, b);
|
|
738
|
+
if (d !== 0)
|
|
739
|
+
return d;
|
|
740
|
+
if (field === 'name') {
|
|
741
|
+
return a.name.localeCompare(b.name) * factor;
|
|
742
|
+
}
|
|
743
|
+
if (field === 'size') {
|
|
744
|
+
const av = a.size ?? 0;
|
|
745
|
+
const bv = b.size ?? 0;
|
|
746
|
+
return (av - bv) * factor;
|
|
747
|
+
}
|
|
748
|
+
const av = a.mtimeMs ?? 0;
|
|
749
|
+
const bv = b.mtimeMs ?? 0;
|
|
750
|
+
return (av - bv) * factor;
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
sortRemoteEntries(entries) {
|
|
754
|
+
const dirFirst = (a, b) => Number(b.isDirectory) - Number(a.isDirectory);
|
|
755
|
+
const factor = this.remoteSortAsc ? 1 : -1;
|
|
756
|
+
const field = this.remoteSortBy;
|
|
757
|
+
return entries.sort((a, b) => {
|
|
758
|
+
const d = dirFirst(a, b);
|
|
759
|
+
if (d !== 0)
|
|
760
|
+
return d;
|
|
761
|
+
if (field === 'name') {
|
|
762
|
+
return a.name.localeCompare(b.name) * factor;
|
|
763
|
+
}
|
|
764
|
+
if (field === 'size') {
|
|
765
|
+
const av = a.size ?? 0;
|
|
766
|
+
const bv = b.size ?? 0;
|
|
767
|
+
return (av - bv) * factor;
|
|
768
|
+
}
|
|
769
|
+
const av = a.modified?.getTime?.() ?? 0;
|
|
770
|
+
const bv = b.modified?.getTime?.() ?? 0;
|
|
771
|
+
return (av - bv) * factor;
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
getLocalMovePayload(ev) {
|
|
775
|
+
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui-local-move');
|
|
776
|
+
if (!raw) {
|
|
777
|
+
return null;
|
|
778
|
+
}
|
|
779
|
+
try {
|
|
780
|
+
const arr = JSON.parse(raw);
|
|
781
|
+
if (Array.isArray(arr)) {
|
|
782
|
+
return arr;
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
// ignore
|
|
787
|
+
}
|
|
788
|
+
return null;
|
|
789
|
+
}
|
|
790
|
+
getRemoteMovePayload(ev) {
|
|
791
|
+
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui-remote-move');
|
|
792
|
+
if (!raw) {
|
|
793
|
+
return null;
|
|
794
|
+
}
|
|
795
|
+
try {
|
|
796
|
+
const arr = JSON.parse(raw);
|
|
797
|
+
if (Array.isArray(arr)) {
|
|
798
|
+
return arr;
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
// ignore
|
|
803
|
+
}
|
|
804
|
+
return null;
|
|
805
|
+
}
|
|
806
|
+
formatSize(bytes) {
|
|
807
|
+
if (bytes === undefined || bytes === null) {
|
|
808
|
+
return '';
|
|
809
|
+
}
|
|
810
|
+
if (bytes === 0) {
|
|
811
|
+
return '0 B';
|
|
812
|
+
}
|
|
813
|
+
const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB'];
|
|
814
|
+
let value = bytes;
|
|
815
|
+
let unitIndex = 0;
|
|
816
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
817
|
+
value /= 1024;
|
|
818
|
+
unitIndex++;
|
|
819
|
+
}
|
|
820
|
+
const digits = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
821
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
822
|
+
}
|
|
823
|
+
formatSpeed(bytesPerSecond) {
|
|
824
|
+
if (bytesPerSecond === undefined || bytesPerSecond === null) {
|
|
825
|
+
return '';
|
|
826
|
+
}
|
|
827
|
+
if (bytesPerSecond === 0) {
|
|
828
|
+
return '0 B/s';
|
|
829
|
+
}
|
|
830
|
+
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s', 'TB/s', 'PB/s'];
|
|
831
|
+
let value = bytesPerSecond;
|
|
832
|
+
let unitIndex = 0;
|
|
833
|
+
while (value >= 1024 && unitIndex < units.length - 1) {
|
|
834
|
+
value /= 1024;
|
|
835
|
+
unitIndex++;
|
|
836
|
+
}
|
|
837
|
+
const digits = value >= 10 || unitIndex === 0 ? 0 : 1;
|
|
838
|
+
return `${value.toFixed(digits)} ${units[unitIndex]}`;
|
|
839
|
+
}
|
|
840
|
+
getLocalSizeDisplay(e) {
|
|
841
|
+
if (!e.isDirectory) {
|
|
842
|
+
return this.formatSize(e.size);
|
|
843
|
+
}
|
|
844
|
+
if (e.size !== undefined) {
|
|
845
|
+
return this.formatSize(e.size);
|
|
846
|
+
}
|
|
847
|
+
if (this.localFolderSizeLoading.has(e.fullPath)) {
|
|
848
|
+
return '…';
|
|
849
|
+
}
|
|
850
|
+
return '';
|
|
851
|
+
}
|
|
852
|
+
getRemoteSizeDisplay(e) {
|
|
853
|
+
if (!e.isDirectory) {
|
|
854
|
+
return this.formatSize(e.size);
|
|
855
|
+
}
|
|
856
|
+
const key = e.fullPath;
|
|
857
|
+
if (e.dirSize !== undefined) {
|
|
858
|
+
return this.formatSize(e.dirSize);
|
|
859
|
+
}
|
|
860
|
+
if (this.remoteFolderSizeLoading.has(key)) {
|
|
861
|
+
return '…';
|
|
862
|
+
}
|
|
863
|
+
return '';
|
|
864
|
+
}
|
|
865
|
+
onLocalEntryDragOver(entry, ev) {
|
|
866
|
+
if (!entry.isDirectory) {
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
if (!this.getLocalMovePayload(ev)) {
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
ev.preventDefault();
|
|
873
|
+
}
|
|
874
|
+
async onLocalEntryDrop(entry, ev) {
|
|
875
|
+
if (!entry.isDirectory) {
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
const sources = this.getLocalMovePayload(ev);
|
|
879
|
+
if (!sources || !sources.length) {
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
ev.preventDefault();
|
|
883
|
+
const targetDir = entry.fullPath;
|
|
884
|
+
try {
|
|
885
|
+
for (const src of sources) {
|
|
886
|
+
if (!src || src === targetDir) {
|
|
887
|
+
continue;
|
|
888
|
+
}
|
|
889
|
+
// avoid moving a directory into its own subtree
|
|
890
|
+
if (targetDir.startsWith(src + path__WEBPACK_IMPORTED_MODULE_0__.sep)) {
|
|
891
|
+
continue;
|
|
892
|
+
}
|
|
893
|
+
const name = path__WEBPACK_IMPORTED_MODULE_0__.basename(src);
|
|
894
|
+
const dst = path__WEBPACK_IMPORTED_MODULE_0__.join(targetDir, name);
|
|
895
|
+
if (dst === src) {
|
|
896
|
+
continue;
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(src, dst);
|
|
900
|
+
}
|
|
901
|
+
catch (e) {
|
|
902
|
+
console.error('[SFTP-UI] Local move failed', e);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
await this.refreshLocal();
|
|
906
|
+
}
|
|
907
|
+
catch (e) {
|
|
908
|
+
console.error('[SFTP-UI] Local move batch failed', e);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
onRemoteEntryDragOver(entry, ev) {
|
|
912
|
+
if (!entry.isDirectory) {
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if (!this.getRemoteMovePayload(ev)) {
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
ev.preventDefault();
|
|
919
|
+
}
|
|
920
|
+
async onRemoteEntryDrop(entry, ev) {
|
|
921
|
+
if (!entry.isDirectory || !this.sftpSession || !this.connected) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const sources = this.getRemoteMovePayload(ev);
|
|
925
|
+
if (!sources || !sources.length) {
|
|
926
|
+
return;
|
|
927
|
+
}
|
|
928
|
+
ev.preventDefault();
|
|
929
|
+
const targetDir = entry.fullPath;
|
|
930
|
+
try {
|
|
931
|
+
for (const src of sources) {
|
|
932
|
+
if (!src || src === targetDir) {
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
// avoid moving a directory into its own subtree
|
|
936
|
+
if (targetDir.startsWith(src + '/')) {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
const name = src.split('/').filter(Boolean).pop() || '';
|
|
940
|
+
if (!name) {
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
const dst = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(targetDir, name);
|
|
944
|
+
if (dst === src) {
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
try {
|
|
948
|
+
await this.sftpSession.rename(src, dst);
|
|
949
|
+
}
|
|
950
|
+
catch (e) {
|
|
951
|
+
console.error('[SFTP-UI] Remote move failed', e);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
await this.refreshRemote();
|
|
955
|
+
}
|
|
956
|
+
catch (e) {
|
|
957
|
+
console.error('[SFTP-UI] Remote move batch failed', e);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
getRemoteBreadcrumbs() {
|
|
961
|
+
const parts = this.remotePath.split('/').filter(Boolean);
|
|
962
|
+
const crumbs = [];
|
|
963
|
+
let current = '/';
|
|
964
|
+
crumbs.push({ label: '/', path: '/' });
|
|
965
|
+
for (const p of parts) {
|
|
966
|
+
current = current === '/' ? `/${p}` : `${current}/${p}`;
|
|
967
|
+
crumbs.push({ label: p, path: current });
|
|
968
|
+
}
|
|
969
|
+
return crumbs;
|
|
970
|
+
}
|
|
971
|
+
navigateRemoteBreadcrumb(index) {
|
|
972
|
+
const crumbs = this.getRemoteBreadcrumbs();
|
|
973
|
+
const crumb = crumbs[index];
|
|
974
|
+
if (!crumb) {
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
this.remotePath = crumb.path;
|
|
978
|
+
this.remotePathInput = this.remotePath;
|
|
979
|
+
void this.refreshRemote();
|
|
980
|
+
}
|
|
981
|
+
goToRemotePathInput() {
|
|
982
|
+
if (!this.connected) {
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
const target = this.normalizeRemotePath(this.remotePathInput || '/');
|
|
986
|
+
this.remotePath = target;
|
|
987
|
+
this.remotePathInput = target;
|
|
988
|
+
void this.refreshRemote();
|
|
989
|
+
}
|
|
990
|
+
goToLocalPathInput() {
|
|
991
|
+
const target = this.normalizeLocalPath(this.localPathInput || this.localPath);
|
|
992
|
+
this.goToLocalPath(target);
|
|
993
|
+
}
|
|
994
|
+
getLocalBreadcrumbs() {
|
|
995
|
+
const currentPath = this.localPath;
|
|
996
|
+
const parsed = path__WEBPACK_IMPORTED_MODULE_0__.parse(currentPath);
|
|
997
|
+
const root = parsed.root || path__WEBPACK_IMPORTED_MODULE_0__.sep;
|
|
998
|
+
const withoutRoot = currentPath.slice(root.length);
|
|
999
|
+
const parts = withoutRoot.split(path__WEBPACK_IMPORTED_MODULE_0__.sep).filter(Boolean);
|
|
1000
|
+
const crumbs = [];
|
|
1001
|
+
const rootLabel = root.replace(/[\\\/]+$/, '') || root;
|
|
1002
|
+
crumbs.push({ label: rootLabel, path: root });
|
|
1003
|
+
let accum = root;
|
|
1004
|
+
for (const p of parts) {
|
|
1005
|
+
accum = path__WEBPACK_IMPORTED_MODULE_0__.join(accum, p);
|
|
1006
|
+
crumbs.push({ label: p, path: accum });
|
|
1007
|
+
}
|
|
1008
|
+
return crumbs;
|
|
1009
|
+
}
|
|
1010
|
+
navigateLocalBreadcrumb(index) {
|
|
1011
|
+
const crumbs = this.getLocalBreadcrumbs();
|
|
1012
|
+
const crumb = crumbs[index];
|
|
1013
|
+
if (!crumb) {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
this.goToLocalPath(crumb.path);
|
|
1017
|
+
}
|
|
1018
|
+
goToLocalPath(target) {
|
|
1019
|
+
this.localPath = target;
|
|
1020
|
+
this.localPathInput = target;
|
|
1021
|
+
void this.refreshLocal();
|
|
1022
|
+
}
|
|
1023
|
+
onLocalPresetChange(id) {
|
|
1024
|
+
if (!id) {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
const preset = this.localPathPresets.find(p => p.id === id);
|
|
1028
|
+
if (!preset) {
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
this.goToLocalPath(preset.path);
|
|
1032
|
+
}
|
|
1033
|
+
isCurrentFavorite() {
|
|
1034
|
+
return this.localFavorites.some(f => f.path === this.localPath);
|
|
1035
|
+
}
|
|
1036
|
+
toggleCurrentFavorite() {
|
|
1037
|
+
const existingIndex = this.localFavorites.findIndex(f => f.path === this.localPath);
|
|
1038
|
+
if (existingIndex >= 0) {
|
|
1039
|
+
this.localFavorites.splice(existingIndex, 1);
|
|
1040
|
+
this.saveLocalFavorites();
|
|
1041
|
+
return;
|
|
1042
|
+
}
|
|
1043
|
+
const label = path__WEBPACK_IMPORTED_MODULE_0__.basename(this.localPath) || this.localPath;
|
|
1044
|
+
const id = `fav-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
|
|
1045
|
+
this.localFavorites.push({ id, label, path: this.localPath });
|
|
1046
|
+
this.saveLocalFavorites();
|
|
1047
|
+
}
|
|
1048
|
+
onLocalFavoriteSelect(id) {
|
|
1049
|
+
if (!id) {
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
const fav = this.localFavorites.find(f => f.id === id);
|
|
1053
|
+
if (!fav) {
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
this.goToLocalPath(fav.path);
|
|
1057
|
+
}
|
|
1058
|
+
onLocalBreadcrumbContextMenu(index, event) {
|
|
1059
|
+
event.preventDefault();
|
|
1060
|
+
event.stopPropagation();
|
|
1061
|
+
const crumbs = this.getLocalBreadcrumbs();
|
|
1062
|
+
const crumb = crumbs[index];
|
|
1063
|
+
if (!crumb) {
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
const menuItems = [];
|
|
1067
|
+
const isWindows = process.platform === 'win32';
|
|
1068
|
+
const isRootCrumb = index === 0;
|
|
1069
|
+
const basePath = crumb.path;
|
|
1070
|
+
// Root crumb on Windows: offer other drives as "siblings"
|
|
1071
|
+
if (isWindows && isRootCrumb) {
|
|
1072
|
+
const drives = [];
|
|
1073
|
+
for (let code = 67; code <= 90; code++) { // C..Z
|
|
1074
|
+
const letter = String.fromCharCode(code);
|
|
1075
|
+
const rootPath = `${letter}:\\`;
|
|
1076
|
+
try {
|
|
1077
|
+
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(rootPath)) {
|
|
1078
|
+
drives.push(rootPath);
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
catch {
|
|
1082
|
+
// ignore
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
for (const d of drives) {
|
|
1086
|
+
menuItems.push({ label: d, path: d });
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
// For non-root crumbs (or non-Windows), show sibling folders only
|
|
1091
|
+
const parentPath = path__WEBPACK_IMPORTED_MODULE_0__.dirname(basePath);
|
|
1092
|
+
try {
|
|
1093
|
+
const parentEntries = fs__WEBPACK_IMPORTED_MODULE_3__.readdirSync(parentPath);
|
|
1094
|
+
for (const name of parentEntries) {
|
|
1095
|
+
const full = path__WEBPACK_IMPORTED_MODULE_0__.join(parentPath, name);
|
|
1096
|
+
try {
|
|
1097
|
+
const st = fs__WEBPACK_IMPORTED_MODULE_3__.statSync(full);
|
|
1098
|
+
if (st.isDirectory()) {
|
|
1099
|
+
menuItems.push({ label: name, path: full });
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
catch {
|
|
1103
|
+
// ignore
|
|
1104
|
+
}
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
catch {
|
|
1108
|
+
// ignore
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (!menuItems.length) {
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
1114
|
+
this.localMenuItems = menuItems;
|
|
1115
|
+
this.localMenuVisible = true;
|
|
1116
|
+
this.localMenuX = event.clientX;
|
|
1117
|
+
this.localMenuY = event.clientY;
|
|
1118
|
+
}
|
|
1119
|
+
loadLocalFavorites() {
|
|
1120
|
+
try {
|
|
1121
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
const raw = window.localStorage.getItem('tabby-sftp-ui-local-favorites');
|
|
1125
|
+
if (!raw) {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
const parsed = JSON.parse(raw);
|
|
1129
|
+
if (Array.isArray(parsed)) {
|
|
1130
|
+
this.localFavorites = parsed
|
|
1131
|
+
.filter(f => f && typeof f.path === 'string')
|
|
1132
|
+
.map(f => ({
|
|
1133
|
+
id: String(f.id || `fav-${Math.random().toString(36).slice(2, 8)}`),
|
|
1134
|
+
label: String(f.label || path__WEBPACK_IMPORTED_MODULE_0__.basename(f.path) || f.path),
|
|
1135
|
+
path: String(f.path),
|
|
1136
|
+
}));
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
catch {
|
|
1140
|
+
// ignore
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
saveLocalFavorites() {
|
|
1144
|
+
try {
|
|
1145
|
+
if (typeof window === 'undefined' || !window.localStorage) {
|
|
1146
|
+
return;
|
|
1147
|
+
}
|
|
1148
|
+
window.localStorage.setItem('tabby-sftp-ui-local-favorites', JSON.stringify(this.localFavorites));
|
|
1149
|
+
}
|
|
1150
|
+
catch {
|
|
1151
|
+
// ignore
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
onLocalMenuItemClick(item) {
|
|
1155
|
+
this.localMenuVisible = false;
|
|
1156
|
+
this.goToLocalPath(item.path);
|
|
1157
|
+
}
|
|
1158
|
+
normalizeLocalPath(p) {
|
|
1159
|
+
if (!p) {
|
|
1160
|
+
return this.localPath;
|
|
1161
|
+
}
|
|
1162
|
+
let result = p.trim();
|
|
1163
|
+
// On Windows allow drive letters and backslashes, but normalize to current OS-style
|
|
1164
|
+
if (path__WEBPACK_IMPORTED_MODULE_0__.win32.isAbsolute(result) || path__WEBPACK_IMPORTED_MODULE_0__.posix.isAbsolute(result)) {
|
|
1165
|
+
return result;
|
|
1166
|
+
}
|
|
1167
|
+
// relative path from current localPath
|
|
1168
|
+
return path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, result);
|
|
1169
|
+
}
|
|
1170
|
+
normalizeRemotePath(p) {
|
|
1171
|
+
if (!p) {
|
|
1172
|
+
return '/';
|
|
1173
|
+
}
|
|
1174
|
+
let result = p.trim();
|
|
1175
|
+
if (!result.startsWith('/')) {
|
|
1176
|
+
result = '/' + result;
|
|
1177
|
+
}
|
|
1178
|
+
// remove duplicate slashes
|
|
1179
|
+
result = result.replace(/\/+/g, '/');
|
|
1180
|
+
return result;
|
|
1181
|
+
}
|
|
1182
|
+
getDefaultRemotePath() {
|
|
1183
|
+
const username = (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
|
|
1184
|
+
if (username) {
|
|
1185
|
+
return `/home/${username}`;
|
|
1186
|
+
}
|
|
1187
|
+
return '/';
|
|
1188
|
+
}
|
|
1189
|
+
loadRecentProfiles() {
|
|
1190
|
+
try {
|
|
1191
|
+
const rec = this.profilesService.getRecentProfiles?.();
|
|
1192
|
+
if (Array.isArray(rec)) {
|
|
1193
|
+
this.recentProfiles = rec;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
catch {
|
|
1197
|
+
this.recentProfiles = [];
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
getProfileLabel(p) {
|
|
1201
|
+
if (!p) {
|
|
1202
|
+
return '';
|
|
1203
|
+
}
|
|
1204
|
+
return p.name || p.options?.host || p.id || 'Profile';
|
|
1205
|
+
}
|
|
1206
|
+
launchProfileFromSFTP(p) {
|
|
1207
|
+
try {
|
|
1208
|
+
void this.profilesService.launchProfile(p);
|
|
1209
|
+
}
|
|
1210
|
+
catch (e) {
|
|
1211
|
+
console.error('[SFTP-UI] launchProfile failed', e);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
onDocumentClick() {
|
|
1215
|
+
this.localMenuVisible = false;
|
|
1216
|
+
}
|
|
1217
|
+
localRename() {
|
|
1218
|
+
if (this.selectedLocal.length !== 1 || !this.localActionName?.trim()) {
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
const entry = this.selectedLocal[0];
|
|
1222
|
+
const name = this.localActionName.trim();
|
|
1223
|
+
if (name === entry.name) {
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const target = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, name);
|
|
1227
|
+
void fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(entry.fullPath, target)
|
|
1228
|
+
.then(() => this.refreshLocal())
|
|
1229
|
+
.catch(e => console.error('[SFTP-UI] Local rename failed', e));
|
|
1230
|
+
}
|
|
1231
|
+
localDelete() {
|
|
1232
|
+
if (!this.selectedLocal.length) {
|
|
1233
|
+
return;
|
|
1234
|
+
}
|
|
1235
|
+
this.deleteConfirmMode = 'local';
|
|
1236
|
+
this.pendingLocalDelete = this.selectedLocal.slice();
|
|
1237
|
+
const names = this.pendingLocalDelete.map(e => e.name);
|
|
1238
|
+
this.deleteConfirmText = this.buildDeleteConfirmText('local', names);
|
|
1239
|
+
this.deleteConfirmVisible = true;
|
|
1240
|
+
}
|
|
1241
|
+
localNewFolder() {
|
|
1242
|
+
const name = (this.localActionName || 'New folder').trim();
|
|
1243
|
+
if (!name) {
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const target = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, name);
|
|
1247
|
+
void fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(target, { recursive: true })
|
|
1248
|
+
.then(() => this.refreshLocal())
|
|
1249
|
+
.catch(e => console.error('[SFTP-UI] Local mkdir failed', e));
|
|
1250
|
+
}
|
|
1251
|
+
localEditPermissions() {
|
|
1252
|
+
if (this.selectedLocal.length !== 1 || !this.localActionPerms?.trim()) {
|
|
1253
|
+
return;
|
|
1254
|
+
}
|
|
1255
|
+
const entry = this.selectedLocal[0];
|
|
1256
|
+
const mode = parseInt(this.localActionPerms.trim(), 8);
|
|
1257
|
+
if (Number.isNaN(mode)) {
|
|
1258
|
+
console.error('[SFTP-UI] Invalid local permissions value');
|
|
1259
|
+
return;
|
|
1260
|
+
}
|
|
1261
|
+
void fs_promises__WEBPACK_IMPORTED_MODULE_2__.chmod(entry.fullPath, mode)
|
|
1262
|
+
.then(() => this.refreshLocal())
|
|
1263
|
+
.catch(e => console.error('[SFTP-UI] Local chmod failed', e));
|
|
1264
|
+
}
|
|
1265
|
+
localShowSize() {
|
|
1266
|
+
if (this.selectedLocal.length === 1 && this.selectedLocal[0].isDirectory) {
|
|
1267
|
+
this.ensureLocalFolderSize(this.selectedLocal[0]);
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
remoteRename() {
|
|
1271
|
+
if (this.selectedRemote.length !== 1 || !this.remoteActionName?.trim() || !this.sftpSession) {
|
|
1272
|
+
return;
|
|
1273
|
+
}
|
|
1274
|
+
const entry = this.selectedRemote[0];
|
|
1275
|
+
const name = this.remoteActionName.trim();
|
|
1276
|
+
if (name === entry.name) {
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const target = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, name);
|
|
1280
|
+
void this.sftpSession.rename(entry.fullPath, target)
|
|
1281
|
+
.then(() => this.refreshRemote())
|
|
1282
|
+
.catch(e => console.error('[SFTP-UI] Remote rename failed', e));
|
|
1283
|
+
}
|
|
1284
|
+
remoteDelete() {
|
|
1285
|
+
if (!this.selectedRemote.length || !this.sftpSession) {
|
|
1286
|
+
return;
|
|
1287
|
+
}
|
|
1288
|
+
this.deleteConfirmMode = 'remote';
|
|
1289
|
+
this.pendingRemoteDelete = this.selectedRemote.slice();
|
|
1290
|
+
const names = this.pendingRemoteDelete.map(e => e.name);
|
|
1291
|
+
this.deleteConfirmText = this.buildDeleteConfirmText('remote', names);
|
|
1292
|
+
this.deleteConfirmVisible = true;
|
|
1293
|
+
}
|
|
1294
|
+
remoteNewFolder() {
|
|
1295
|
+
if (!this.sftpSession) {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
const name = (this.remoteActionName || 'New folder').trim();
|
|
1299
|
+
if (!name) {
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
const target = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, name);
|
|
1303
|
+
void this.sftpSession.mkdir(target)
|
|
1304
|
+
.then(() => this.refreshRemote())
|
|
1305
|
+
.catch(e => console.error('[SFTP-UI] Remote mkdir failed', e));
|
|
1306
|
+
}
|
|
1307
|
+
remoteEditPermissions() {
|
|
1308
|
+
if (this.selectedRemote.length !== 1 || !this.remoteActionPerms?.trim() || !this.sftpSession) {
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
const entry = this.selectedRemote[0];
|
|
1312
|
+
const mode = parseInt(this.remoteActionPerms.trim(), 8);
|
|
1313
|
+
if (Number.isNaN(mode)) {
|
|
1314
|
+
console.error('[SFTP-UI] Invalid remote permissions value');
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
void this.sftpSession.chmod(entry.fullPath, mode)
|
|
1318
|
+
.then(() => this.refreshRemote())
|
|
1319
|
+
.catch((e) => console.error('[SFTP-UI] Remote chmod failed', e));
|
|
1320
|
+
}
|
|
1321
|
+
remoteShowSize() {
|
|
1322
|
+
if (this.selectedRemote.length === 1 && this.selectedRemote[0].isDirectory) {
|
|
1323
|
+
this.ensureRemoteFolderSize(this.selectedRemote[0]);
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
remoteDownload() {
|
|
1327
|
+
if (!this.selectedRemote.length || !this.sftpSession) {
|
|
1328
|
+
return;
|
|
1329
|
+
}
|
|
1330
|
+
for (const entry of this.selectedRemote) {
|
|
1331
|
+
if (entry.isDirectory) {
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, entry.name);
|
|
1335
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
|
|
1336
|
+
this.trackTransfer(dl, 'download', entry.fullPath, targetLocalPath);
|
|
1337
|
+
void this.sftpSession.download(entry.fullPath, dl)
|
|
1338
|
+
.then(() => this.refreshLocal())
|
|
1339
|
+
.catch(e => console.error('[SFTP-UI] Remote download failed', e));
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
ensureLocalFolderSize(entry) {
|
|
1343
|
+
if (!entry.isDirectory) {
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (entry.size !== undefined) {
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
if (this.localFolderSizeLoading.has(entry.fullPath)) {
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
this.localFolderSizeLoading.add(entry.fullPath);
|
|
1353
|
+
void this.computeLocalFolderSize(entry.fullPath)
|
|
1354
|
+
.then(size => {
|
|
1355
|
+
entry.size = size;
|
|
1356
|
+
})
|
|
1357
|
+
.catch(e => {
|
|
1358
|
+
console.error('[SFTP-UI] Local folder size failed', e);
|
|
1359
|
+
})
|
|
1360
|
+
.finally(() => {
|
|
1361
|
+
this.localFolderSizeLoading.delete(entry.fullPath);
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
ensureRemoteFolderSize(entry) {
|
|
1365
|
+
if (!entry.isDirectory) {
|
|
1366
|
+
return;
|
|
1367
|
+
}
|
|
1368
|
+
const key = entry.fullPath;
|
|
1369
|
+
if (entry.dirSize !== undefined) {
|
|
1370
|
+
return;
|
|
1371
|
+
}
|
|
1372
|
+
if (this.remoteFolderSizeLoading.has(key)) {
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
if (!this.sftpSession || !this.connected) {
|
|
1376
|
+
return;
|
|
1377
|
+
}
|
|
1378
|
+
this.remoteFolderSizeLoading.add(key);
|
|
1379
|
+
void this.computeRemoteFolderSize(key)
|
|
1380
|
+
.then(size => {
|
|
1381
|
+
;
|
|
1382
|
+
entry.dirSize = size;
|
|
1383
|
+
})
|
|
1384
|
+
.catch(e => {
|
|
1385
|
+
console.error('[SFTP-UI] Remote folder size failed', e);
|
|
1386
|
+
})
|
|
1387
|
+
.finally(() => {
|
|
1388
|
+
this.remoteFolderSizeLoading.delete(key);
|
|
1389
|
+
});
|
|
1390
|
+
}
|
|
1391
|
+
async computeLocalFolderSize(root) {
|
|
1392
|
+
let total = 0;
|
|
1393
|
+
const stack = [root];
|
|
1394
|
+
const maxEntries = 5000;
|
|
1395
|
+
let visited = 0;
|
|
1396
|
+
while (stack.length) {
|
|
1397
|
+
const dir = stack.pop();
|
|
1398
|
+
let names;
|
|
1399
|
+
try {
|
|
1400
|
+
names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(dir);
|
|
1401
|
+
}
|
|
1402
|
+
catch {
|
|
1403
|
+
continue;
|
|
1404
|
+
}
|
|
1405
|
+
for (const name of names) {
|
|
1406
|
+
if (visited++ > maxEntries) {
|
|
1407
|
+
return total;
|
|
1408
|
+
}
|
|
1409
|
+
const full = path__WEBPACK_IMPORTED_MODULE_0__.join(dir, name);
|
|
1410
|
+
try {
|
|
1411
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(full);
|
|
1412
|
+
if (st.isDirectory()) {
|
|
1413
|
+
stack.push(full);
|
|
1414
|
+
}
|
|
1415
|
+
else {
|
|
1416
|
+
total += st.size;
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
catch {
|
|
1420
|
+
// ignore
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
return total;
|
|
1425
|
+
}
|
|
1426
|
+
async computeRemoteFolderSize(root) {
|
|
1427
|
+
if (!this.sftpSession) {
|
|
1428
|
+
return 0;
|
|
1429
|
+
}
|
|
1430
|
+
let total = 0;
|
|
1431
|
+
const stack = [root];
|
|
1432
|
+
const maxEntries = 5000;
|
|
1433
|
+
let visited = 0;
|
|
1434
|
+
while (stack.length) {
|
|
1435
|
+
const dir = stack.pop();
|
|
1436
|
+
let entries;
|
|
1437
|
+
try {
|
|
1438
|
+
entries = await this.sftpSession.readdir(dir);
|
|
1439
|
+
}
|
|
1440
|
+
catch {
|
|
1441
|
+
continue;
|
|
1442
|
+
}
|
|
1443
|
+
for (const item of entries) {
|
|
1444
|
+
if (visited++ > maxEntries) {
|
|
1445
|
+
return total;
|
|
1446
|
+
}
|
|
1447
|
+
if (item.isDirectory) {
|
|
1448
|
+
stack.push(item.fullPath);
|
|
1449
|
+
}
|
|
1450
|
+
else {
|
|
1451
|
+
total += item.size;
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
return total;
|
|
1456
|
+
}
|
|
1457
|
+
onLocalContextMenu(entry, event) {
|
|
1458
|
+
event.preventDefault();
|
|
1459
|
+
event.stopPropagation();
|
|
1460
|
+
// TODO: полноценное контекстное меню. Пока все действия — через нижнюю панель.
|
|
1461
|
+
}
|
|
1462
|
+
onRemoteContextMenu(entry, event) {
|
|
1463
|
+
event.preventDefault();
|
|
1464
|
+
event.stopPropagation();
|
|
1465
|
+
if (!this.sftpSession) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
// TODO: полноценное контекстное меню. Пока все действия — через нижнюю панель.
|
|
1469
|
+
}
|
|
1470
|
+
trackTransfer(transfer, direction, remotePath, localPath) {
|
|
1471
|
+
this.transfers.push({
|
|
1472
|
+
transfer,
|
|
1473
|
+
direction,
|
|
1474
|
+
name: transfer.getName(),
|
|
1475
|
+
remotePath,
|
|
1476
|
+
localPath,
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
cancelTransfer(entry) {
|
|
1480
|
+
try {
|
|
1481
|
+
if (entry.transfer.isComplete() || entry.transfer.isCancelled()) {
|
|
1482
|
+
return;
|
|
1483
|
+
}
|
|
1484
|
+
entry.transfer.cancel?.();
|
|
1485
|
+
}
|
|
1486
|
+
catch (e) {
|
|
1487
|
+
console.error('[SFTP-UI] Cancel transfer failed', e);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
getTransferProgress(transfer) {
|
|
1491
|
+
try {
|
|
1492
|
+
const total = transfer.getSize?.();
|
|
1493
|
+
const done = transfer.getCompletedBytes?.();
|
|
1494
|
+
if (typeof total !== 'number' || total <= 0 || typeof done !== 'number' || done < 0) {
|
|
1495
|
+
return transfer.isComplete() ? 100 : 0;
|
|
1496
|
+
}
|
|
1497
|
+
const value = (done / total) * 100;
|
|
1498
|
+
const clamped = Math.max(0, Math.min(100, value));
|
|
1499
|
+
return clamped;
|
|
1500
|
+
}
|
|
1501
|
+
catch {
|
|
1502
|
+
return transfer.isComplete() ? 100 : 0;
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
onKeyDown(event) {
|
|
1506
|
+
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
1507
|
+
event.preventDefault();
|
|
1508
|
+
if (this.selectedRemote.length) {
|
|
1509
|
+
this.remoteDelete();
|
|
1510
|
+
}
|
|
1511
|
+
else if (this.selectedLocal.length) {
|
|
1512
|
+
this.localDelete();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
destroy() {
|
|
1517
|
+
// stop file watchers for opened remote files
|
|
1518
|
+
for (const { watcher } of this.openedRemoteFiles.values()) {
|
|
1519
|
+
try {
|
|
1520
|
+
watcher?.close();
|
|
1521
|
+
}
|
|
1522
|
+
catch {
|
|
1523
|
+
// ignore
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
this.openedRemoteFiles.clear();
|
|
1527
|
+
void this.disconnect();
|
|
1528
|
+
if (this.transfersTimer !== null) {
|
|
1529
|
+
clearInterval(this.transfersTimer);
|
|
1530
|
+
this.transfersTimer = null;
|
|
1531
|
+
}
|
|
1532
|
+
super.destroy();
|
|
1533
|
+
}
|
|
1534
|
+
// Prevent Tabby from restoring SFTP-UI tabs across restarts, since they rely
|
|
1535
|
+
// on a live SSH session from a terminal tab.
|
|
1536
|
+
// Typинги допускают RecoveryToken | null, нам достаточно всегда возвращать null.
|
|
1537
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1538
|
+
async getRecoveryToken(_options) {
|
|
1539
|
+
return null;
|
|
1540
|
+
}
|
|
1541
|
+
async confirmDelete() {
|
|
1542
|
+
if (!this.deleteConfirmVisible) {
|
|
1543
|
+
return;
|
|
1544
|
+
}
|
|
1545
|
+
const mode = this.deleteConfirmMode;
|
|
1546
|
+
this.deleteConfirmVisible = false;
|
|
1547
|
+
try {
|
|
1548
|
+
if (mode === 'local') {
|
|
1549
|
+
const toDelete = this.pendingLocalDelete.slice();
|
|
1550
|
+
this.pendingLocalDelete = [];
|
|
1551
|
+
for (const entry of toDelete) {
|
|
1552
|
+
await this.deleteLocalEntry(entry);
|
|
1553
|
+
}
|
|
1554
|
+
await this.refreshLocal();
|
|
1555
|
+
this.selectedLocal = [];
|
|
1556
|
+
}
|
|
1557
|
+
else if (mode === 'remote' && this.sftpSession) {
|
|
1558
|
+
const toDelete = this.pendingRemoteDelete.slice();
|
|
1559
|
+
this.pendingRemoteDelete = [];
|
|
1560
|
+
for (const entry of toDelete) {
|
|
1561
|
+
await this.deleteRemoteEntry(entry);
|
|
1562
|
+
}
|
|
1563
|
+
await this.refreshRemote();
|
|
1564
|
+
this.selectedRemote = [];
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
catch (e) {
|
|
1568
|
+
console.error('[SFTP-UI] Delete failed', e);
|
|
1569
|
+
}
|
|
1570
|
+
finally {
|
|
1571
|
+
this.deleteConfirmMode = null;
|
|
1572
|
+
this.deleteConfirmText = '';
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
cancelDelete() {
|
|
1576
|
+
this.deleteConfirmVisible = false;
|
|
1577
|
+
this.deleteConfirmMode = null;
|
|
1578
|
+
this.deleteConfirmText = '';
|
|
1579
|
+
this.pendingLocalDelete = [];
|
|
1580
|
+
this.pendingRemoteDelete = [];
|
|
1581
|
+
}
|
|
1582
|
+
async deleteLocalEntry(entry) {
|
|
1583
|
+
await this.deleteLocalPathRecursive(entry.fullPath);
|
|
1584
|
+
}
|
|
1585
|
+
async deleteRemoteEntry(entry) {
|
|
1586
|
+
if (!this.sftpSession) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
await this.deleteRemotePathRecursive(entry.fullPath);
|
|
1590
|
+
}
|
|
1591
|
+
buildDeleteConfirmText(scope, names) {
|
|
1592
|
+
const total = names.length;
|
|
1593
|
+
const label = scope === 'local' ? 'local' : 'remote';
|
|
1594
|
+
if (!total) {
|
|
1595
|
+
return `Delete 0 item(s) from ${label}?`;
|
|
1596
|
+
}
|
|
1597
|
+
const maxShown = 5;
|
|
1598
|
+
const shown = names.slice(0, maxShown);
|
|
1599
|
+
const list = shown.join(', ');
|
|
1600
|
+
if (total <= maxShown) {
|
|
1601
|
+
return `Delete ${total} item(s) from ${label}: ${list}?`;
|
|
1602
|
+
}
|
|
1603
|
+
const rest = total - maxShown;
|
|
1604
|
+
return `Delete ${total} item(s) from ${label}: ${list} and ${rest} more?`;
|
|
1605
|
+
}
|
|
1606
|
+
async deleteLocalPathRecursive(target) {
|
|
1607
|
+
try {
|
|
1608
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(target);
|
|
1609
|
+
if (!st.isDirectory()) {
|
|
1610
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.unlink(target);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
catch (e) {
|
|
1615
|
+
console.error('[SFTP-UI] Local delete failed (stat)', e);
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
try {
|
|
1619
|
+
const names = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(target);
|
|
1620
|
+
for (const name of names) {
|
|
1621
|
+
const child = path__WEBPACK_IMPORTED_MODULE_0__.join(target, name);
|
|
1622
|
+
await this.deleteLocalPathRecursive(child);
|
|
1623
|
+
}
|
|
1624
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.rmdir(target);
|
|
1625
|
+
}
|
|
1626
|
+
catch (e) {
|
|
1627
|
+
console.error('[SFTP-UI] Local recursive delete failed', e);
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
async deleteRemotePathRecursive(target) {
|
|
1631
|
+
if (!this.sftpSession) {
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
try {
|
|
1635
|
+
const entries = await this.sftpSession.readdir(target).catch(() => null);
|
|
1636
|
+
if (!entries) {
|
|
1637
|
+
// treat as file
|
|
1638
|
+
try {
|
|
1639
|
+
await this.sftpSession.unlink(target);
|
|
1640
|
+
}
|
|
1641
|
+
catch (e) {
|
|
1642
|
+
console.error('[SFTP-UI] Remote delete failed', e);
|
|
1643
|
+
}
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
for (const item of entries) {
|
|
1647
|
+
const full = item.fullPath;
|
|
1648
|
+
if (item.isDirectory) {
|
|
1649
|
+
await this.deleteRemotePathRecursive(full);
|
|
1650
|
+
}
|
|
1651
|
+
else {
|
|
1652
|
+
try {
|
|
1653
|
+
await this.sftpSession.unlink(full);
|
|
1654
|
+
}
|
|
1655
|
+
catch (e) {
|
|
1656
|
+
console.error('[SFTP-UI] Remote unlink failed', e);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
}
|
|
1660
|
+
try {
|
|
1661
|
+
await this.sftpSession.rmdir(target);
|
|
1662
|
+
}
|
|
1663
|
+
catch (e) {
|
|
1664
|
+
console.error('[SFTP-UI] Remote rmdir failed', e);
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
catch (e) {
|
|
1668
|
+
console.error('[SFTP-UI] Remote recursive delete failed', e);
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
async openRemoteFile(entry) {
|
|
1672
|
+
if (!this.sftpSession || !this.connected || entry.isDirectory) {
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const tmpRoot = path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'tabby-sftp-ui');
|
|
1677
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(tmpRoot, { recursive: true });
|
|
1678
|
+
const localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, entry.name);
|
|
1679
|
+
// если уже есть watcher на этот файл – закроем его и перезапишем
|
|
1680
|
+
const existing = this.openedRemoteFiles.get(localPath);
|
|
1681
|
+
if (existing?.watcher) {
|
|
1682
|
+
try {
|
|
1683
|
+
existing.watcher.close();
|
|
1684
|
+
}
|
|
1685
|
+
catch {
|
|
1686
|
+
// ignore
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(localPath, entry.mode, entry.size);
|
|
1690
|
+
this.trackTransfer(dl, 'download', entry.fullPath, localPath);
|
|
1691
|
+
await this.sftpSession.download(entry.fullPath, dl);
|
|
1692
|
+
// настроим наблюдение за изменениями локального файла
|
|
1693
|
+
const watcher = fs__WEBPACK_IMPORTED_MODULE_3__.watch(localPath, { persistent: false }, (eventType) => {
|
|
1694
|
+
if (eventType === 'change') {
|
|
1695
|
+
void this.syncBackRemoteFile(localPath);
|
|
1696
|
+
}
|
|
1697
|
+
});
|
|
1698
|
+
this.openedRemoteFiles.set(localPath, { remotePath: entry.fullPath, mode: entry.mode, watcher });
|
|
1699
|
+
this.platform.openPath(localPath);
|
|
1700
|
+
}
|
|
1701
|
+
catch (e) {
|
|
1702
|
+
console.error('[SFTP-UI] Open remote file failed', e);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
async syncBackRemoteFile(localPath) {
|
|
1706
|
+
if (!this.sftpSession || !this.connected) {
|
|
1707
|
+
return;
|
|
1708
|
+
}
|
|
1709
|
+
const info = this.openedRemoteFiles.get(localPath);
|
|
1710
|
+
if (!info) {
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
try {
|
|
1714
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(localPath);
|
|
1715
|
+
this.trackTransfer(upload, 'upload', info.remotePath, localPath);
|
|
1716
|
+
await this.sftpSession.upload(info.remotePath, upload);
|
|
1717
|
+
await this.refreshRemote();
|
|
1718
|
+
}
|
|
1719
|
+
catch (e) {
|
|
1720
|
+
console.error('[SFTP-UI] Sync-back remote file failed', e);
|
|
1721
|
+
}
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
__decorate([
|
|
1725
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_4__.HostListener)('document:click'),
|
|
1726
|
+
__metadata("design:type", Function),
|
|
1727
|
+
__metadata("design:paramtypes", []),
|
|
1728
|
+
__metadata("design:returntype", void 0)
|
|
1729
|
+
], SftpManagerTabComponent.prototype, "onDocumentClick", null);
|
|
1730
|
+
SftpManagerTabComponent = __decorate([
|
|
1731
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_4__.Component)({
|
|
1732
|
+
selector: 'tabby-sftp-manager-tab',
|
|
1733
|
+
template: `
|
|
1734
|
+
<div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
|
|
1735
|
+
<div class="top-profiles" *ngIf="profile || recentProfiles.length">
|
|
1736
|
+
<div class="current" *ngIf="profile">
|
|
1737
|
+
<span class="label">Device:</span>
|
|
1738
|
+
<span class="value">{{ getProfileLabel(profile) }}</span>
|
|
1739
|
+
</div>
|
|
1740
|
+
<div class="recent" *ngIf="recentProfiles.length">
|
|
1741
|
+
<span class="label">Recent:</span>
|
|
1742
|
+
<button
|
|
1743
|
+
class="profile-chip"
|
|
1744
|
+
*ngFor="let p of recentProfiles"
|
|
1745
|
+
(click)="launchProfileFromSFTP(p)"
|
|
1746
|
+
>
|
|
1747
|
+
{{ getProfileLabel(p) }}
|
|
1748
|
+
</button>
|
|
1749
|
+
</div>
|
|
1750
|
+
</div>
|
|
1751
|
+
<div class="sftp-body">
|
|
1752
|
+
<div class="pane">
|
|
1753
|
+
<div class="pane-title">
|
|
1754
|
+
<div class="pane-label">Local</div>
|
|
1755
|
+
<div class="pane-path">
|
|
1756
|
+
<input
|
|
1757
|
+
[(ngModel)]="localPathInput"
|
|
1758
|
+
(keyup.enter)="goToLocalPathInput()"
|
|
1759
|
+
/>
|
|
1760
|
+
</div>
|
|
1761
|
+
<div class="pane-actions">
|
|
1762
|
+
<select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
|
|
1763
|
+
<option value="">Go to…</option>
|
|
1764
|
+
<option *ngFor="let p of localPathPresets" [value]="p.id">
|
|
1765
|
+
{{ p.label }}
|
|
1766
|
+
</option>
|
|
1767
|
+
</select>
|
|
1768
|
+
<button
|
|
1769
|
+
class="fav-toggle"
|
|
1770
|
+
[class.active]="isCurrentFavorite()"
|
|
1771
|
+
(click)="toggleCurrentFavorite()"
|
|
1772
|
+
title="Toggle favorite for this path"
|
|
1773
|
+
>
|
|
1774
|
+
★
|
|
1775
|
+
</button>
|
|
1776
|
+
<select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
|
|
1777
|
+
<option value="">Favorites…</option>
|
|
1778
|
+
<option *ngFor="let f of localFavorites" [value]="f.id">
|
|
1779
|
+
{{ f.label }}
|
|
1780
|
+
</option>
|
|
1781
|
+
</select>
|
|
1782
|
+
<button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
|
|
1783
|
+
<button (click)="goToLocalPathInput()">Go</button>
|
|
1784
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
1785
|
+
</div>
|
|
1786
|
+
</div>
|
|
1787
|
+
<div class="pane-filters">
|
|
1788
|
+
<div class="breadcrumbs">
|
|
1789
|
+
<ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
|
|
1790
|
+
<button
|
|
1791
|
+
class="crumb-button"
|
|
1792
|
+
(click)="navigateLocalBreadcrumb(i)"
|
|
1793
|
+
(contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
|
|
1794
|
+
>
|
|
1795
|
+
{{ part.label }}
|
|
1796
|
+
</button>
|
|
1797
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
1798
|
+
</ng-container>
|
|
1799
|
+
</div>
|
|
1800
|
+
<input [(ngModel)]="localFilter" placeholder="Filter files..." />
|
|
1801
|
+
<label class="show-hidden-toggle">
|
|
1802
|
+
<input type="checkbox" [(ngModel)]="showHiddenLocal" />
|
|
1803
|
+
<span>Show hidden</span>
|
|
1804
|
+
</label>
|
|
1805
|
+
</div>
|
|
1806
|
+
<div class="pane-list"
|
|
1807
|
+
(dragover)="onDragOver($event)"
|
|
1808
|
+
(drop)="onDropOnLocal($event)"
|
|
1809
|
+
>
|
|
1810
|
+
<div class="entry header">
|
|
1811
|
+
<span class="icon"></span>
|
|
1812
|
+
<span class="name sortable" (click)="setLocalSort('name')">Name</span>
|
|
1813
|
+
<span class="size sortable" (click)="setLocalSort('size')">Size</span>
|
|
1814
|
+
<span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
|
|
1815
|
+
</div>
|
|
1816
|
+
<div
|
|
1817
|
+
class="entry"
|
|
1818
|
+
*ngFor="let e of getFilteredLocalEntries()"
|
|
1819
|
+
(click)="selectLocal(e, $event)"
|
|
1820
|
+
(dblclick)="openLocal(e)"
|
|
1821
|
+
(mousedown)="onLocalMouseDown(e, $event)"
|
|
1822
|
+
(contextmenu)="onLocalContextMenu(e, $event)"
|
|
1823
|
+
(dragover)="onLocalEntryDragOver(e, $event)"
|
|
1824
|
+
(drop)="onLocalEntryDrop(e, $event)"
|
|
1825
|
+
[class.drop-target]="localDropActive"
|
|
1826
|
+
[class.selected]="isLocalSelected(e)"
|
|
1827
|
+
[draggable]="true"
|
|
1828
|
+
(dragstart)="onDragStartLocal($event, e)"
|
|
1829
|
+
>
|
|
1830
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
1831
|
+
<span class="name">{{ e.name }}</span>
|
|
1832
|
+
<span class="size">{{ getLocalSizeDisplay(e) }}</span>
|
|
1833
|
+
<span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
|
|
1834
|
+
</div>
|
|
1835
|
+
</div>
|
|
1836
|
+
<div class="pane-actions-bar">
|
|
1837
|
+
<div class="selection" *ngIf="selectedLocal.length">
|
|
1838
|
+
Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
|
|
1839
|
+
</div>
|
|
1840
|
+
<div class="action-inputs">
|
|
1841
|
+
<input [(ngModel)]="localActionName" placeholder="Name / new name" />
|
|
1842
|
+
<input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
|
|
1843
|
+
</div>
|
|
1844
|
+
<div class="action-buttons">
|
|
1845
|
+
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1 || !localActionName">Rename</button>
|
|
1846
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
1847
|
+
<button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
|
|
1848
|
+
<button (click)="localNewFolder()">New Folder</button>
|
|
1849
|
+
<button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
|
|
1850
|
+
<button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
|
|
1851
|
+
</div>
|
|
1852
|
+
</div>
|
|
1853
|
+
</div>
|
|
1854
|
+
|
|
1855
|
+
<div class="pane">
|
|
1856
|
+
<div class="pane-title">
|
|
1857
|
+
<div class="pane-label">
|
|
1858
|
+
Remote
|
|
1859
|
+
<span *ngIf="connected && profile?.options?.host" class="pane-sub">
|
|
1860
|
+
— {{ profile.options.host }}
|
|
1861
|
+
</span>
|
|
1862
|
+
</div>
|
|
1863
|
+
<div class="pane-path">
|
|
1864
|
+
<input
|
|
1865
|
+
[(ngModel)]="remotePathInput"
|
|
1866
|
+
(keyup.enter)="goToRemotePathInput()"
|
|
1867
|
+
[disabled]="!connected"
|
|
1868
|
+
/>
|
|
1869
|
+
</div>
|
|
1870
|
+
<div class="pane-actions">
|
|
1871
|
+
<button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
|
|
1872
|
+
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
1873
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
1874
|
+
</div>
|
|
1875
|
+
</div>
|
|
1876
|
+
<div class="pane-filters">
|
|
1877
|
+
<div class="breadcrumbs" *ngIf="connected">
|
|
1878
|
+
<ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
|
|
1879
|
+
<button
|
|
1880
|
+
class="crumb-button"
|
|
1881
|
+
(click)="navigateRemoteBreadcrumb(i)"
|
|
1882
|
+
>
|
|
1883
|
+
{{ part.label }}
|
|
1884
|
+
</button>
|
|
1885
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
1886
|
+
</ng-container>
|
|
1887
|
+
</div>
|
|
1888
|
+
<input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
|
|
1889
|
+
<label class="show-hidden-toggle">
|
|
1890
|
+
<input type="checkbox" [(ngModel)]="showHiddenRemote" />
|
|
1891
|
+
<span>Show hidden</span>
|
|
1892
|
+
</label>
|
|
1893
|
+
</div>
|
|
1894
|
+
<div class="pane-list"
|
|
1895
|
+
(dragover)="onDragOver($event)"
|
|
1896
|
+
(drop)="onDropOnRemote($event)"
|
|
1897
|
+
>
|
|
1898
|
+
<div class="entry dim" *ngIf="!connected">
|
|
1899
|
+
<span class="name">Not connected</span>
|
|
1900
|
+
</div>
|
|
1901
|
+
<div class="entry header" *ngIf="connected">
|
|
1902
|
+
<span class="icon"></span>
|
|
1903
|
+
<span class="name sortable" (click)="setRemoteSort('name')">Name</span>
|
|
1904
|
+
<span class="size sortable" (click)="setRemoteSort('size')">Size</span>
|
|
1905
|
+
<span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
|
|
1906
|
+
</div>
|
|
1907
|
+
<div
|
|
1908
|
+
class="entry"
|
|
1909
|
+
*ngIf="connected && remotePath !== '/'"
|
|
1910
|
+
(dblclick)="remoteUp()"
|
|
1911
|
+
>
|
|
1912
|
+
<span class="icon">⬆</span>
|
|
1913
|
+
<span class="name">Go up</span>
|
|
1914
|
+
<span class="size"></span>
|
|
1915
|
+
<span class="date"></span>
|
|
1916
|
+
</div>
|
|
1917
|
+
<div
|
|
1918
|
+
class="entry"
|
|
1919
|
+
*ngFor="let e of getFilteredRemoteEntries()"
|
|
1920
|
+
(click)="selectRemote(e, $event)"
|
|
1921
|
+
(dblclick)="openRemote(e)"
|
|
1922
|
+
(mousedown)="onRemoteMouseDown(e, $event)"
|
|
1923
|
+
(contextmenu)="onRemoteContextMenu(e, $event)"
|
|
1924
|
+
(dragover)="onRemoteEntryDragOver(e, $event)"
|
|
1925
|
+
(drop)="onRemoteEntryDrop(e, $event)"
|
|
1926
|
+
[class.drop-target]="remoteDropActive"
|
|
1927
|
+
[class.selected]="isRemoteSelected(e)"
|
|
1928
|
+
[draggable]="connected"
|
|
1929
|
+
(dragstart)="onDragStartRemote($event, e)"
|
|
1930
|
+
>
|
|
1931
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
1932
|
+
<span class="name">{{ e.name }}</span>
|
|
1933
|
+
<span class="size">{{ getRemoteSizeDisplay(e) }}</span>
|
|
1934
|
+
<span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
|
|
1935
|
+
</div>
|
|
1936
|
+
</div>
|
|
1937
|
+
<div class="pane-actions-bar">
|
|
1938
|
+
<div class="selection" *ngIf="selectedRemote.length">
|
|
1939
|
+
Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
|
|
1940
|
+
</div>
|
|
1941
|
+
<div class="action-inputs">
|
|
1942
|
+
<input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
|
|
1943
|
+
<input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
|
|
1944
|
+
</div>
|
|
1945
|
+
<div class="action-buttons">
|
|
1946
|
+
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1 || !remoteActionName">Rename</button>
|
|
1947
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
1948
|
+
<button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
|
|
1949
|
+
<button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
|
|
1950
|
+
<button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
|
|
1951
|
+
<button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
|
|
1952
|
+
<button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>
|
|
1955
|
+
</div>
|
|
1956
|
+
</div>
|
|
1957
|
+
<div class="sftp-transfers" *ngIf="transfers.length">
|
|
1958
|
+
<div class="transfer" *ngFor="let t of transfers">
|
|
1959
|
+
<div class="transfer-main">
|
|
1960
|
+
<div class="transfer-title">
|
|
1961
|
+
<span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
|
|
1962
|
+
<span class="name">{{ t.name }}</span>
|
|
1963
|
+
</div>
|
|
1964
|
+
<div class="transfer-path">
|
|
1965
|
+
<span class="label">Remote:</span>
|
|
1966
|
+
<span class="value">{{ t.remotePath }}</span>
|
|
1967
|
+
</div>
|
|
1968
|
+
<div class="transfer-path">
|
|
1969
|
+
<span class="label">Local:</span>
|
|
1970
|
+
<span class="value">{{ t.localPath }}</span>
|
|
1971
|
+
</div>
|
|
1972
|
+
<div class="bar">
|
|
1973
|
+
<div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
|
|
1974
|
+
</div>
|
|
1975
|
+
</div>
|
|
1976
|
+
<div class="transfer-stats">
|
|
1977
|
+
<div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
|
|
1978
|
+
<div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
|
|
1979
|
+
<button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
|
|
1980
|
+
</div>
|
|
1981
|
+
</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
|
|
1984
|
+
<div class="delete-overlay" *ngIf="deleteConfirmVisible">
|
|
1985
|
+
<div class="delete-dialog">
|
|
1986
|
+
<div class="delete-text">{{ deleteConfirmText }}</div>
|
|
1987
|
+
<div class="delete-buttons">
|
|
1988
|
+
<button class="danger" (click)="confirmDelete()">Delete</button>
|
|
1989
|
+
<button (click)="cancelDelete()">Cancel</button>
|
|
1990
|
+
</div>
|
|
1991
|
+
</div>
|
|
1992
|
+
</div>
|
|
1993
|
+
|
|
1994
|
+
<div
|
|
1995
|
+
class="local-menu"
|
|
1996
|
+
*ngIf="localMenuVisible"
|
|
1997
|
+
[style.left.px]="localMenuX"
|
|
1998
|
+
[style.top.px]="localMenuY"
|
|
1999
|
+
(click)="$event.stopPropagation()"
|
|
2000
|
+
>
|
|
2001
|
+
<div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
|
|
2002
|
+
{{ item.label }}
|
|
2003
|
+
</div>
|
|
2004
|
+
</div>
|
|
2005
|
+
</div>
|
|
2006
|
+
`,
|
|
2007
|
+
styles: [`
|
|
2008
|
+
.sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
|
|
2009
|
+
button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
|
|
2010
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
2011
|
+
.top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
|
|
2012
|
+
.top-profiles .current .label,
|
|
2013
|
+
.top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
|
|
2014
|
+
.top-profiles .value { font-weight: 600; }
|
|
2015
|
+
.top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
|
|
2016
|
+
.top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
|
|
2017
|
+
.sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
|
|
2018
|
+
.pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
|
|
2019
|
+
.pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
|
|
2020
|
+
.pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
|
|
2021
|
+
.pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
|
|
2022
|
+
.pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2023
|
+
.pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
|
|
2024
|
+
.pane-actions { display: flex; gap: 8px; align-items: center; }
|
|
2025
|
+
.pane-actions .path-preset,
|
|
2026
|
+
.pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
|
|
2027
|
+
.pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
|
|
2028
|
+
.pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
|
|
2029
|
+
.pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
|
|
2030
|
+
.pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
|
|
2031
|
+
.pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
|
|
2032
|
+
.pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
|
|
2033
|
+
.show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
|
|
2034
|
+
.show-hidden-toggle input[type="checkbox"] { margin: 0; }
|
|
2035
|
+
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
|
|
2036
|
+
.crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
|
|
2037
|
+
.crumb-button:hover { background: rgba(255,255,255,0.10); }
|
|
2038
|
+
.crumb-separator { opacity: 0.6; }
|
|
2039
|
+
.pane-list { flex: 1; overflow: auto; padding: 4px; }
|
|
2040
|
+
.entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
|
|
2041
|
+
.entry:hover { background: rgba(255,255,255,0.06); }
|
|
2042
|
+
.entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
|
|
2043
|
+
.entry.dim { opacity: 0.7; }
|
|
2044
|
+
.icon { text-align: center; opacity: 0.85; }
|
|
2045
|
+
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2046
|
+
.size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
|
|
2047
|
+
.date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
|
|
2048
|
+
.entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
|
|
2049
|
+
.sortable { cursor: pointer; }
|
|
2050
|
+
.entry.selected { background: rgba(80,160,255,0.18); }
|
|
2051
|
+
.pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
|
|
2052
|
+
.pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
|
|
2053
|
+
.pane-actions-bar .action-inputs { display: flex; gap: 6px; }
|
|
2054
|
+
.pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
|
|
2055
|
+
.pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
2056
|
+
.sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
|
|
2057
|
+
.transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
|
|
2058
|
+
.transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
|
|
2059
|
+
.transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
|
|
2060
|
+
.transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2061
|
+
.transfer-path { display: flex; gap: 4px; opacity: 0.75; }
|
|
2062
|
+
.transfer-path .label { min-width: 48px; }
|
|
2063
|
+
.transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2064
|
+
.bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
|
|
2065
|
+
.bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
|
|
2066
|
+
.transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
|
|
2067
|
+
.transfer-stats .percent { font-weight: 600; }
|
|
2068
|
+
.transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2069
|
+
.btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
|
|
2070
|
+
.delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
|
|
2071
|
+
.delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
|
|
2072
|
+
.delete-text { font-size: 13px; }
|
|
2073
|
+
.delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2074
|
+
.delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
|
|
2075
|
+
.local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
|
|
2076
|
+
.local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
|
|
2077
|
+
.local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
|
|
2078
|
+
`],
|
|
2079
|
+
}),
|
|
2080
|
+
__metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_4__.Injector,
|
|
2081
|
+
_sftp_service__WEBPACK_IMPORTED_MODULE_7__.SftpConnectionService,
|
|
2082
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_5__.ProfilesService,
|
|
2083
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_5__.AppService])
|
|
2084
|
+
], SftpManagerTabComponent);
|
|
2085
|
+
|
|
2086
|
+
|
|
2087
|
+
|
|
2088
|
+
/***/ },
|
|
2089
|
+
|
|
2090
|
+
/***/ "./src/sftp-terminal-decorator.ts"
|
|
2091
|
+
/*!****************************************!*\
|
|
2092
|
+
!*** ./src/sftp-terminal-decorator.ts ***!
|
|
2093
|
+
\****************************************/
|
|
2094
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2095
|
+
|
|
2096
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2097
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2098
|
+
/* harmony export */ SftpTerminalDecorator: () => (/* binding */ SftpTerminalDecorator)
|
|
2099
|
+
/* harmony export */ });
|
|
2100
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2101
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2102
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
|
|
2103
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_terminal__WEBPACK_IMPORTED_MODULE_1__);
|
|
2104
|
+
/* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
|
|
2105
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2106
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2107
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2108
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2109
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2110
|
+
};
|
|
2111
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2112
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
|
|
2116
|
+
|
|
2117
|
+
let SftpTerminalDecorator = class SftpTerminalDecorator extends tabby_terminal__WEBPACK_IMPORTED_MODULE_1__.TerminalDecorator {
|
|
2118
|
+
constructor(sftpUi) {
|
|
2119
|
+
super();
|
|
2120
|
+
this.sftpUi = sftpUi;
|
|
2121
|
+
}
|
|
2122
|
+
attach(terminal) {
|
|
2123
|
+
super.attach(terminal);
|
|
2124
|
+
// Best-effort DOM injection: place button near the existing Reconnect button if present.
|
|
2125
|
+
const tryInsert = () => {
|
|
2126
|
+
try {
|
|
2127
|
+
const host = terminal.element?.nativeElement ?? null;
|
|
2128
|
+
if (!host) {
|
|
2129
|
+
return false;
|
|
2130
|
+
}
|
|
2131
|
+
// Find a likely toolbar area in the tab UI
|
|
2132
|
+
const toolbar = host.querySelector('.terminal-toolbar') ??
|
|
2133
|
+
host.querySelector('terminal-toolbar') ??
|
|
2134
|
+
host.querySelector('.btn-toolbar');
|
|
2135
|
+
const container = toolbar ?? host;
|
|
2136
|
+
if (container.querySelector('[data-tabby-sftp-ui-button="1"]')) {
|
|
2137
|
+
return true;
|
|
2138
|
+
}
|
|
2139
|
+
const btn = document.createElement('button');
|
|
2140
|
+
btn.type = 'button';
|
|
2141
|
+
// Match Tabby's terminal toolbar buttons styling
|
|
2142
|
+
btn.className = 'btn btn-sm btn-link me-2';
|
|
2143
|
+
btn.setAttribute('data-tabby-sftp-ui-button', '1');
|
|
2144
|
+
btn.title = 'SFTP-UI';
|
|
2145
|
+
btn.textContent = 'SFTP-UI';
|
|
2146
|
+
btn.style.pointerEvents = 'auto';
|
|
2147
|
+
btn.style.zIndex = '10';
|
|
2148
|
+
btn.style.position = 'relative';
|
|
2149
|
+
btn.addEventListener('mousedown', (ev) => {
|
|
2150
|
+
ev.stopPropagation();
|
|
2151
|
+
});
|
|
2152
|
+
btn.addEventListener('click', (ev) => {
|
|
2153
|
+
ev.preventDefault();
|
|
2154
|
+
ev.stopPropagation();
|
|
2155
|
+
this.sftpUi.openForSourceTab(terminal);
|
|
2156
|
+
});
|
|
2157
|
+
// If there's a Reconnect button, insert next to it.
|
|
2158
|
+
const allButtons = Array.from(container.querySelectorAll('button'));
|
|
2159
|
+
const reconnectButton = allButtons.find(b => {
|
|
2160
|
+
const t = `${b.textContent ?? ''} ${b.title ?? ''} ${b.getAttribute('aria-label') ?? ''}`.toLowerCase();
|
|
2161
|
+
return t.includes('reconnect') || t.includes('переподключ');
|
|
2162
|
+
});
|
|
2163
|
+
if (reconnectButton?.parentElement) {
|
|
2164
|
+
// Put it right after Reconnect to avoid overlay issues
|
|
2165
|
+
reconnectButton.parentElement.insertBefore(btn, reconnectButton.nextSibling);
|
|
2166
|
+
}
|
|
2167
|
+
else {
|
|
2168
|
+
container.appendChild(btn);
|
|
2169
|
+
}
|
|
2170
|
+
return true;
|
|
2171
|
+
}
|
|
2172
|
+
catch {
|
|
2173
|
+
return false;
|
|
2174
|
+
}
|
|
2175
|
+
};
|
|
2176
|
+
// try a few times while the view is settling
|
|
2177
|
+
let attempts = 0;
|
|
2178
|
+
const timer = setInterval(() => {
|
|
2179
|
+
attempts++;
|
|
2180
|
+
if (tryInsert() || attempts > 20) {
|
|
2181
|
+
clearInterval(timer);
|
|
2182
|
+
}
|
|
2183
|
+
}, 500);
|
|
2184
|
+
this.subscribeUntilDetached(terminal, { unsubscribe: () => clearInterval(timer) });
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
SftpTerminalDecorator = __decorate([
|
|
2188
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)(),
|
|
2189
|
+
__metadata("design:paramtypes", [_sftp_ui_service__WEBPACK_IMPORTED_MODULE_2__.SftpUiService])
|
|
2190
|
+
], SftpTerminalDecorator);
|
|
2191
|
+
|
|
2192
|
+
|
|
2193
|
+
|
|
2194
|
+
/***/ },
|
|
2195
|
+
|
|
2196
|
+
/***/ "./src/sftp-toolbar-buttons.ts"
|
|
2197
|
+
/*!*************************************!*\
|
|
2198
|
+
!*** ./src/sftp-toolbar-buttons.ts ***!
|
|
2199
|
+
\*************************************/
|
|
2200
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2201
|
+
|
|
2202
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2203
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2204
|
+
/* harmony export */ SftpToolbarButtons: () => (/* binding */ SftpToolbarButtons)
|
|
2205
|
+
/* harmony export */ });
|
|
2206
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2207
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2208
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2209
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2210
|
+
/* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
|
|
2211
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2212
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2213
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2214
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2215
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2216
|
+
};
|
|
2217
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2218
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2219
|
+
};
|
|
2220
|
+
|
|
2221
|
+
|
|
2222
|
+
|
|
2223
|
+
let SftpToolbarButtons = class SftpToolbarButtons extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.ToolbarButtonProvider {
|
|
2224
|
+
constructor(app) {
|
|
2225
|
+
super();
|
|
2226
|
+
this.app = app;
|
|
2227
|
+
}
|
|
2228
|
+
provide() {
|
|
2229
|
+
return [
|
|
2230
|
+
{
|
|
2231
|
+
title: 'SFTP',
|
|
2232
|
+
weight: 10,
|
|
2233
|
+
icon: `
|
|
2234
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
2235
|
+
<path d="M3 7h5l2 2h11a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V9a2 2 0 0 1 2-2z"/>
|
|
2236
|
+
<path d="M12 12v6"/>
|
|
2237
|
+
<path d="M9 15l3 3 3-3"/>
|
|
2238
|
+
</svg>
|
|
2239
|
+
`,
|
|
2240
|
+
click: () => {
|
|
2241
|
+
const tab = this.app.openNewTab({
|
|
2242
|
+
type: _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__.SftpManagerTabComponent,
|
|
2243
|
+
});
|
|
2244
|
+
tab.setTitle('SFTP');
|
|
2245
|
+
},
|
|
2246
|
+
},
|
|
2247
|
+
];
|
|
2248
|
+
}
|
|
2249
|
+
};
|
|
2250
|
+
SftpToolbarButtons = __decorate([
|
|
2251
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)(),
|
|
2252
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService])
|
|
2253
|
+
], SftpToolbarButtons);
|
|
2254
|
+
|
|
2255
|
+
|
|
2256
|
+
|
|
2257
|
+
/***/ },
|
|
2258
|
+
|
|
2259
|
+
/***/ "./src/sftp-ui.service.ts"
|
|
2260
|
+
/*!********************************!*\
|
|
2261
|
+
!*** ./src/sftp-ui.service.ts ***!
|
|
2262
|
+
\********************************/
|
|
2263
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2264
|
+
|
|
2265
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2266
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2267
|
+
/* harmony export */ SftpUiService: () => (/* binding */ SftpUiService)
|
|
2268
|
+
/* harmony export */ });
|
|
2269
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2270
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2271
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2272
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2273
|
+
/* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
|
|
2274
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2275
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2276
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2277
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2278
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2279
|
+
};
|
|
2280
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2281
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2282
|
+
};
|
|
2283
|
+
|
|
2284
|
+
|
|
2285
|
+
|
|
2286
|
+
let SftpUiService = class SftpUiService {
|
|
2287
|
+
constructor(app, hotkeys, log, zone, notifications) {
|
|
2288
|
+
this.app = app;
|
|
2289
|
+
this.hotkeys = hotkeys;
|
|
2290
|
+
this.log = log;
|
|
2291
|
+
this.zone = zone;
|
|
2292
|
+
this.notifications = notifications;
|
|
2293
|
+
this.logger = this.log.create('sftp-ui');
|
|
2294
|
+
this.hotkeys.hotkey$.subscribe(h => {
|
|
2295
|
+
if (h === 'open-sftp-ui') {
|
|
2296
|
+
this.open();
|
|
2297
|
+
}
|
|
2298
|
+
});
|
|
2299
|
+
this.logger.info('loaded');
|
|
2300
|
+
}
|
|
2301
|
+
open() {
|
|
2302
|
+
const active = this.app.activeTab;
|
|
2303
|
+
const focused = active instanceof tabby_core__WEBPACK_IMPORTED_MODULE_1__.SplitTabComponent ? (active.getFocusedTab?.() ?? null) : active;
|
|
2304
|
+
this.openForSourceTab(focused);
|
|
2305
|
+
}
|
|
2306
|
+
openForSourceTab(sourceTab) {
|
|
2307
|
+
const sshSession = sourceTab?.sshSession ?? null;
|
|
2308
|
+
const profile = sourceTab?.profile ?? null;
|
|
2309
|
+
this.zone.run(() => {
|
|
2310
|
+
try {
|
|
2311
|
+
if (!sshSession) {
|
|
2312
|
+
this.notifications.error('SFTP-UI', 'No SSH session on current tab');
|
|
2313
|
+
return;
|
|
2314
|
+
}
|
|
2315
|
+
const baseTitle = sourceTab?.customTitle ||
|
|
2316
|
+
sourceTab?.title ||
|
|
2317
|
+
profile?.name ||
|
|
2318
|
+
profile?.options?.host ||
|
|
2319
|
+
'SFTP';
|
|
2320
|
+
const tab = this.app.openNewTab({
|
|
2321
|
+
type: _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__.SftpManagerTabComponent,
|
|
2322
|
+
inputs: {
|
|
2323
|
+
sshSession,
|
|
2324
|
+
profile,
|
|
2325
|
+
},
|
|
2326
|
+
});
|
|
2327
|
+
tab.setTitle(`${baseTitle} + SFTP`);
|
|
2328
|
+
this.notifications.notice('SFTP-UI opened');
|
|
2329
|
+
}
|
|
2330
|
+
catch (e) {
|
|
2331
|
+
this.notifications.error('SFTP-UI failed to open', String(e));
|
|
2332
|
+
this.logger.error('openForSourceTab failed', e);
|
|
2333
|
+
}
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
};
|
|
2337
|
+
SftpUiService = __decorate([
|
|
2338
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)(),
|
|
2339
|
+
__metadata("design:paramtypes", [tabby_core__WEBPACK_IMPORTED_MODULE_1__.AppService,
|
|
2340
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.HotkeysService,
|
|
2341
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.LogService,
|
|
2342
|
+
_angular_core__WEBPACK_IMPORTED_MODULE_0__.NgZone,
|
|
2343
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_1__.NotificationsService])
|
|
2344
|
+
], SftpUiService);
|
|
2345
|
+
|
|
2346
|
+
|
|
2347
|
+
|
|
2348
|
+
/***/ },
|
|
2349
|
+
|
|
2350
|
+
/***/ "./src/sftp.service.ts"
|
|
2351
|
+
/*!*****************************!*\
|
|
2352
|
+
!*** ./src/sftp.service.ts ***!
|
|
2353
|
+
\*****************************/
|
|
2354
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2355
|
+
|
|
2356
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2357
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2358
|
+
/* harmony export */ SftpConnectionService: () => (/* binding */ SftpConnectionService)
|
|
2359
|
+
/* harmony export */ });
|
|
2360
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2361
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2362
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2363
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2364
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2365
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2366
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2367
|
+
};
|
|
2368
|
+
|
|
2369
|
+
let SftpConnectionService = class SftpConnectionService {
|
|
2370
|
+
async openFromSSHSession(sshSession) {
|
|
2371
|
+
return await sshSession.openSFTP();
|
|
2372
|
+
}
|
|
2373
|
+
};
|
|
2374
|
+
SftpConnectionService = __decorate([
|
|
2375
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)({ providedIn: 'root' })
|
|
2376
|
+
], SftpConnectionService);
|
|
2377
|
+
|
|
2378
|
+
|
|
2379
|
+
|
|
2380
|
+
/***/ },
|
|
2381
|
+
|
|
2382
|
+
/***/ "fs/promises"
|
|
2383
|
+
/*!******************************!*\
|
|
2384
|
+
!*** external "fs/promises" ***!
|
|
2385
|
+
\******************************/
|
|
2386
|
+
(module) {
|
|
2387
|
+
|
|
2388
|
+
module.exports = require("fs/promises");
|
|
2389
|
+
|
|
2390
|
+
/***/ },
|
|
2391
|
+
|
|
2392
|
+
/***/ "os"
|
|
2393
|
+
/*!*********************!*\
|
|
2394
|
+
!*** external "os" ***!
|
|
2395
|
+
\*********************/
|
|
2396
|
+
(module) {
|
|
2397
|
+
|
|
2398
|
+
module.exports = require("os");
|
|
2399
|
+
|
|
2400
|
+
/***/ },
|
|
2401
|
+
|
|
2402
|
+
/***/ "@angular/common"
|
|
2403
|
+
/*!**********************************!*\
|
|
2404
|
+
!*** external "@angular/common" ***!
|
|
2405
|
+
\**********************************/
|
|
2406
|
+
(module) {
|
|
2407
|
+
|
|
2408
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_common__;
|
|
2409
|
+
|
|
2410
|
+
/***/ },
|
|
2411
|
+
|
|
2412
|
+
/***/ "@angular/core"
|
|
2413
|
+
/*!********************************!*\
|
|
2414
|
+
!*** external "@angular/core" ***!
|
|
2415
|
+
\********************************/
|
|
2416
|
+
(module) {
|
|
2417
|
+
|
|
2418
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_core__;
|
|
2419
|
+
|
|
2420
|
+
/***/ },
|
|
2421
|
+
|
|
2422
|
+
/***/ "@angular/forms"
|
|
2423
|
+
/*!*********************************!*\
|
|
2424
|
+
!*** external "@angular/forms" ***!
|
|
2425
|
+
\*********************************/
|
|
2426
|
+
(module) {
|
|
2427
|
+
|
|
2428
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE__angular_forms__;
|
|
2429
|
+
|
|
2430
|
+
/***/ },
|
|
2431
|
+
|
|
2432
|
+
/***/ "fs"
|
|
2433
|
+
/*!*********************!*\
|
|
2434
|
+
!*** external "fs" ***!
|
|
2435
|
+
\*********************/
|
|
2436
|
+
(module) {
|
|
2437
|
+
|
|
2438
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_fs__;
|
|
2439
|
+
|
|
2440
|
+
/***/ },
|
|
2441
|
+
|
|
2442
|
+
/***/ "path"
|
|
2443
|
+
/*!***********************!*\
|
|
2444
|
+
!*** external "path" ***!
|
|
2445
|
+
\***********************/
|
|
2446
|
+
(module) {
|
|
2447
|
+
|
|
2448
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_path__;
|
|
2449
|
+
|
|
2450
|
+
/***/ },
|
|
2451
|
+
|
|
2452
|
+
/***/ "tabby-core"
|
|
2453
|
+
/*!*****************************!*\
|
|
2454
|
+
!*** external "tabby-core" ***!
|
|
2455
|
+
\*****************************/
|
|
2456
|
+
(module) {
|
|
2457
|
+
|
|
2458
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_core__;
|
|
2459
|
+
|
|
2460
|
+
/***/ },
|
|
2461
|
+
|
|
2462
|
+
/***/ "tabby-terminal"
|
|
2463
|
+
/*!*********************************!*\
|
|
2464
|
+
!*** external "tabby-terminal" ***!
|
|
2465
|
+
\*********************************/
|
|
2466
|
+
(module) {
|
|
2467
|
+
|
|
2468
|
+
module.exports = __WEBPACK_EXTERNAL_MODULE_tabby_terminal__;
|
|
2469
|
+
|
|
2470
|
+
/***/ }
|
|
2471
|
+
|
|
2472
|
+
/******/ });
|
|
2473
|
+
/************************************************************************/
|
|
2474
|
+
/******/ // The module cache
|
|
2475
|
+
/******/ var __webpack_module_cache__ = {};
|
|
2476
|
+
/******/
|
|
2477
|
+
/******/ // The require function
|
|
2478
|
+
/******/ function __webpack_require__(moduleId) {
|
|
2479
|
+
/******/ // Check if module is in cache
|
|
2480
|
+
/******/ var cachedModule = __webpack_module_cache__[moduleId];
|
|
2481
|
+
/******/ if (cachedModule !== undefined) {
|
|
2482
|
+
/******/ return cachedModule.exports;
|
|
2483
|
+
/******/ }
|
|
2484
|
+
/******/ // Create a new module (and put it into the cache)
|
|
2485
|
+
/******/ var module = __webpack_module_cache__[moduleId] = {
|
|
2486
|
+
/******/ // no module.id needed
|
|
2487
|
+
/******/ // no module.loaded needed
|
|
2488
|
+
/******/ exports: {}
|
|
2489
|
+
/******/ };
|
|
2490
|
+
/******/
|
|
2491
|
+
/******/ // Execute the module function
|
|
2492
|
+
/******/ if (!(moduleId in __webpack_modules__)) {
|
|
2493
|
+
/******/ delete __webpack_module_cache__[moduleId];
|
|
2494
|
+
/******/ var e = new Error("Cannot find module '" + moduleId + "'");
|
|
2495
|
+
/******/ e.code = 'MODULE_NOT_FOUND';
|
|
2496
|
+
/******/ throw e;
|
|
2497
|
+
/******/ }
|
|
2498
|
+
/******/ __webpack_modules__[moduleId](module, module.exports, __webpack_require__);
|
|
2499
|
+
/******/
|
|
2500
|
+
/******/ // Return the exports of the module
|
|
2501
|
+
/******/ return module.exports;
|
|
2502
|
+
/******/ }
|
|
2503
|
+
/******/
|
|
2504
|
+
/************************************************************************/
|
|
2505
|
+
/******/ /* webpack/runtime/compat get default export */
|
|
2506
|
+
/******/ (() => {
|
|
2507
|
+
/******/ // getDefaultExport function for compatibility with non-harmony modules
|
|
2508
|
+
/******/ __webpack_require__.n = (module) => {
|
|
2509
|
+
/******/ var getter = module && module.__esModule ?
|
|
2510
|
+
/******/ () => (module['default']) :
|
|
2511
|
+
/******/ () => (module);
|
|
2512
|
+
/******/ __webpack_require__.d(getter, { a: getter });
|
|
2513
|
+
/******/ return getter;
|
|
2514
|
+
/******/ };
|
|
2515
|
+
/******/ })();
|
|
2516
|
+
/******/
|
|
2517
|
+
/******/ /* webpack/runtime/define property getters */
|
|
2518
|
+
/******/ (() => {
|
|
2519
|
+
/******/ // define getter functions for harmony exports
|
|
2520
|
+
/******/ __webpack_require__.d = (exports, definition) => {
|
|
2521
|
+
/******/ for(var key in definition) {
|
|
2522
|
+
/******/ if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
|
|
2523
|
+
/******/ Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
|
|
2524
|
+
/******/ }
|
|
2525
|
+
/******/ }
|
|
2526
|
+
/******/ };
|
|
2527
|
+
/******/ })();
|
|
2528
|
+
/******/
|
|
2529
|
+
/******/ /* webpack/runtime/hasOwnProperty shorthand */
|
|
2530
|
+
/******/ (() => {
|
|
2531
|
+
/******/ __webpack_require__.o = (obj, prop) => (Object.prototype.hasOwnProperty.call(obj, prop))
|
|
2532
|
+
/******/ })();
|
|
2533
|
+
/******/
|
|
2534
|
+
/******/ /* webpack/runtime/make namespace object */
|
|
2535
|
+
/******/ (() => {
|
|
2536
|
+
/******/ // define __esModule on exports
|
|
2537
|
+
/******/ __webpack_require__.r = (exports) => {
|
|
2538
|
+
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
|
|
2539
|
+
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
2540
|
+
/******/ }
|
|
2541
|
+
/******/ Object.defineProperty(exports, '__esModule', { value: true });
|
|
2542
|
+
/******/ };
|
|
2543
|
+
/******/ })();
|
|
2544
|
+
/******/
|
|
2545
|
+
/************************************************************************/
|
|
2546
|
+
var __webpack_exports__ = {};
|
|
2547
|
+
// This entry needs to be wrapped in an IIFE because it needs to be isolated against other modules in the chunk.
|
|
2548
|
+
(() => {
|
|
2549
|
+
/*!**********************!*\
|
|
2550
|
+
!*** ./src/index.ts ***!
|
|
2551
|
+
\**********************/
|
|
2552
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2553
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2554
|
+
/* harmony export */ SftpManagerTabComponent: () => (/* reexport safe */ _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_6__.SftpManagerTabComponent),
|
|
2555
|
+
/* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__)
|
|
2556
|
+
/* harmony export */ });
|
|
2557
|
+
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/common */ "@angular/common");
|
|
2558
|
+
/* harmony import */ var _angular_common__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_common__WEBPACK_IMPORTED_MODULE_0__);
|
|
2559
|
+
/* harmony import */ var _angular_forms__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @angular/forms */ "@angular/forms");
|
|
2560
|
+
/* harmony import */ var _angular_forms__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_angular_forms__WEBPACK_IMPORTED_MODULE_1__);
|
|
2561
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2562
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_2__);
|
|
2563
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2564
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_3__);
|
|
2565
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! tabby-terminal */ "tabby-terminal");
|
|
2566
|
+
/* harmony import */ var tabby_terminal__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(tabby_terminal__WEBPACK_IMPORTED_MODULE_4__);
|
|
2567
|
+
/* harmony import */ var _sftp_toolbar_buttons__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./sftp-toolbar-buttons */ "./src/sftp-toolbar-buttons.ts");
|
|
2568
|
+
/* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
|
|
2569
|
+
/* harmony import */ var _sftp_context_menu__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp-context-menu */ "./src/sftp-context-menu.ts");
|
|
2570
|
+
/* harmony import */ var _sftp_hotkey__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp-hotkey */ "./src/sftp-hotkey.ts");
|
|
2571
|
+
/* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
|
|
2572
|
+
/* harmony import */ var _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./sftp-terminal-decorator */ "./src/sftp-terminal-decorator.ts");
|
|
2573
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2574
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2575
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2576
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2577
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2578
|
+
};
|
|
2579
|
+
var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
2580
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
2581
|
+
};
|
|
2582
|
+
|
|
2583
|
+
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
|
|
2587
|
+
|
|
2588
|
+
|
|
2589
|
+
|
|
2590
|
+
|
|
2591
|
+
|
|
2592
|
+
|
|
2593
|
+
let SftpUiModule = class SftpUiModule {
|
|
2594
|
+
constructor(_) { }
|
|
2595
|
+
};
|
|
2596
|
+
SftpUiModule = __decorate([
|
|
2597
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_2__.NgModule)({
|
|
2598
|
+
imports: [
|
|
2599
|
+
_angular_common__WEBPACK_IMPORTED_MODULE_0__.CommonModule,
|
|
2600
|
+
_angular_forms__WEBPACK_IMPORTED_MODULE_1__.FormsModule,
|
|
2601
|
+
],
|
|
2602
|
+
declarations: [
|
|
2603
|
+
_sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_6__.SftpManagerTabComponent,
|
|
2604
|
+
],
|
|
2605
|
+
providers: [
|
|
2606
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.ToolbarButtonProvider, useClass: _sftp_toolbar_buttons__WEBPACK_IMPORTED_MODULE_5__.SftpToolbarButtons, multi: true },
|
|
2607
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabContextMenuItemProvider, useClass: _sftp_context_menu__WEBPACK_IMPORTED_MODULE_7__.SftpContextMenuProvider, multi: true },
|
|
2608
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _sftp_hotkey__WEBPACK_IMPORTED_MODULE_8__.SftpUiHotkeyProvider, multi: true },
|
|
2609
|
+
{ provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_4__.TerminalDecorator, useClass: _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_10__.SftpTerminalDecorator, multi: true },
|
|
2610
|
+
_sftp_ui_service__WEBPACK_IMPORTED_MODULE_9__.SftpUiService,
|
|
2611
|
+
],
|
|
2612
|
+
}),
|
|
2613
|
+
__metadata("design:paramtypes", [_sftp_ui_service__WEBPACK_IMPORTED_MODULE_9__.SftpUiService])
|
|
2614
|
+
], SftpUiModule);
|
|
2615
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (SftpUiModule);
|
|
2616
|
+
|
|
2617
|
+
|
|
2618
|
+
})();
|
|
2619
|
+
|
|
2620
|
+
/******/ return __webpack_exports__;
|
|
2621
|
+
/******/ })()
|
|
2622
|
+
;
|
|
2623
|
+
});
|
|
2624
|
+
//# sourceMappingURL=index.js.map
|