tabby-sftp-ui 0.2.0 → 0.2.2
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/README.md +145 -136
- package/dist/index.js +801 -399
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +17 -0
- package/package.json +44 -44
package/dist/index.js
CHANGED
|
@@ -229,12 +229,14 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
229
229
|
/* harmony import */ var fs_promises__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(fs_promises__WEBPACK_IMPORTED_MODULE_2__);
|
|
230
230
|
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! fs */ "fs");
|
|
231
231
|
/* harmony import */ var fs__WEBPACK_IMPORTED_MODULE_3___default = /*#__PURE__*/__webpack_require__.n(fs__WEBPACK_IMPORTED_MODULE_3__);
|
|
232
|
-
/* harmony import */ var
|
|
233
|
-
/* harmony import */ var
|
|
234
|
-
/* harmony import */ var
|
|
235
|
-
/* harmony import */ var
|
|
236
|
-
/* harmony import */ var
|
|
237
|
-
/* harmony import */ var
|
|
232
|
+
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! crypto */ "crypto");
|
|
233
|
+
/* harmony import */ var crypto__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(crypto__WEBPACK_IMPORTED_MODULE_4__);
|
|
234
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
235
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_5__);
|
|
236
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
237
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_6__);
|
|
238
|
+
/* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
|
|
239
|
+
/* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
|
|
238
240
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
239
241
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
240
242
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -252,7 +254,8 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
252
254
|
|
|
253
255
|
|
|
254
256
|
|
|
255
|
-
|
|
257
|
+
|
|
258
|
+
let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_6__.BaseTabComponent {
|
|
256
259
|
constructor(injector, sftp, profilesService, app) {
|
|
257
260
|
// Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
|
|
258
261
|
// @ts-expect-error runtime-compatible super(injector)
|
|
@@ -306,6 +309,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
306
309
|
this.deleteConfirmText = '';
|
|
307
310
|
this.pendingLocalDelete = [];
|
|
308
311
|
this.pendingRemoteDelete = [];
|
|
312
|
+
this.inputDialogVisible = false;
|
|
313
|
+
this.inputDialogTitle = '';
|
|
314
|
+
this.inputDialogPlaceholder = '';
|
|
315
|
+
this.inputDialogValue = '';
|
|
316
|
+
this.inputDialogMode = null;
|
|
317
|
+
this.inputDialogTargetPath = null;
|
|
318
|
+
this.inputDialogRemotePath = null;
|
|
309
319
|
this.openedRemoteFiles = new Map();
|
|
310
320
|
this.localPathPresets = [];
|
|
311
321
|
this.localFavorites = [];
|
|
@@ -314,7 +324,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
314
324
|
this.localMenuX = 0;
|
|
315
325
|
this.localMenuY = 0;
|
|
316
326
|
this.localMenuItems = [];
|
|
317
|
-
this.platform = injector.get(
|
|
327
|
+
this.platform = injector.get(tabby_core__WEBPACK_IMPORTED_MODULE_6__.PlatformService);
|
|
318
328
|
// build local path presets (similar to Termius quick locations)
|
|
319
329
|
const home = os__WEBPACK_IMPORTED_MODULE_1__.homedir();
|
|
320
330
|
this.localPathPresets.push({ id: 'home', label: 'Home', path: home });
|
|
@@ -621,6 +631,32 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
621
631
|
if (!this.sftpSession) {
|
|
622
632
|
return;
|
|
623
633
|
}
|
|
634
|
+
// Drag & drop from OS file manager (Explorer/Finder) into the remote pane
|
|
635
|
+
const osPaths = this.getDroppedOsPaths(ev);
|
|
636
|
+
if (osPaths.length) {
|
|
637
|
+
try {
|
|
638
|
+
for (const p of osPaths) {
|
|
639
|
+
await this.uploadLocalPathToRemote(this.remotePath, p);
|
|
640
|
+
}
|
|
641
|
+
await this.refreshRemote();
|
|
642
|
+
}
|
|
643
|
+
catch (e) {
|
|
644
|
+
console.error('[SFTP-UI] Upload from OS drop failed', e);
|
|
645
|
+
}
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
// Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
|
|
649
|
+
try {
|
|
650
|
+
const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
|
|
651
|
+
if (dirUpload && this.sftpSession) {
|
|
652
|
+
await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
|
|
653
|
+
await this.refreshRemote();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
|
|
659
|
+
}
|
|
624
660
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
625
661
|
if (!raw) {
|
|
626
662
|
return;
|
|
@@ -637,7 +673,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
637
673
|
}
|
|
638
674
|
try {
|
|
639
675
|
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
640
|
-
const upload = new
|
|
676
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
641
677
|
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
642
678
|
await this.sftpSession.upload(targetRemotePath, upload);
|
|
643
679
|
await this.refreshRemote();
|
|
@@ -649,6 +685,32 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
649
685
|
async onDropOnLocal(ev) {
|
|
650
686
|
ev.preventDefault();
|
|
651
687
|
this.localDropActive = false;
|
|
688
|
+
// Drag & drop from OS file manager into the local pane (copy into current local folder)
|
|
689
|
+
const osPaths = this.getDroppedOsPaths(ev);
|
|
690
|
+
if (osPaths.length) {
|
|
691
|
+
try {
|
|
692
|
+
for (const p of osPaths) {
|
|
693
|
+
await this.copyLocalPathIntoLocalDir(this.localPath, p);
|
|
694
|
+
}
|
|
695
|
+
await this.refreshLocal();
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
console.error('[SFTP-UI] Local copy from OS drop failed', e);
|
|
699
|
+
}
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
// Fallback: use Tabby's native drag parser, then write files to disk
|
|
703
|
+
try {
|
|
704
|
+
const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
|
|
705
|
+
if (dirUpload) {
|
|
706
|
+
await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
|
|
707
|
+
await this.refreshLocal();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
|
|
713
|
+
}
|
|
652
714
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
653
715
|
if (!raw) {
|
|
654
716
|
return;
|
|
@@ -668,7 +730,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
668
730
|
if (!this.sftpSession) {
|
|
669
731
|
throw new Error('Not connected');
|
|
670
732
|
}
|
|
671
|
-
const dl = new
|
|
733
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
|
|
672
734
|
this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
|
|
673
735
|
await this.sftpSession.download(payload.remotePath, dl);
|
|
674
736
|
await this.refreshLocal();
|
|
@@ -677,6 +739,155 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
677
739
|
console.error('[SFTP-UI] Download failed', e);
|
|
678
740
|
}
|
|
679
741
|
}
|
|
742
|
+
async uploadLocalPathToRemote(remoteDir, localPath) {
|
|
743
|
+
if (!this.sftpSession) {
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
|
|
747
|
+
if (!st) {
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(localPath);
|
|
751
|
+
const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, baseName);
|
|
752
|
+
if (st.isDirectory()) {
|
|
753
|
+
// Ensure destination folder exists, then recursively upload children
|
|
754
|
+
try {
|
|
755
|
+
await this.sftpSession.mkdir(remoteTarget);
|
|
756
|
+
}
|
|
757
|
+
catch {
|
|
758
|
+
// ignore (might already exist)
|
|
759
|
+
}
|
|
760
|
+
const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(localPath);
|
|
761
|
+
for (const child of children) {
|
|
762
|
+
await this.uploadLocalPathToRemote(remoteTarget, path__WEBPACK_IMPORTED_MODULE_0__.join(localPath, child));
|
|
763
|
+
}
|
|
764
|
+
return;
|
|
765
|
+
}
|
|
766
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
|
|
767
|
+
this.trackTransfer(upload, 'upload', remoteTarget, localPath);
|
|
768
|
+
await this.sftpSession.upload(remoteTarget, upload);
|
|
769
|
+
}
|
|
770
|
+
async copyLocalPathIntoLocalDir(destDir, srcPath) {
|
|
771
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(srcPath).catch(() => null);
|
|
772
|
+
if (!st) {
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(srcPath);
|
|
776
|
+
const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(destDir, baseName);
|
|
777
|
+
if (st.isDirectory()) {
|
|
778
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(destPath, { recursive: true });
|
|
779
|
+
const children = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.readdir(srcPath);
|
|
780
|
+
for (const child of children) {
|
|
781
|
+
await this.copyLocalPathIntoLocalDir(destPath, path__WEBPACK_IMPORTED_MODULE_0__.join(srcPath, child));
|
|
782
|
+
}
|
|
783
|
+
return;
|
|
784
|
+
}
|
|
785
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
|
|
786
|
+
}
|
|
787
|
+
async uploadDirectoryUploadToRemote(remoteDir, dirUpload) {
|
|
788
|
+
if (!this.sftpSession) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const childrens = dirUpload?.getChildrens?.() ?? [];
|
|
792
|
+
for (const item of childrens) {
|
|
793
|
+
// DirectoryUpload
|
|
794
|
+
if (typeof item?.getChildrens === 'function') {
|
|
795
|
+
const name = item.getName?.() || 'folder';
|
|
796
|
+
const nextRemote = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
797
|
+
try {
|
|
798
|
+
await this.sftpSession.mkdir(nextRemote);
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
// ignore (might already exist)
|
|
802
|
+
}
|
|
803
|
+
await this.uploadDirectoryUploadToRemote(nextRemote, item);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
// FileUpload (including HTMLFileUpload)
|
|
807
|
+
if (typeof item?.read === 'function' && typeof item?.getName === 'function') {
|
|
808
|
+
const fileUpload = item;
|
|
809
|
+
const name = fileUpload.getName();
|
|
810
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
811
|
+
this.trackTransfer(fileUpload, 'upload', targetRemotePath, name);
|
|
812
|
+
await this.sftpSession.upload(targetRemotePath, fileUpload);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async writeDirectoryUploadToLocal(localDir, dirUpload) {
|
|
817
|
+
const childrens = dirUpload?.getChildrens?.() ?? [];
|
|
818
|
+
for (const item of childrens) {
|
|
819
|
+
if (typeof item?.getChildrens === 'function') {
|
|
820
|
+
const name = item.getName?.() || 'folder';
|
|
821
|
+
const nextLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
|
|
822
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(nextLocal, { recursive: true });
|
|
823
|
+
await this.writeDirectoryUploadToLocal(nextLocal, item);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (typeof item?.readAll === 'function' && typeof item?.getName === 'function') {
|
|
827
|
+
const name = item.getName();
|
|
828
|
+
const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
|
|
829
|
+
const buf = await item.readAll();
|
|
830
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.writeFile(targetLocal, Buffer.from(buf));
|
|
831
|
+
try {
|
|
832
|
+
item.close?.();
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// ignore
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
getDroppedOsPaths(ev) {
|
|
841
|
+
const dt = ev.dataTransfer;
|
|
842
|
+
if (!dt) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
// 1) Electron-style File.path
|
|
846
|
+
const filePaths = Array.from(dt.files ?? [])
|
|
847
|
+
.map(f => f.path)
|
|
848
|
+
.filter((p) => Boolean(p));
|
|
849
|
+
if (filePaths.length) {
|
|
850
|
+
return filePaths;
|
|
851
|
+
}
|
|
852
|
+
// 2) Sometimes paths are exposed as URIs
|
|
853
|
+
const uriList = dt.getData('text/uri-list') || '';
|
|
854
|
+
const uris = uriList
|
|
855
|
+
.split(/\r?\n/g)
|
|
856
|
+
.map(x => x.trim())
|
|
857
|
+
.filter(x => x && !x.startsWith('#'))
|
|
858
|
+
.map(x => {
|
|
859
|
+
if (x.startsWith('file://')) {
|
|
860
|
+
try {
|
|
861
|
+
return decodeURIComponent(x.replace(/^file:\/\//, ''));
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return x.replace(/^file:\/\//, '');
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return x;
|
|
868
|
+
})
|
|
869
|
+
.filter(x => x && (x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\')));
|
|
870
|
+
if (uris.length) {
|
|
871
|
+
return uris;
|
|
872
|
+
}
|
|
873
|
+
// 3) Plain text sometimes contains a local path
|
|
874
|
+
const text = dt.getData('text/plain') || '';
|
|
875
|
+
const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
|
|
876
|
+
const textPaths = textLines
|
|
877
|
+
.map(x => {
|
|
878
|
+
if (x.startsWith('file://')) {
|
|
879
|
+
try {
|
|
880
|
+
return decodeURIComponent(x.replace(/^file:\/\//, ''));
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return x.replace(/^file:\/\//, '');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return x;
|
|
887
|
+
})
|
|
888
|
+
.filter(x => x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\'));
|
|
889
|
+
return textPaths;
|
|
890
|
+
}
|
|
680
891
|
getFilteredLocalEntries() {
|
|
681
892
|
const entriesRef = this.localEntries;
|
|
682
893
|
const filter = this.localFilter;
|
|
@@ -1215,18 +1426,17 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1215
1426
|
this.localMenuVisible = false;
|
|
1216
1427
|
}
|
|
1217
1428
|
localRename() {
|
|
1218
|
-
if (this.selectedLocal.length !== 1
|
|
1429
|
+
if (this.selectedLocal.length !== 1) {
|
|
1219
1430
|
return;
|
|
1220
1431
|
}
|
|
1221
1432
|
const entry = this.selectedLocal[0];
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
.catch(e => console.error('[SFTP-UI] Local rename failed', e));
|
|
1433
|
+
this.openInputDialog({
|
|
1434
|
+
mode: 'local-rename',
|
|
1435
|
+
title: 'Rename (local)',
|
|
1436
|
+
placeholder: 'New name',
|
|
1437
|
+
value: entry.name,
|
|
1438
|
+
targetPath: entry.fullPath,
|
|
1439
|
+
});
|
|
1230
1440
|
}
|
|
1231
1441
|
localDelete() {
|
|
1232
1442
|
if (!this.selectedLocal.length) {
|
|
@@ -1239,14 +1449,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1239
1449
|
this.deleteConfirmVisible = true;
|
|
1240
1450
|
}
|
|
1241
1451
|
localNewFolder() {
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
.catch(e => console.error('[SFTP-UI] Local mkdir failed', e));
|
|
1452
|
+
this.openInputDialog({
|
|
1453
|
+
mode: 'local-new-folder',
|
|
1454
|
+
title: 'New folder (local)',
|
|
1455
|
+
placeholder: 'Folder name',
|
|
1456
|
+
value: 'New folder',
|
|
1457
|
+
targetPath: this.localPath,
|
|
1458
|
+
});
|
|
1250
1459
|
}
|
|
1251
1460
|
localEditPermissions() {
|
|
1252
1461
|
if (this.selectedLocal.length !== 1 || !this.localActionPerms?.trim()) {
|
|
@@ -1268,18 +1477,18 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1268
1477
|
}
|
|
1269
1478
|
}
|
|
1270
1479
|
remoteRename() {
|
|
1271
|
-
if (this.selectedRemote.length !== 1 || !this.
|
|
1480
|
+
if (this.selectedRemote.length !== 1 || !this.sftpSession) {
|
|
1272
1481
|
return;
|
|
1273
1482
|
}
|
|
1274
1483
|
const entry = this.selectedRemote[0];
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1484
|
+
this.openInputDialog({
|
|
1485
|
+
mode: 'remote-rename',
|
|
1486
|
+
title: 'Rename (remote)',
|
|
1487
|
+
placeholder: 'New name',
|
|
1488
|
+
value: entry.name,
|
|
1489
|
+
remotePath: entry.fullPath,
|
|
1490
|
+
targetPath: this.remotePath,
|
|
1491
|
+
});
|
|
1283
1492
|
}
|
|
1284
1493
|
remoteDelete() {
|
|
1285
1494
|
if (!this.selectedRemote.length || !this.sftpSession) {
|
|
@@ -1295,14 +1504,87 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1295
1504
|
if (!this.sftpSession) {
|
|
1296
1505
|
return;
|
|
1297
1506
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1507
|
+
this.openInputDialog({
|
|
1508
|
+
mode: 'remote-new-folder',
|
|
1509
|
+
title: 'New folder (remote)',
|
|
1510
|
+
placeholder: 'Folder name',
|
|
1511
|
+
value: 'New folder',
|
|
1512
|
+
targetPath: this.remotePath,
|
|
1513
|
+
});
|
|
1514
|
+
}
|
|
1515
|
+
openInputDialog(opts) {
|
|
1516
|
+
this.inputDialogMode = opts.mode;
|
|
1517
|
+
this.inputDialogTitle = opts.title;
|
|
1518
|
+
this.inputDialogPlaceholder = opts.placeholder;
|
|
1519
|
+
this.inputDialogValue = opts.value;
|
|
1520
|
+
this.inputDialogTargetPath = opts.targetPath;
|
|
1521
|
+
this.inputDialogRemotePath = opts.remotePath ?? null;
|
|
1522
|
+
this.inputDialogVisible = true;
|
|
1523
|
+
}
|
|
1524
|
+
cancelInputDialog() {
|
|
1525
|
+
this.inputDialogVisible = false;
|
|
1526
|
+
this.inputDialogMode = null;
|
|
1527
|
+
this.inputDialogTitle = '';
|
|
1528
|
+
this.inputDialogPlaceholder = '';
|
|
1529
|
+
this.inputDialogValue = '';
|
|
1530
|
+
this.inputDialogTargetPath = null;
|
|
1531
|
+
this.inputDialogRemotePath = null;
|
|
1532
|
+
}
|
|
1533
|
+
async confirmInputDialog() {
|
|
1534
|
+
if (!this.inputDialogVisible || !this.inputDialogMode) {
|
|
1535
|
+
return;
|
|
1536
|
+
}
|
|
1537
|
+
const mode = this.inputDialogMode;
|
|
1538
|
+
const value = this.inputDialogValue.trim();
|
|
1539
|
+
const targetPath = this.inputDialogTargetPath;
|
|
1540
|
+
const remotePath = this.inputDialogRemotePath;
|
|
1541
|
+
this.cancelInputDialog();
|
|
1542
|
+
if (!value || !targetPath) {
|
|
1300
1543
|
return;
|
|
1301
1544
|
}
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1545
|
+
try {
|
|
1546
|
+
if (mode === 'local-new-folder') {
|
|
1547
|
+
const dir = targetPath;
|
|
1548
|
+
const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.join(dir, value);
|
|
1549
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(folderPath, { recursive: true });
|
|
1550
|
+
await this.refreshLocal();
|
|
1551
|
+
return;
|
|
1552
|
+
}
|
|
1553
|
+
if (mode === 'local-rename') {
|
|
1554
|
+
const from = targetPath;
|
|
1555
|
+
const to = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, value);
|
|
1556
|
+
if (path__WEBPACK_IMPORTED_MODULE_0__.basename(from) === value) {
|
|
1557
|
+
return;
|
|
1558
|
+
}
|
|
1559
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.rename(from, to);
|
|
1560
|
+
await this.refreshLocal();
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
if (mode === 'remote-new-folder') {
|
|
1564
|
+
if (!this.sftpSession) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const dir = targetPath;
|
|
1568
|
+
const folderPath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(dir, value);
|
|
1569
|
+
await this.sftpSession.mkdir(folderPath);
|
|
1570
|
+
await this.refreshRemote();
|
|
1571
|
+
return;
|
|
1572
|
+
}
|
|
1573
|
+
if (mode === 'remote-rename') {
|
|
1574
|
+
if (!this.sftpSession || !remotePath) {
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
const to = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, value);
|
|
1578
|
+
if (path__WEBPACK_IMPORTED_MODULE_0__.posix.basename(remotePath) === value) {
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
await this.sftpSession.rename(remotePath, to);
|
|
1582
|
+
await this.refreshRemote();
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
catch (e) {
|
|
1586
|
+
console.error('[SFTP-UI] Input dialog action failed', e);
|
|
1587
|
+
}
|
|
1306
1588
|
}
|
|
1307
1589
|
remoteEditPermissions() {
|
|
1308
1590
|
if (this.selectedRemote.length !== 1 || !this.remoteActionPerms?.trim() || !this.sftpSession) {
|
|
@@ -1332,7 +1614,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1332
1614
|
continue;
|
|
1333
1615
|
}
|
|
1334
1616
|
const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, entry.name);
|
|
1335
|
-
const dl = new
|
|
1617
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
|
|
1336
1618
|
this.trackTransfer(dl, 'download', entry.fullPath, targetLocalPath);
|
|
1337
1619
|
void this.sftpSession.download(entry.fullPath, dl)
|
|
1338
1620
|
.then(() => this.refreshLocal())
|
|
@@ -1503,7 +1785,27 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1503
1785
|
}
|
|
1504
1786
|
}
|
|
1505
1787
|
onKeyDown(event) {
|
|
1788
|
+
const target = event.target;
|
|
1789
|
+
const isTypingTarget = Boolean(target) && (target?.tagName === 'INPUT' ||
|
|
1790
|
+
target?.tagName === 'TEXTAREA' ||
|
|
1791
|
+
target?.isContentEditable);
|
|
1792
|
+
if (event.key === 'Escape') {
|
|
1793
|
+
if (this.inputDialogVisible) {
|
|
1794
|
+
event.preventDefault();
|
|
1795
|
+
this.cancelInputDialog();
|
|
1796
|
+
return;
|
|
1797
|
+
}
|
|
1798
|
+
if (this.deleteConfirmVisible) {
|
|
1799
|
+
event.preventDefault();
|
|
1800
|
+
this.cancelDelete();
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1506
1804
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
1805
|
+
// Don't intercept Delete/Backspace while typing in inputs
|
|
1806
|
+
if (isTypingTarget) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1507
1809
|
event.preventDefault();
|
|
1508
1810
|
if (this.selectedRemote.length) {
|
|
1509
1811
|
this.remoteDelete();
|
|
@@ -1675,7 +1977,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1675
1977
|
try {
|
|
1676
1978
|
const tmpRoot = path__WEBPACK_IMPORTED_MODULE_0__.join(os__WEBPACK_IMPORTED_MODULE_1__.tmpdir(), 'tabby-sftp-ui');
|
|
1677
1979
|
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(tmpRoot, { recursive: true });
|
|
1678
|
-
const
|
|
1980
|
+
const hash = crypto__WEBPACK_IMPORTED_MODULE_4__.createHash('sha1').update(entry.fullPath).digest('hex').slice(0, 10);
|
|
1981
|
+
const safeName = entry.name.replace(/[<>:"/\\|?*\x00-\x1F]/g, '_');
|
|
1982
|
+
const localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, `${hash}-${safeName}`);
|
|
1679
1983
|
// если уже есть watcher на этот файл – закроем его и перезапишем
|
|
1680
1984
|
const existing = this.openedRemoteFiles.get(localPath);
|
|
1681
1985
|
if (existing?.watcher) {
|
|
@@ -1686,22 +1990,72 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1686
1990
|
// ignore
|
|
1687
1991
|
}
|
|
1688
1992
|
}
|
|
1689
|
-
|
|
1993
|
+
if (existing?.debounceTimer != null) {
|
|
1994
|
+
try {
|
|
1995
|
+
clearTimeout(existing.debounceTimer);
|
|
1996
|
+
}
|
|
1997
|
+
catch {
|
|
1998
|
+
// ignore
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(localPath, entry.mode, entry.size);
|
|
1690
2002
|
this.trackTransfer(dl, 'download', entry.fullPath, localPath);
|
|
1691
2003
|
await this.sftpSession.download(entry.fullPath, dl);
|
|
1692
2004
|
// настроим наблюдение за изменениями локального файла
|
|
2005
|
+
const schedule = () => this.scheduleSyncBackRemoteFile(localPath);
|
|
1693
2006
|
const watcher = fs__WEBPACK_IMPORTED_MODULE_3__.watch(localPath, { persistent: false }, (eventType) => {
|
|
1694
|
-
|
|
1695
|
-
|
|
2007
|
+
// Many editors save atomically (rename) or emit multiple change events.
|
|
2008
|
+
if (eventType === 'change' || eventType === 'rename') {
|
|
2009
|
+
schedule();
|
|
1696
2010
|
}
|
|
1697
2011
|
});
|
|
1698
|
-
this.openedRemoteFiles.set(localPath, {
|
|
2012
|
+
this.openedRemoteFiles.set(localPath, {
|
|
2013
|
+
remotePath: entry.fullPath,
|
|
2014
|
+
mode: entry.mode,
|
|
2015
|
+
watcher,
|
|
2016
|
+
debounceTimer: null,
|
|
2017
|
+
syncing: false,
|
|
2018
|
+
pending: false,
|
|
2019
|
+
lastUploadedSignature: null,
|
|
2020
|
+
});
|
|
1699
2021
|
this.platform.openPath(localPath);
|
|
1700
2022
|
}
|
|
1701
2023
|
catch (e) {
|
|
1702
2024
|
console.error('[SFTP-UI] Open remote file failed', e);
|
|
1703
2025
|
}
|
|
1704
2026
|
}
|
|
2027
|
+
scheduleSyncBackRemoteFile(localPath) {
|
|
2028
|
+
const info = this.openedRemoteFiles.get(localPath);
|
|
2029
|
+
if (!info) {
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
2032
|
+
if (info.debounceTimer != null) {
|
|
2033
|
+
clearTimeout(info.debounceTimer);
|
|
2034
|
+
}
|
|
2035
|
+
// Debounce a burst of editor save events
|
|
2036
|
+
info.debounceTimer = window.setTimeout(() => {
|
|
2037
|
+
info.debounceTimer = null;
|
|
2038
|
+
void this.syncBackRemoteFile(localPath);
|
|
2039
|
+
}, 650);
|
|
2040
|
+
}
|
|
2041
|
+
async waitForStableLocalFile(localPath) {
|
|
2042
|
+
// Wait until the file stops changing (editors often write in multiple passes)
|
|
2043
|
+
let last = null;
|
|
2044
|
+
for (let i = 0; i < 10; i++) {
|
|
2045
|
+
const st = await fs_promises__WEBPACK_IMPORTED_MODULE_2__.stat(localPath).catch(() => null);
|
|
2046
|
+
if (!st || !st.isFile()) {
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
const cur = { size: st.size, mtimeMs: st.mtimeMs };
|
|
2050
|
+
if (last && cur.size === last.size && cur.mtimeMs === last.mtimeMs) {
|
|
2051
|
+
// stable for one interval
|
|
2052
|
+
return cur;
|
|
2053
|
+
}
|
|
2054
|
+
last = cur;
|
|
2055
|
+
await new Promise(resolve => setTimeout(resolve, 180));
|
|
2056
|
+
}
|
|
2057
|
+
return last;
|
|
2058
|
+
}
|
|
1705
2059
|
async syncBackRemoteFile(localPath) {
|
|
1706
2060
|
if (!this.sftpSession || !this.connected) {
|
|
1707
2061
|
return;
|
|
@@ -1710,377 +2064,415 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1710
2064
|
if (!info) {
|
|
1711
2065
|
return;
|
|
1712
2066
|
}
|
|
2067
|
+
if (info.syncing) {
|
|
2068
|
+
info.pending = true;
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
info.syncing = true;
|
|
1713
2072
|
try {
|
|
1714
|
-
const
|
|
2073
|
+
const stable = await this.waitForStableLocalFile(localPath);
|
|
2074
|
+
if (!stable) {
|
|
2075
|
+
return;
|
|
2076
|
+
}
|
|
2077
|
+
const signature = `${stable.size}:${stable.mtimeMs}`;
|
|
2078
|
+
if (info.lastUploadedSignature === signature) {
|
|
2079
|
+
return;
|
|
2080
|
+
}
|
|
2081
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(localPath);
|
|
1715
2082
|
this.trackTransfer(upload, 'upload', info.remotePath, localPath);
|
|
1716
2083
|
await this.sftpSession.upload(info.remotePath, upload);
|
|
2084
|
+
info.lastUploadedSignature = signature;
|
|
1717
2085
|
await this.refreshRemote();
|
|
1718
2086
|
}
|
|
1719
2087
|
catch (e) {
|
|
1720
2088
|
console.error('[SFTP-UI] Sync-back remote file failed', e);
|
|
1721
2089
|
}
|
|
2090
|
+
finally {
|
|
2091
|
+
info.syncing = false;
|
|
2092
|
+
if (info.pending) {
|
|
2093
|
+
info.pending = false;
|
|
2094
|
+
this.scheduleSyncBackRemoteFile(localPath);
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
1722
2097
|
}
|
|
1723
2098
|
};
|
|
1724
2099
|
__decorate([
|
|
1725
|
-
(0,
|
|
2100
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.HostListener)('document:click'),
|
|
1726
2101
|
__metadata("design:type", Function),
|
|
1727
2102
|
__metadata("design:paramtypes", []),
|
|
1728
2103
|
__metadata("design:returntype", void 0)
|
|
1729
2104
|
], SftpManagerTabComponent.prototype, "onDocumentClick", null);
|
|
1730
2105
|
SftpManagerTabComponent = __decorate([
|
|
1731
|
-
(0,
|
|
2106
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
|
|
1732
2107
|
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
|
|
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
|
|
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="
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2108
|
+
template: `
|
|
2109
|
+
<div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
|
|
2110
|
+
<div class="top-profiles" *ngIf="profile || recentProfiles.length">
|
|
2111
|
+
<div class="current" *ngIf="profile">
|
|
2112
|
+
<span class="label">Device:</span>
|
|
2113
|
+
<span class="value">{{ getProfileLabel(profile) }}</span>
|
|
2114
|
+
</div>
|
|
2115
|
+
<div class="recent" *ngIf="recentProfiles.length">
|
|
2116
|
+
<span class="label">Recent:</span>
|
|
2117
|
+
<button
|
|
2118
|
+
class="profile-chip"
|
|
2119
|
+
*ngFor="let p of recentProfiles"
|
|
2120
|
+
(click)="launchProfileFromSFTP(p)"
|
|
2121
|
+
>
|
|
2122
|
+
{{ getProfileLabel(p) }}
|
|
2123
|
+
</button>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
<div class="sftp-body">
|
|
2127
|
+
<div class="pane">
|
|
2128
|
+
<div class="pane-title">
|
|
2129
|
+
<div class="pane-label">Local</div>
|
|
2130
|
+
<div class="pane-path">
|
|
2131
|
+
<input
|
|
2132
|
+
[(ngModel)]="localPathInput"
|
|
2133
|
+
(keyup.enter)="goToLocalPathInput()"
|
|
2134
|
+
/>
|
|
2135
|
+
</div>
|
|
2136
|
+
<div class="pane-actions">
|
|
2137
|
+
<select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
|
|
2138
|
+
<option value="">Go to…</option>
|
|
2139
|
+
<option *ngFor="let p of localPathPresets" [value]="p.id">
|
|
2140
|
+
{{ p.label }}
|
|
2141
|
+
</option>
|
|
2142
|
+
</select>
|
|
2143
|
+
<button
|
|
2144
|
+
class="fav-toggle"
|
|
2145
|
+
[class.active]="isCurrentFavorite()"
|
|
2146
|
+
(click)="toggleCurrentFavorite()"
|
|
2147
|
+
title="Toggle favorite for this path"
|
|
2148
|
+
>
|
|
2149
|
+
★
|
|
2150
|
+
</button>
|
|
2151
|
+
<select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
|
|
2152
|
+
<option value="">Favorites…</option>
|
|
2153
|
+
<option *ngFor="let f of localFavorites" [value]="f.id">
|
|
2154
|
+
{{ f.label }}
|
|
2155
|
+
</option>
|
|
2156
|
+
</select>
|
|
2157
|
+
<button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
|
|
2158
|
+
<button (click)="goToLocalPathInput()">Go</button>
|
|
2159
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2160
|
+
</div>
|
|
2161
|
+
</div>
|
|
2162
|
+
<div class="pane-filters">
|
|
2163
|
+
<div class="breadcrumbs">
|
|
2164
|
+
<ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
|
|
2165
|
+
<button
|
|
2166
|
+
class="crumb-button"
|
|
2167
|
+
(click)="navigateLocalBreadcrumb(i)"
|
|
2168
|
+
(contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
|
|
2169
|
+
>
|
|
2170
|
+
{{ part.label }}
|
|
2171
|
+
</button>
|
|
2172
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2173
|
+
</ng-container>
|
|
2174
|
+
</div>
|
|
2175
|
+
<input [(ngModel)]="localFilter" placeholder="Filter files..." />
|
|
2176
|
+
<label class="show-hidden-toggle">
|
|
2177
|
+
<input type="checkbox" [(ngModel)]="showHiddenLocal" />
|
|
2178
|
+
<span>Show hidden</span>
|
|
2179
|
+
</label>
|
|
2180
|
+
</div>
|
|
2181
|
+
<div class="pane-list"
|
|
2182
|
+
(dragover)="onDragOver($event)"
|
|
2183
|
+
(drop)="onDropOnLocal($event)"
|
|
2184
|
+
>
|
|
2185
|
+
<div class="entry header">
|
|
2186
|
+
<span class="icon"></span>
|
|
2187
|
+
<span class="name sortable" (click)="setLocalSort('name')">Name</span>
|
|
2188
|
+
<span class="size sortable" (click)="setLocalSort('size')">Size</span>
|
|
2189
|
+
<span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
|
|
2190
|
+
</div>
|
|
2191
|
+
<div
|
|
2192
|
+
class="entry"
|
|
2193
|
+
*ngFor="let e of getFilteredLocalEntries()"
|
|
2194
|
+
(click)="selectLocal(e, $event)"
|
|
2195
|
+
(dblclick)="openLocal(e)"
|
|
2196
|
+
(mousedown)="onLocalMouseDown(e, $event)"
|
|
2197
|
+
(contextmenu)="onLocalContextMenu(e, $event)"
|
|
2198
|
+
(dragover)="onLocalEntryDragOver(e, $event)"
|
|
2199
|
+
(drop)="onLocalEntryDrop(e, $event)"
|
|
2200
|
+
[class.drop-target]="localDropActive"
|
|
2201
|
+
[class.selected]="isLocalSelected(e)"
|
|
2202
|
+
[draggable]="true"
|
|
2203
|
+
(dragstart)="onDragStartLocal($event, e)"
|
|
2204
|
+
>
|
|
2205
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2206
|
+
<span class="name">{{ e.name }}</span>
|
|
2207
|
+
<span class="size">{{ getLocalSizeDisplay(e) }}</span>
|
|
2208
|
+
<span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
|
|
2209
|
+
</div>
|
|
2210
|
+
</div>
|
|
2211
|
+
<div class="pane-actions-bar">
|
|
2212
|
+
<div class="selection" *ngIf="selectedLocal.length">
|
|
2213
|
+
Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
|
|
2214
|
+
</div>
|
|
2215
|
+
<div class="action-inputs">
|
|
2216
|
+
<input [(ngModel)]="localActionName" placeholder="Name / new name" />
|
|
2217
|
+
<input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2218
|
+
</div>
|
|
2219
|
+
<div class="action-buttons">
|
|
2220
|
+
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
|
|
2221
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2222
|
+
<button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
|
|
2223
|
+
<button (click)="localNewFolder()">New Folder</button>
|
|
2224
|
+
<button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
|
|
2225
|
+
<button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
|
|
2226
|
+
</div>
|
|
2227
|
+
</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
|
|
2230
|
+
<div class="pane">
|
|
2231
|
+
<div class="pane-title">
|
|
2232
|
+
<div class="pane-label">
|
|
2233
|
+
Remote
|
|
2234
|
+
<span *ngIf="connected && profile?.options?.host" class="pane-sub">
|
|
2235
|
+
— {{ profile.options.host }}
|
|
2236
|
+
</span>
|
|
2237
|
+
</div>
|
|
2238
|
+
<div class="pane-path">
|
|
2239
|
+
<input
|
|
2240
|
+
[(ngModel)]="remotePathInput"
|
|
2241
|
+
(keyup.enter)="goToRemotePathInput()"
|
|
2242
|
+
[disabled]="!connected"
|
|
2243
|
+
/>
|
|
2244
|
+
</div>
|
|
2245
|
+
<div class="pane-actions">
|
|
2246
|
+
<button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
|
|
2247
|
+
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
2248
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2249
|
+
</div>
|
|
2250
|
+
</div>
|
|
2251
|
+
<div class="pane-filters">
|
|
2252
|
+
<div class="breadcrumbs" *ngIf="connected">
|
|
2253
|
+
<ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
|
|
2254
|
+
<button
|
|
2255
|
+
class="crumb-button"
|
|
2256
|
+
(click)="navigateRemoteBreadcrumb(i)"
|
|
2257
|
+
>
|
|
2258
|
+
{{ part.label }}
|
|
2259
|
+
</button>
|
|
2260
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2261
|
+
</ng-container>
|
|
2262
|
+
</div>
|
|
2263
|
+
<input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
|
|
2264
|
+
<label class="show-hidden-toggle">
|
|
2265
|
+
<input type="checkbox" [(ngModel)]="showHiddenRemote" />
|
|
2266
|
+
<span>Show hidden</span>
|
|
2267
|
+
</label>
|
|
2268
|
+
</div>
|
|
2269
|
+
<div class="pane-list"
|
|
2270
|
+
(dragover)="onDragOver($event)"
|
|
2271
|
+
(drop)="onDropOnRemote($event)"
|
|
2272
|
+
>
|
|
2273
|
+
<div class="entry dim" *ngIf="!connected">
|
|
2274
|
+
<span class="name">Not connected</span>
|
|
2275
|
+
</div>
|
|
2276
|
+
<div class="entry header" *ngIf="connected">
|
|
2277
|
+
<span class="icon"></span>
|
|
2278
|
+
<span class="name sortable" (click)="setRemoteSort('name')">Name</span>
|
|
2279
|
+
<span class="size sortable" (click)="setRemoteSort('size')">Size</span>
|
|
2280
|
+
<span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
|
|
2281
|
+
</div>
|
|
2282
|
+
<div
|
|
2283
|
+
class="entry"
|
|
2284
|
+
*ngIf="connected && remotePath !== '/'"
|
|
2285
|
+
(dblclick)="remoteUp()"
|
|
2286
|
+
>
|
|
2287
|
+
<span class="icon">⬆</span>
|
|
2288
|
+
<span class="name">Go up</span>
|
|
2289
|
+
<span class="size"></span>
|
|
2290
|
+
<span class="date"></span>
|
|
2291
|
+
</div>
|
|
2292
|
+
<div
|
|
2293
|
+
class="entry"
|
|
2294
|
+
*ngFor="let e of getFilteredRemoteEntries()"
|
|
2295
|
+
(click)="selectRemote(e, $event)"
|
|
2296
|
+
(dblclick)="openRemote(e)"
|
|
2297
|
+
(mousedown)="onRemoteMouseDown(e, $event)"
|
|
2298
|
+
(contextmenu)="onRemoteContextMenu(e, $event)"
|
|
2299
|
+
(dragover)="onRemoteEntryDragOver(e, $event)"
|
|
2300
|
+
(drop)="onRemoteEntryDrop(e, $event)"
|
|
2301
|
+
[class.drop-target]="remoteDropActive"
|
|
2302
|
+
[class.selected]="isRemoteSelected(e)"
|
|
2303
|
+
[draggable]="connected"
|
|
2304
|
+
(dragstart)="onDragStartRemote($event, e)"
|
|
2305
|
+
>
|
|
2306
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2307
|
+
<span class="name">{{ e.name }}</span>
|
|
2308
|
+
<span class="size">{{ getRemoteSizeDisplay(e) }}</span>
|
|
2309
|
+
<span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
|
|
2310
|
+
</div>
|
|
2311
|
+
</div>
|
|
2312
|
+
<div class="pane-actions-bar">
|
|
2313
|
+
<div class="selection" *ngIf="selectedRemote.length">
|
|
2314
|
+
Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
|
|
2315
|
+
</div>
|
|
2316
|
+
<div class="action-inputs">
|
|
2317
|
+
<input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
|
|
2318
|
+
<input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2319
|
+
</div>
|
|
2320
|
+
<div class="action-buttons">
|
|
2321
|
+
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
|
|
2322
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2323
|
+
<button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
|
|
2324
|
+
<button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
|
|
2325
|
+
<button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
|
|
2326
|
+
<button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
|
|
2327
|
+
<button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
|
|
2328
|
+
</div>
|
|
2329
|
+
</div>
|
|
2330
|
+
</div>
|
|
2331
|
+
</div>
|
|
2332
|
+
<div class="sftp-transfers" *ngIf="transfers.length">
|
|
2333
|
+
<div class="transfer" *ngFor="let t of transfers">
|
|
2334
|
+
<div class="transfer-main">
|
|
2335
|
+
<div class="transfer-title">
|
|
2336
|
+
<span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
|
|
2337
|
+
<span class="name">{{ t.name }}</span>
|
|
2338
|
+
</div>
|
|
2339
|
+
<div class="transfer-path">
|
|
2340
|
+
<span class="label">Remote:</span>
|
|
2341
|
+
<span class="value">{{ t.remotePath }}</span>
|
|
2342
|
+
</div>
|
|
2343
|
+
<div class="transfer-path">
|
|
2344
|
+
<span class="label">Local:</span>
|
|
2345
|
+
<span class="value">{{ t.localPath }}</span>
|
|
2346
|
+
</div>
|
|
2347
|
+
<div class="bar">
|
|
2348
|
+
<div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
|
|
2349
|
+
</div>
|
|
2350
|
+
</div>
|
|
2351
|
+
<div class="transfer-stats">
|
|
2352
|
+
<div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
|
|
2353
|
+
<div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
|
|
2354
|
+
<button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
|
|
2355
|
+
</div>
|
|
2356
|
+
</div>
|
|
2357
|
+
</div>
|
|
2358
|
+
|
|
2359
|
+
<div class="delete-overlay" *ngIf="deleteConfirmVisible">
|
|
2360
|
+
<div class="delete-dialog">
|
|
2361
|
+
<div class="delete-text">{{ deleteConfirmText }}</div>
|
|
2362
|
+
<div class="delete-buttons">
|
|
2363
|
+
<button class="danger" (click)="confirmDelete()">Delete</button>
|
|
2364
|
+
<button (click)="cancelDelete()">Cancel</button>
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
</div>
|
|
2368
|
+
|
|
2369
|
+
<div class="delete-overlay" *ngIf="inputDialogVisible">
|
|
2370
|
+
<div class="delete-dialog" (click)="$event.stopPropagation()">
|
|
2371
|
+
<div class="delete-text">{{ inputDialogTitle }}</div>
|
|
2372
|
+
<input
|
|
2373
|
+
class="dialog-input"
|
|
2374
|
+
[(ngModel)]="inputDialogValue"
|
|
2375
|
+
[placeholder]="inputDialogPlaceholder"
|
|
2376
|
+
(keyup.enter)="confirmInputDialog()"
|
|
2377
|
+
/>
|
|
2378
|
+
<div class="delete-buttons">
|
|
2379
|
+
<button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
|
|
2380
|
+
<button (click)="cancelInputDialog()">Cancel</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
</div>
|
|
2383
|
+
</div>
|
|
2384
|
+
|
|
2385
|
+
<div
|
|
2386
|
+
class="local-menu"
|
|
2387
|
+
*ngIf="localMenuVisible"
|
|
2388
|
+
[style.left.px]="localMenuX"
|
|
2389
|
+
[style.top.px]="localMenuY"
|
|
2390
|
+
(click)="$event.stopPropagation()"
|
|
2391
|
+
>
|
|
2392
|
+
<div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
|
|
2393
|
+
{{ item.label }}
|
|
2394
|
+
</div>
|
|
2395
|
+
</div>
|
|
2396
|
+
</div>
|
|
2006
2397
|
`,
|
|
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
|
-
.
|
|
2074
|
-
.delete-buttons
|
|
2075
|
-
.
|
|
2076
|
-
.local-menu
|
|
2077
|
-
.local-menu-item
|
|
2398
|
+
styles: [`
|
|
2399
|
+
.sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
|
|
2400
|
+
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; }
|
|
2401
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
2402
|
+
.top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
|
|
2403
|
+
.top-profiles .current .label,
|
|
2404
|
+
.top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
|
|
2405
|
+
.top-profiles .value { font-weight: 600; }
|
|
2406
|
+
.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; }
|
|
2407
|
+
.top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
|
|
2408
|
+
.sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
|
|
2409
|
+
.pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
|
|
2410
|
+
.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); }
|
|
2411
|
+
.pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
|
|
2412
|
+
.pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
|
|
2413
|
+
.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; }
|
|
2414
|
+
.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; }
|
|
2415
|
+
.pane-actions { display: flex; gap: 8px; align-items: center; }
|
|
2416
|
+
.pane-actions .path-preset,
|
|
2417
|
+
.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; }
|
|
2418
|
+
.pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
|
|
2419
|
+
.pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
|
|
2420
|
+
.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; }
|
|
2421
|
+
.pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
|
|
2422
|
+
.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); }
|
|
2423
|
+
.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; }
|
|
2424
|
+
.show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
|
|
2425
|
+
.show-hidden-toggle input[type="checkbox"] { margin: 0; }
|
|
2426
|
+
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
|
|
2427
|
+
.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; }
|
|
2428
|
+
.crumb-button:hover { background: rgba(255,255,255,0.10); }
|
|
2429
|
+
.crumb-separator { opacity: 0.6; }
|
|
2430
|
+
.pane-list { flex: 1; overflow: auto; padding: 4px; }
|
|
2431
|
+
.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; }
|
|
2432
|
+
.entry:hover { background: rgba(255,255,255,0.06); }
|
|
2433
|
+
.entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
|
|
2434
|
+
.entry.dim { opacity: 0.7; }
|
|
2435
|
+
.icon { text-align: center; opacity: 0.85; }
|
|
2436
|
+
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2437
|
+
.size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
|
|
2438
|
+
.date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
|
|
2439
|
+
.entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
|
|
2440
|
+
.sortable { cursor: pointer; }
|
|
2441
|
+
.entry.selected { background: rgba(80,160,255,0.18); }
|
|
2442
|
+
.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); }
|
|
2443
|
+
.pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
|
|
2444
|
+
.pane-actions-bar .action-inputs { display: flex; gap: 6px; }
|
|
2445
|
+
.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; }
|
|
2446
|
+
.pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
2447
|
+
.sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
|
|
2448
|
+
.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; }
|
|
2449
|
+
.transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
|
|
2450
|
+
.transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
|
|
2451
|
+
.transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2452
|
+
.transfer-path { display: flex; gap: 4px; opacity: 0.75; }
|
|
2453
|
+
.transfer-path .label { min-width: 48px; }
|
|
2454
|
+
.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; }
|
|
2455
|
+
.bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
|
|
2456
|
+
.bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
|
|
2457
|
+
.transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
|
|
2458
|
+
.transfer-stats .percent { font-weight: 600; }
|
|
2459
|
+
.transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2460
|
+
.btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
|
|
2461
|
+
.delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
|
|
2462
|
+
.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; }
|
|
2463
|
+
.delete-text { font-size: 13px; }
|
|
2464
|
+
.dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
|
|
2465
|
+
.delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2466
|
+
.delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
|
|
2467
|
+
.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); }
|
|
2468
|
+
.local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
|
|
2469
|
+
.local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
|
|
2078
2470
|
`],
|
|
2079
2471
|
}),
|
|
2080
|
-
__metadata("design:paramtypes", [
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2472
|
+
__metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
|
|
2473
|
+
_sftp_service__WEBPACK_IMPORTED_MODULE_8__.SftpConnectionService,
|
|
2474
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_6__.ProfilesService,
|
|
2475
|
+
tabby_core__WEBPACK_IMPORTED_MODULE_6__.AppService])
|
|
2084
2476
|
], SftpManagerTabComponent);
|
|
2085
2477
|
|
|
2086
2478
|
|
|
@@ -2314,6 +2706,16 @@ SftpConnectionService = __decorate([
|
|
|
2314
2706
|
|
|
2315
2707
|
|
|
2316
2708
|
|
|
2709
|
+
/***/ },
|
|
2710
|
+
|
|
2711
|
+
/***/ "crypto"
|
|
2712
|
+
/*!*************************!*\
|
|
2713
|
+
!*** external "crypto" ***!
|
|
2714
|
+
\*************************/
|
|
2715
|
+
(module) {
|
|
2716
|
+
|
|
2717
|
+
module.exports = require("crypto");
|
|
2718
|
+
|
|
2317
2719
|
/***/ },
|
|
2318
2720
|
|
|
2319
2721
|
/***/ "fs/promises"
|