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/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 _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");
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
- let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_5__.BaseTabComponent {
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(tabby_core__WEBPACK_IMPORTED_MODULE_5__.PlatformService);
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 _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(payload.fullPath);
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 _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
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 || !this.localActionName?.trim()) {
1429
+ if (this.selectedLocal.length !== 1) {
1219
1430
  return;
1220
1431
  }
1221
1432
  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));
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
- 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));
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.remoteActionName?.trim() || !this.sftpSession) {
1480
+ if (this.selectedRemote.length !== 1 || !this.sftpSession) {
1272
1481
  return;
1273
1482
  }
1274
1483
  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));
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
- const name = (this.remoteActionName || 'New folder').trim();
1299
- if (!name) {
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
- 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));
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 _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(targetLocalPath, entry.mode, entry.size);
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 localPath = path__WEBPACK_IMPORTED_MODULE_0__.join(tmpRoot, entry.name);
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
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileDownload(localPath, entry.mode, entry.size);
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
- if (eventType === 'change') {
1695
- void this.syncBackRemoteFile(localPath);
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, { remotePath: entry.fullPath, mode: entry.mode, watcher });
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 upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_6__.LocalPathFileUpload(localPath);
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,_angular_core__WEBPACK_IMPORTED_MODULE_4__.HostListener)('document:click'),
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,_angular_core__WEBPACK_IMPORTED_MODULE_4__.Component)({
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 || !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>
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
- .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)); }
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", [_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])
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"