tabby-sftp-ui 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -309,6 +309,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
309
309
  this.deleteConfirmText = '';
310
310
  this.pendingLocalDelete = [];
311
311
  this.pendingRemoteDelete = [];
312
+ this.replaceConfirmVisible = false;
313
+ this.replaceConfirmText = '';
314
+ this.replaceConfirmResolve = null;
312
315
  this.inputDialogVisible = false;
313
316
  this.inputDialogTitle = '';
314
317
  this.inputDialogPlaceholder = '';
@@ -609,17 +612,11 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
609
612
  const movePayload = sources.map(x => x.fullPath);
610
613
  ev.dataTransfer?.setData('application/x-tabby-sftp-ui-remote-move', JSON.stringify(movePayload));
611
614
  // Existing cross-device drag (remote -> local) only for files
612
- if (!item.isDirectory) {
613
- const payload = {
614
- kind: 'remote-file',
615
- remotePath: item.fullPath,
616
- name: item.name,
617
- size: item.size,
618
- mode: item.mode,
619
- };
620
- ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
621
- ev.dataTransfer?.setData('text/plain', item.fullPath);
622
- }
615
+ const payload = item.isDirectory
616
+ ? { kind: 'remote-dir', remotePath: item.fullPath, name: item.name }
617
+ : { kind: 'remote-file', remotePath: item.fullPath, name: item.name, size: item.size, mode: item.mode };
618
+ ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
619
+ ev.dataTransfer?.setData('text/plain', item.fullPath);
623
620
  ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
624
621
  }
625
622
  async onDropOnRemote(ev) {
@@ -632,24 +629,41 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
632
629
  return;
633
630
  }
634
631
  // Drag & drop from OS file manager (Explorer/Finder) into the remote pane
635
- const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
636
- if (dtFiles.length) {
637
- const paths = dtFiles
638
- .map(f => f.path)
639
- .filter((p) => Boolean(p));
640
- if (paths.length) {
641
- try {
642
- for (const p of paths) {
643
- await this.uploadLocalPathToRemote(this.remotePath, p);
632
+ const osPaths = this.getDroppedOsPaths(ev);
633
+ if (osPaths.length) {
634
+ try {
635
+ for (const p of osPaths) {
636
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
637
+ const existing = this.remoteEntries.find(e => e.name === baseName);
638
+ if (existing) {
639
+ const ok = await this.showReplaceConfirm(`Replace existing "${baseName}" on remote?`);
640
+ if (!ok) {
641
+ continue;
642
+ }
643
+ const remoteTarget = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, baseName);
644
+ await this.deleteRemotePathRecursive(remoteTarget);
644
645
  }
645
- await this.refreshRemote();
646
- }
647
- catch (e) {
648
- console.error('[SFTP-UI] Upload from OS drop failed', e);
646
+ await this.uploadLocalPathToRemote(this.remotePath, p);
649
647
  }
648
+ await this.refreshRemote();
649
+ }
650
+ catch (e) {
651
+ console.error('[SFTP-UI] Upload from OS drop failed', e);
650
652
  }
651
653
  return;
652
654
  }
655
+ // Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
656
+ try {
657
+ const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
658
+ if (dirUpload && this.sftpSession) {
659
+ await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
660
+ await this.refreshRemote();
661
+ return;
662
+ }
663
+ }
664
+ catch (e) {
665
+ console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
666
+ }
653
667
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
654
668
  if (!raw) {
655
669
  return;
@@ -666,6 +680,14 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
666
680
  }
667
681
  try {
668
682
  const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
683
+ const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
684
+ if (existsOnRemote) {
685
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
686
+ if (!ok) {
687
+ return;
688
+ }
689
+ await this.deleteRemotePathRecursive(targetRemotePath);
690
+ }
669
691
  const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
670
692
  this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
671
693
  await this.sftpSession.upload(targetRemotePath, upload);
@@ -678,25 +700,94 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
678
700
  async onDropOnLocal(ev) {
679
701
  ev.preventDefault();
680
702
  this.localDropActive = false;
681
- // Drag & drop from OS file manager into the local pane (copy into current local folder)
682
- const dtFiles = Array.from(ev.dataTransfer?.files ?? []);
683
- if (dtFiles.length) {
684
- const paths = dtFiles
685
- .map(f => f.path)
686
- .filter((p) => Boolean(p));
687
- if (paths.length) {
703
+ // 1) Tabby's internal drag (remote -> local download)
704
+ const rawInternal = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
705
+ if (rawInternal) {
706
+ let payload;
707
+ try {
708
+ payload = JSON.parse(rawInternal);
709
+ }
710
+ catch {
711
+ payload = null;
712
+ }
713
+ if (payload && payload.kind === 'remote-file') {
688
714
  try {
689
- for (const p of paths) {
690
- await this.copyLocalPathIntoLocalDir(this.localPath, p);
715
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
716
+ if (!this.sftpSession) {
717
+ throw new Error('Not connected');
691
718
  }
719
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
720
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}"?`);
721
+ if (!ok) {
722
+ return;
723
+ }
724
+ }
725
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
726
+ this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
727
+ await this.sftpSession.download(payload.remotePath, dl);
692
728
  await this.refreshLocal();
693
729
  }
694
730
  catch (e) {
695
- console.error('[SFTP-UI] Local copy from OS drop failed', e);
731
+ console.error('[SFTP-UI] Download failed', e);
696
732
  }
733
+ return;
734
+ }
735
+ if (payload && payload.kind === 'remote-dir') {
736
+ try {
737
+ if (!this.sftpSession) {
738
+ throw new Error('Not connected');
739
+ }
740
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
741
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
742
+ const ok = await this.showReplaceConfirm(`Replace existing folder "${payload.name}"?`);
743
+ if (!ok) {
744
+ return;
745
+ }
746
+ await this.deleteLocalPathRecursive(targetLocalPath);
747
+ }
748
+ await this.downloadRemoteDirectoryRecursive(payload.remotePath, targetLocalPath);
749
+ await this.refreshLocal();
750
+ }
751
+ catch (e) {
752
+ console.error('[SFTP-UI] Download directory failed', e);
753
+ }
754
+ return;
755
+ }
756
+ }
757
+ // Drag & drop from OS file manager into the local pane (copy into current local folder)
758
+ const osPaths = this.getDroppedOsPaths(ev);
759
+ if (osPaths.length) {
760
+ try {
761
+ for (const p of osPaths) {
762
+ const baseName = path__WEBPACK_IMPORTED_MODULE_0__.basename(p);
763
+ const destPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, baseName);
764
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(destPath)) {
765
+ const ok = await this.showReplaceConfirm(`Replace existing "${baseName}"?`);
766
+ if (!ok) {
767
+ continue;
768
+ }
769
+ }
770
+ await this.copyLocalPathIntoLocalDir(this.localPath, p);
771
+ }
772
+ await this.refreshLocal();
773
+ }
774
+ catch (e) {
775
+ console.error('[SFTP-UI] Local copy from OS drop failed', e);
697
776
  }
698
777
  return;
699
778
  }
779
+ // Fallback: use Tabby's native drag parser, then write files to disk
780
+ try {
781
+ const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
782
+ if (dirUpload) {
783
+ await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
784
+ await this.refreshLocal();
785
+ return;
786
+ }
787
+ }
788
+ catch (e) {
789
+ console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
790
+ }
700
791
  const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
701
792
  if (!raw) {
702
793
  return;
@@ -708,17 +799,41 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
708
799
  catch {
709
800
  return;
710
801
  }
711
- if (payload.kind !== 'remote-file') {
802
+ if (payload.kind !== 'remote-file' && payload.kind !== 'remote-dir') {
712
803
  return;
713
804
  }
714
805
  try {
715
- const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
806
+ if (payload.kind === 'remote-file') {
807
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
808
+ if (!this.sftpSession) {
809
+ throw new Error('Not connected');
810
+ }
811
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
812
+ const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}"?`);
813
+ if (!ok) {
814
+ return;
815
+ }
816
+ await this.deleteLocalPathRecursive(targetLocalPath);
817
+ }
818
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
819
+ this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
820
+ await this.sftpSession.download(payload.remotePath, dl);
821
+ await this.refreshLocal();
822
+ return;
823
+ }
824
+ // remote-dir -> local-dir (recursive download)
716
825
  if (!this.sftpSession) {
717
826
  throw new Error('Not connected');
718
827
  }
719
- const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, payload.mode, payload.size);
720
- this.trackTransfer(dl, 'download', payload.remotePath, targetLocalPath);
721
- await this.sftpSession.download(payload.remotePath, dl);
828
+ const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, payload.name);
829
+ if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
830
+ const ok = await this.showReplaceConfirm(`Replace existing folder "${payload.name}"?`);
831
+ if (!ok) {
832
+ return;
833
+ }
834
+ await this.deleteLocalPathRecursive(targetLocalPath);
835
+ }
836
+ await this.downloadRemoteDirectoryRecursive(payload.remotePath, targetLocalPath);
722
837
  await this.refreshLocal();
723
838
  }
724
839
  catch (e) {
@@ -770,6 +885,138 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
770
885
  }
771
886
  await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
772
887
  }
888
+ async uploadDirectoryUploadToRemote(remoteDir, dirUpload) {
889
+ if (!this.sftpSession) {
890
+ return;
891
+ }
892
+ const childrens = dirUpload?.getChildrens?.() ?? [];
893
+ for (const item of childrens) {
894
+ // DirectoryUpload
895
+ if (typeof item?.getChildrens === 'function') {
896
+ const name = item.getName?.() || 'folder';
897
+ const nextRemote = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
898
+ try {
899
+ await this.sftpSession.mkdir(nextRemote);
900
+ }
901
+ catch {
902
+ // ignore (might already exist)
903
+ }
904
+ await this.uploadDirectoryUploadToRemote(nextRemote, item);
905
+ continue;
906
+ }
907
+ // FileUpload (including HTMLFileUpload)
908
+ if (typeof item?.read === 'function' && typeof item?.getName === 'function') {
909
+ const fileUpload = item;
910
+ const name = fileUpload.getName();
911
+ const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
912
+ this.trackTransfer(fileUpload, 'upload', targetRemotePath, name);
913
+ await this.sftpSession.upload(targetRemotePath, fileUpload);
914
+ }
915
+ }
916
+ }
917
+ async writeDirectoryUploadToLocal(localDir, dirUpload) {
918
+ const childrens = dirUpload?.getChildrens?.() ?? [];
919
+ for (const item of childrens) {
920
+ if (typeof item?.getChildrens === 'function') {
921
+ const name = item.getName?.() || 'folder';
922
+ const nextLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
923
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(nextLocal, { recursive: true });
924
+ await this.writeDirectoryUploadToLocal(nextLocal, item);
925
+ continue;
926
+ }
927
+ if (typeof item?.readAll === 'function' && typeof item?.getName === 'function') {
928
+ const name = item.getName();
929
+ const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
930
+ const buf = await item.readAll();
931
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.writeFile(targetLocal, Buffer.from(buf));
932
+ try {
933
+ item.close?.();
934
+ }
935
+ catch {
936
+ // ignore
937
+ }
938
+ }
939
+ }
940
+ }
941
+ async downloadRemoteDirectoryRecursive(remoteDir, localDir) {
942
+ if (!this.sftpSession) {
943
+ return;
944
+ }
945
+ await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(localDir, { recursive: true });
946
+ const entries = await this.sftpSession.readdir(remoteDir).catch(() => null);
947
+ if (!entries) {
948
+ return;
949
+ }
950
+ for (const e of entries) {
951
+ const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, e.name);
952
+ if (e.isDirectory) {
953
+ await this.downloadRemoteDirectoryRecursive(e.fullPath, targetLocal);
954
+ }
955
+ else {
956
+ const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocal, e.mode, e.size);
957
+ this.trackTransfer(dl, 'download', e.fullPath, targetLocal);
958
+ await this.sftpSession.download(e.fullPath, dl);
959
+ }
960
+ }
961
+ }
962
+ getDroppedOsPaths(ev) {
963
+ const dt = ev.dataTransfer;
964
+ if (!dt) {
965
+ return [];
966
+ }
967
+ const isWin = os__WEBPACK_IMPORTED_MODULE_1__.platform() === 'win32';
968
+ const isLocalPath = (p) => {
969
+ if (isWin) {
970
+ return /^[A-Za-z]:\\/.test(p) || p.startsWith('\\\\');
971
+ }
972
+ return p.startsWith('/');
973
+ };
974
+ // 1) Electron-style File.path
975
+ const filePaths = Array.from(dt.files ?? [])
976
+ .map(f => f.path)
977
+ .filter((p) => Boolean(p));
978
+ if (filePaths.length) {
979
+ return filePaths;
980
+ }
981
+ // 2) Sometimes paths are exposed as URIs
982
+ const uriList = dt.getData('text/uri-list') || '';
983
+ const uris = uriList
984
+ .split(/\r?\n/g)
985
+ .map(x => x.trim())
986
+ .filter(x => x && !x.startsWith('#'))
987
+ .map(x => {
988
+ if (x.startsWith('file://')) {
989
+ try {
990
+ return decodeURIComponent(x.replace(/^file:\/\//, ''));
991
+ }
992
+ catch {
993
+ return x.replace(/^file:\/\//, '');
994
+ }
995
+ }
996
+ return x;
997
+ })
998
+ .filter(x => x && isLocalPath(x));
999
+ if (uris.length) {
1000
+ return uris;
1001
+ }
1002
+ // 3) Plain text sometimes contains a local path
1003
+ const text = dt.getData('text/plain') || '';
1004
+ const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
1005
+ const textPaths = textLines
1006
+ .map(x => {
1007
+ if (x.startsWith('file://')) {
1008
+ try {
1009
+ return decodeURIComponent(x.replace(/^file:\/\//, ''));
1010
+ }
1011
+ catch {
1012
+ return x.replace(/^file:\/\//, '');
1013
+ }
1014
+ }
1015
+ return x;
1016
+ })
1017
+ .filter(x => x && isLocalPath(x));
1018
+ return textPaths;
1019
+ }
773
1020
  getFilteredLocalEntries() {
774
1021
  const entriesRef = this.localEntries;
775
1022
  const filter = this.localFilter;
@@ -1667,6 +1914,10 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1667
1914
  }
1668
1915
  }
1669
1916
  onKeyDown(event) {
1917
+ const target = event.target;
1918
+ const isTypingTarget = Boolean(target) && (target?.tagName === 'INPUT' ||
1919
+ target?.tagName === 'TEXTAREA' ||
1920
+ target?.isContentEditable);
1670
1921
  if (event.key === 'Escape') {
1671
1922
  if (this.inputDialogVisible) {
1672
1923
  event.preventDefault();
@@ -1678,8 +1929,17 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1678
1929
  this.cancelDelete();
1679
1930
  return;
1680
1931
  }
1932
+ if (this.replaceConfirmVisible) {
1933
+ event.preventDefault();
1934
+ this.cancelReplace();
1935
+ return;
1936
+ }
1681
1937
  }
1682
1938
  if (event.key === 'Delete' || event.key === 'Backspace') {
1939
+ // Don't intercept Delete/Backspace while typing in inputs
1940
+ if (isTypingTarget) {
1941
+ return;
1942
+ }
1683
1943
  event.preventDefault();
1684
1944
  if (this.selectedRemote.length) {
1685
1945
  this.remoteDelete();
@@ -1755,6 +2015,37 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
1755
2015
  this.pendingLocalDelete = [];
1756
2016
  this.pendingRemoteDelete = [];
1757
2017
  }
2018
+ async showReplaceConfirm(text) {
2019
+ if (this.replaceConfirmVisible) {
2020
+ // Prevent stacking multiple confirmations; choose the latest replacement intent.
2021
+ return false;
2022
+ }
2023
+ this.replaceConfirmText = text;
2024
+ this.replaceConfirmVisible = true;
2025
+ return new Promise(resolve => {
2026
+ this.replaceConfirmResolve = resolve;
2027
+ });
2028
+ }
2029
+ async confirmReplace() {
2030
+ if (!this.replaceConfirmVisible) {
2031
+ return;
2032
+ }
2033
+ this.replaceConfirmVisible = false;
2034
+ const resolve = this.replaceConfirmResolve;
2035
+ this.replaceConfirmResolve = null;
2036
+ this.replaceConfirmText = '';
2037
+ resolve?.(true);
2038
+ }
2039
+ cancelReplace() {
2040
+ if (!this.replaceConfirmVisible) {
2041
+ return;
2042
+ }
2043
+ this.replaceConfirmVisible = false;
2044
+ const resolve = this.replaceConfirmResolve;
2045
+ this.replaceConfirmResolve = null;
2046
+ this.replaceConfirmText = '';
2047
+ resolve?.(false);
2048
+ }
1758
2049
  async deleteLocalEntry(entry) {
1759
2050
  await this.deleteLocalPathRecursive(entry.fullPath);
1760
2051
  }
@@ -1979,368 +2270,388 @@ __decorate([
1979
2270
  SftpManagerTabComponent = __decorate([
1980
2271
  (0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
1981
2272
  selector: 'tabby-sftp-manager-tab',
1982
- template: `
1983
- <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
1984
- <div class="top-profiles" *ngIf="profile || recentProfiles.length">
1985
- <div class="current" *ngIf="profile">
1986
- <span class="label">Device:</span>
1987
- <span class="value">{{ getProfileLabel(profile) }}</span>
1988
- </div>
1989
- <div class="recent" *ngIf="recentProfiles.length">
1990
- <span class="label">Recent:</span>
1991
- <button
1992
- class="profile-chip"
1993
- *ngFor="let p of recentProfiles"
1994
- (click)="launchProfileFromSFTP(p)"
1995
- >
1996
- {{ getProfileLabel(p) }}
1997
- </button>
1998
- </div>
1999
- </div>
2000
- <div class="sftp-body">
2001
- <div class="pane">
2002
- <div class="pane-title">
2003
- <div class="pane-label">Local</div>
2004
- <div class="pane-path">
2005
- <input
2006
- [(ngModel)]="localPathInput"
2007
- (keyup.enter)="goToLocalPathInput()"
2008
- />
2009
- </div>
2010
- <div class="pane-actions">
2011
- <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2012
- <option value="">Go to…</option>
2013
- <option *ngFor="let p of localPathPresets" [value]="p.id">
2014
- {{ p.label }}
2015
- </option>
2016
- </select>
2017
- <button
2018
- class="fav-toggle"
2019
- [class.active]="isCurrentFavorite()"
2020
- (click)="toggleCurrentFavorite()"
2021
- title="Toggle favorite for this path"
2022
- >
2023
-
2024
- </button>
2025
- <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2026
- <option value="">Favorites…</option>
2027
- <option *ngFor="let f of localFavorites" [value]="f.id">
2028
- {{ f.label }}
2029
- </option>
2030
- </select>
2031
- <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2032
- <button (click)="goToLocalPathInput()">Go</button>
2033
- <button (click)="refreshLocal()">Refresh</button>
2034
- </div>
2035
- </div>
2036
- <div class="pane-filters">
2037
- <div class="breadcrumbs">
2038
- <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2039
- <button
2040
- class="crumb-button"
2041
- (click)="navigateLocalBreadcrumb(i)"
2042
- (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2043
- >
2044
- {{ part.label }}
2045
- </button>
2046
- <span class="crumb-separator" *ngIf="!last">›</span>
2047
- </ng-container>
2048
- </div>
2049
- <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2050
- <label class="show-hidden-toggle">
2051
- <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2052
- <span>Show hidden</span>
2053
- </label>
2054
- </div>
2055
- <div class="pane-list"
2056
- (dragover)="onDragOver($event)"
2057
- (drop)="onDropOnLocal($event)"
2058
- >
2059
- <div class="entry header">
2060
- <span class="icon"></span>
2061
- <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2062
- <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2063
- <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2064
- </div>
2065
- <div
2066
- class="entry"
2067
- *ngFor="let e of getFilteredLocalEntries()"
2068
- (click)="selectLocal(e, $event)"
2069
- (dblclick)="openLocal(e)"
2070
- (mousedown)="onLocalMouseDown(e, $event)"
2071
- (contextmenu)="onLocalContextMenu(e, $event)"
2072
- (dragover)="onLocalEntryDragOver(e, $event)"
2073
- (drop)="onLocalEntryDrop(e, $event)"
2074
- [class.drop-target]="localDropActive"
2075
- [class.selected]="isLocalSelected(e)"
2076
- [draggable]="true"
2077
- (dragstart)="onDragStartLocal($event, e)"
2078
- >
2079
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2080
- <span class="name">{{ e.name }}</span>
2081
- <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2082
- <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2083
- </div>
2084
- </div>
2085
- <div class="pane-actions-bar">
2086
- <div class="selection" *ngIf="selectedLocal.length">
2087
- Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2088
- </div>
2089
- <div class="action-inputs">
2090
- <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2091
- <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2092
- </div>
2093
- <div class="action-buttons">
2094
- <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2095
- <button (click)="refreshLocal()">Refresh</button>
2096
- <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2097
- <button (click)="localNewFolder()">New Folder</button>
2098
- <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2099
- <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2100
- </div>
2101
- </div>
2102
- </div>
2103
-
2104
- <div class="pane">
2105
- <div class="pane-title">
2106
- <div class="pane-label">
2107
- Remote
2108
- <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2109
- {{ profile.options.host }}
2110
- </span>
2111
- </div>
2112
- <div class="pane-path">
2113
- <input
2114
- [(ngModel)]="remotePathInput"
2115
- (keyup.enter)="goToRemotePathInput()"
2116
- [disabled]="!connected"
2117
- />
2118
- </div>
2119
- <div class="pane-actions">
2120
- <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2121
- <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2122
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2123
- </div>
2124
- </div>
2125
- <div class="pane-filters">
2126
- <div class="breadcrumbs" *ngIf="connected">
2127
- <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2128
- <button
2129
- class="crumb-button"
2130
- (click)="navigateRemoteBreadcrumb(i)"
2131
- >
2132
- {{ part.label }}
2133
- </button>
2134
- <span class="crumb-separator" *ngIf="!last">›</span>
2135
- </ng-container>
2136
- </div>
2137
- <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2138
- <label class="show-hidden-toggle">
2139
- <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2140
- <span>Show hidden</span>
2141
- </label>
2142
- </div>
2143
- <div class="pane-list"
2144
- (dragover)="onDragOver($event)"
2145
- (drop)="onDropOnRemote($event)"
2146
- >
2147
- <div class="entry dim" *ngIf="!connected">
2148
- <span class="name">Not connected</span>
2149
- </div>
2150
- <div class="entry header" *ngIf="connected">
2151
- <span class="icon"></span>
2152
- <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2153
- <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2154
- <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2155
- </div>
2156
- <div
2157
- class="entry"
2158
- *ngIf="connected && remotePath !== '/'"
2159
- (dblclick)="remoteUp()"
2160
- >
2161
- <span class="icon">⬆</span>
2162
- <span class="name">Go up</span>
2163
- <span class="size"></span>
2164
- <span class="date"></span>
2165
- </div>
2166
- <div
2167
- class="entry"
2168
- *ngFor="let e of getFilteredRemoteEntries()"
2169
- (click)="selectRemote(e, $event)"
2170
- (dblclick)="openRemote(e)"
2171
- (mousedown)="onRemoteMouseDown(e, $event)"
2172
- (contextmenu)="onRemoteContextMenu(e, $event)"
2173
- (dragover)="onRemoteEntryDragOver(e, $event)"
2174
- (drop)="onRemoteEntryDrop(e, $event)"
2175
- [class.drop-target]="remoteDropActive"
2176
- [class.selected]="isRemoteSelected(e)"
2177
- [draggable]="connected"
2178
- (dragstart)="onDragStartRemote($event, e)"
2179
- >
2180
- <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2181
- <span class="name">{{ e.name }}</span>
2182
- <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2183
- <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2184
- </div>
2185
- </div>
2186
- <div class="pane-actions-bar">
2187
- <div class="selection" *ngIf="selectedRemote.length">
2188
- Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2189
- </div>
2190
- <div class="action-inputs">
2191
- <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2192
- <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2193
- </div>
2194
- <div class="action-buttons">
2195
- <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2196
- <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2197
- <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2198
- <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2199
- <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2200
- <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2201
- <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2202
- </div>
2203
- </div>
2204
- </div>
2205
- </div>
2206
- <div class="sftp-transfers" *ngIf="transfers.length">
2207
- <div class="transfer" *ngFor="let t of transfers">
2208
- <div class="transfer-main">
2209
- <div class="transfer-title">
2210
- <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2211
- <span class="name">{{ t.name }}</span>
2212
- </div>
2213
- <div class="transfer-path">
2214
- <span class="label">Remote:</span>
2215
- <span class="value">{{ t.remotePath }}</span>
2216
- </div>
2217
- <div class="transfer-path">
2218
- <span class="label">Local:</span>
2219
- <span class="value">{{ t.localPath }}</span>
2220
- </div>
2221
- <div class="bar">
2222
- <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2223
- </div>
2224
- </div>
2225
- <div class="transfer-stats">
2226
- <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2227
- <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2228
- <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2229
- </div>
2230
- </div>
2231
- </div>
2232
-
2233
- <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2234
- <div class="delete-dialog">
2235
- <div class="delete-text">{{ deleteConfirmText }}</div>
2236
- <div class="delete-buttons">
2237
- <button class="danger" (click)="confirmDelete()">Delete</button>
2238
- <button (click)="cancelDelete()">Cancel</button>
2239
- </div>
2240
- </div>
2241
- </div>
2242
-
2243
- <div class="delete-overlay" *ngIf="inputDialogVisible">
2244
- <div class="delete-dialog" (click)="$event.stopPropagation()">
2245
- <div class="delete-text">{{ inputDialogTitle }}</div>
2246
- <input
2247
- class="dialog-input"
2248
- [(ngModel)]="inputDialogValue"
2249
- [placeholder]="inputDialogPlaceholder"
2250
- (keyup.enter)="confirmInputDialog()"
2251
- />
2252
- <div class="delete-buttons">
2253
- <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2254
- <button (click)="cancelInputDialog()">Cancel</button>
2255
- </div>
2256
- </div>
2257
- </div>
2258
-
2259
- <div
2260
- class="local-menu"
2261
- *ngIf="localMenuVisible"
2262
- [style.left.px]="localMenuX"
2263
- [style.top.px]="localMenuY"
2264
- (click)="$event.stopPropagation()"
2265
- >
2266
- <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2267
- {{ item.label }}
2268
- </div>
2269
- </div>
2270
- </div>
2273
+ template: `
2274
+ <div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
2275
+ <div class="top-profiles" *ngIf="profile || recentProfiles.length">
2276
+ <div class="current" *ngIf="profile">
2277
+ <span class="label">Device:</span>
2278
+ <span class="value">{{ getProfileLabel(profile) }}</span>
2279
+ </div>
2280
+ <div class="recent" *ngIf="recentProfiles.length">
2281
+ <span class="label">Recent:</span>
2282
+ <button
2283
+ class="profile-chip"
2284
+ *ngFor="let p of recentProfiles"
2285
+ (click)="launchProfileFromSFTP(p)"
2286
+ >
2287
+ {{ getProfileLabel(p) }}
2288
+ </button>
2289
+ </div>
2290
+ </div>
2291
+ <div class="sftp-body">
2292
+ <div class="pane">
2293
+ <div class="pane-title">
2294
+ <div class="pane-label">Local</div>
2295
+ <div class="pane-path">
2296
+ <input
2297
+ [(ngModel)]="localPathInput"
2298
+ (keyup.enter)="goToLocalPathInput()"
2299
+ />
2300
+ </div>
2301
+ <div class="pane-actions">
2302
+ <select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
2303
+ <option value="">Go to…</option>
2304
+ <option *ngFor="let p of localPathPresets" [value]="p.id">
2305
+ {{ p.label }}
2306
+ </option>
2307
+ </select>
2308
+ <button
2309
+ class="fav-toggle"
2310
+ [class.active]="isCurrentFavorite()"
2311
+ (click)="toggleCurrentFavorite()"
2312
+ title="Toggle favorite for this path"
2313
+ >
2314
+
2315
+ </button>
2316
+ <select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
2317
+ <option value="">Favorites…</option>
2318
+ <option *ngFor="let f of localFavorites" [value]="f.id">
2319
+ {{ f.label }}
2320
+ </option>
2321
+ </select>
2322
+ <button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
2323
+ <button (click)="goToLocalPathInput()">Go</button>
2324
+ <button (click)="refreshLocal()">Refresh</button>
2325
+ </div>
2326
+ </div>
2327
+ <div class="pane-filters">
2328
+ <div class="breadcrumbs">
2329
+ <ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
2330
+ <button
2331
+ class="crumb-button"
2332
+ (click)="navigateLocalBreadcrumb(i)"
2333
+ (contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
2334
+ >
2335
+ {{ part.label }}
2336
+ </button>
2337
+ <span class="crumb-separator" *ngIf="!last">›</span>
2338
+ </ng-container>
2339
+ </div>
2340
+ <input [(ngModel)]="localFilter" placeholder="Filter files..." />
2341
+ <label class="show-hidden-toggle">
2342
+ <input type="checkbox" [(ngModel)]="showHiddenLocal" />
2343
+ <span>Show hidden</span>
2344
+ </label>
2345
+ </div>
2346
+ <div class="pane-list"
2347
+ (dragover)="onDragOver($event)"
2348
+ (drop)="onDropOnLocal($event)"
2349
+ >
2350
+ <div class="entry header">
2351
+ <span class="icon"></span>
2352
+ <span class="name sortable" (click)="setLocalSort('name')">Name</span>
2353
+ <span class="size sortable" (click)="setLocalSort('size')">Size</span>
2354
+ <span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
2355
+ </div>
2356
+ <div
2357
+ class="entry"
2358
+ *ngIf="canLocalUp()"
2359
+ (dblclick)="localUp()"
2360
+ >
2361
+ <span class="icon">⬆</span>
2362
+ <span class="name">Go up</span>
2363
+ <span class="size"></span>
2364
+ <span class="date"></span>
2365
+ </div>
2366
+ <div
2367
+ class="entry"
2368
+ *ngFor="let e of getFilteredLocalEntries()"
2369
+ (click)="selectLocal(e, $event)"
2370
+ (dblclick)="openLocal(e)"
2371
+ (mousedown)="onLocalMouseDown(e, $event)"
2372
+ (contextmenu)="onLocalContextMenu(e, $event)"
2373
+ (dragover)="onLocalEntryDragOver(e, $event)"
2374
+ (drop)="onLocalEntryDrop(e, $event)"
2375
+ [class.drop-target]="localDropActive"
2376
+ [class.selected]="isLocalSelected(e)"
2377
+ [draggable]="true"
2378
+ (dragstart)="onDragStartLocal($event, e)"
2379
+ >
2380
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2381
+ <span class="name">{{ e.name }}</span>
2382
+ <span class="size">{{ getLocalSizeDisplay(e) }}</span>
2383
+ <span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
2384
+ </div>
2385
+ </div>
2386
+ <div class="pane-actions-bar">
2387
+ <div class="selection" *ngIf="selectedLocal.length">
2388
+ Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
2389
+ </div>
2390
+ <div class="action-inputs">
2391
+ <input [(ngModel)]="localActionName" placeholder="Name / new name" />
2392
+ <input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
2393
+ </div>
2394
+ <div class="action-buttons">
2395
+ <button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
2396
+ <button (click)="refreshLocal()">Refresh</button>
2397
+ <button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
2398
+ <button (click)="localNewFolder()">New Folder</button>
2399
+ <button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
2400
+ <button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
2401
+ </div>
2402
+ </div>
2403
+ </div>
2404
+
2405
+ <div class="pane">
2406
+ <div class="pane-title">
2407
+ <div class="pane-label">
2408
+ Remote
2409
+ <span *ngIf="connected && profile?.options?.host" class="pane-sub">
2410
+ {{ profile.options.host }}
2411
+ </span>
2412
+ </div>
2413
+ <div class="pane-path">
2414
+ <input
2415
+ [(ngModel)]="remotePathInput"
2416
+ (keyup.enter)="goToRemotePathInput()"
2417
+ [disabled]="!connected"
2418
+ />
2419
+ </div>
2420
+ <div class="pane-actions">
2421
+ <button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
2422
+ <button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
2423
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2424
+ </div>
2425
+ </div>
2426
+ <div class="pane-filters">
2427
+ <div class="breadcrumbs" *ngIf="connected">
2428
+ <ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
2429
+ <button
2430
+ class="crumb-button"
2431
+ (click)="navigateRemoteBreadcrumb(i)"
2432
+ >
2433
+ {{ part.label }}
2434
+ </button>
2435
+ <span class="crumb-separator" *ngIf="!last">›</span>
2436
+ </ng-container>
2437
+ </div>
2438
+ <input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
2439
+ <label class="show-hidden-toggle">
2440
+ <input type="checkbox" [(ngModel)]="showHiddenRemote" />
2441
+ <span>Show hidden</span>
2442
+ </label>
2443
+ </div>
2444
+ <div class="pane-list"
2445
+ (dragover)="onDragOver($event)"
2446
+ (drop)="onDropOnRemote($event)"
2447
+ >
2448
+ <div class="entry dim" *ngIf="!connected">
2449
+ <span class="name">Not connected</span>
2450
+ </div>
2451
+ <div class="entry header" *ngIf="connected">
2452
+ <span class="icon"></span>
2453
+ <span class="name sortable" (click)="setRemoteSort('name')">Name</span>
2454
+ <span class="size sortable" (click)="setRemoteSort('size')">Size</span>
2455
+ <span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
2456
+ </div>
2457
+ <div
2458
+ class="entry"
2459
+ *ngIf="connected && remotePath !== '/'"
2460
+ (dblclick)="remoteUp()"
2461
+ >
2462
+ <span class="icon">⬆</span>
2463
+ <span class="name">Go up</span>
2464
+ <span class="size"></span>
2465
+ <span class="date"></span>
2466
+ </div>
2467
+ <div
2468
+ class="entry"
2469
+ *ngFor="let e of getFilteredRemoteEntries()"
2470
+ (click)="selectRemote(e, $event)"
2471
+ (dblclick)="openRemote(e)"
2472
+ (mousedown)="onRemoteMouseDown(e, $event)"
2473
+ (contextmenu)="onRemoteContextMenu(e, $event)"
2474
+ (dragover)="onRemoteEntryDragOver(e, $event)"
2475
+ (drop)="onRemoteEntryDrop(e, $event)"
2476
+ [class.drop-target]="remoteDropActive"
2477
+ [class.selected]="isRemoteSelected(e)"
2478
+ [draggable]="connected"
2479
+ (dragstart)="onDragStartRemote($event, e)"
2480
+ >
2481
+ <span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
2482
+ <span class="name">{{ e.name }}</span>
2483
+ <span class="size">{{ getRemoteSizeDisplay(e) }}</span>
2484
+ <span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
2485
+ </div>
2486
+ </div>
2487
+ <div class="pane-actions-bar">
2488
+ <div class="selection" *ngIf="selectedRemote.length">
2489
+ Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
2490
+ </div>
2491
+ <div class="action-inputs">
2492
+ <input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
2493
+ <input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
2494
+ </div>
2495
+ <div class="action-buttons">
2496
+ <button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
2497
+ <button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
2498
+ <button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
2499
+ <button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
2500
+ <button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
2501
+ <button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
2502
+ <button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
2503
+ </div>
2504
+ </div>
2505
+ </div>
2506
+ </div>
2507
+ <div class="sftp-transfers" *ngIf="transfers.length">
2508
+ <div class="transfer" *ngFor="let t of transfers">
2509
+ <div class="transfer-main">
2510
+ <div class="transfer-title">
2511
+ <span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
2512
+ <span class="name">{{ t.name }}</span>
2513
+ </div>
2514
+ <div class="transfer-path">
2515
+ <span class="label">Remote:</span>
2516
+ <span class="value">{{ t.remotePath }}</span>
2517
+ </div>
2518
+ <div class="transfer-path">
2519
+ <span class="label">Local:</span>
2520
+ <span class="value">{{ t.localPath }}</span>
2521
+ </div>
2522
+ <div class="bar">
2523
+ <div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
2524
+ </div>
2525
+ </div>
2526
+ <div class="transfer-stats">
2527
+ <div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
2528
+ <div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
2529
+ <button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
2530
+ </div>
2531
+ </div>
2532
+ </div>
2533
+
2534
+ <div class="delete-overlay" *ngIf="deleteConfirmVisible">
2535
+ <div class="delete-dialog">
2536
+ <div class="delete-text">{{ deleteConfirmText }}</div>
2537
+ <div class="delete-buttons">
2538
+ <button class="danger" (click)="confirmDelete()">Delete</button>
2539
+ <button (click)="cancelDelete()">Cancel</button>
2540
+ </div>
2541
+ </div>
2542
+ </div>
2543
+
2544
+ <div class="delete-overlay" *ngIf="replaceConfirmVisible">
2545
+ <div class="delete-dialog">
2546
+ <div class="delete-text">{{ replaceConfirmText }}</div>
2547
+ <div class="delete-buttons">
2548
+ <button class="danger" (click)="confirmReplace()">Replace</button>
2549
+ <button (click)="cancelReplace()">Cancel</button>
2550
+ </div>
2551
+ </div>
2552
+ </div>
2553
+
2554
+ <div class="delete-overlay" *ngIf="inputDialogVisible">
2555
+ <div class="delete-dialog" (click)="$event.stopPropagation()">
2556
+ <div class="delete-text">{{ inputDialogTitle }}</div>
2557
+ <input
2558
+ class="dialog-input"
2559
+ [(ngModel)]="inputDialogValue"
2560
+ [placeholder]="inputDialogPlaceholder"
2561
+ (keyup.enter)="confirmInputDialog()"
2562
+ />
2563
+ <div class="delete-buttons">
2564
+ <button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
2565
+ <button (click)="cancelInputDialog()">Cancel</button>
2566
+ </div>
2567
+ </div>
2568
+ </div>
2569
+
2570
+ <div
2571
+ class="local-menu"
2572
+ *ngIf="localMenuVisible"
2573
+ [style.left.px]="localMenuX"
2574
+ [style.top.px]="localMenuY"
2575
+ (click)="$event.stopPropagation()"
2576
+ >
2577
+ <div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
2578
+ {{ item.label }}
2579
+ </div>
2580
+ </div>
2581
+ </div>
2271
2582
  `,
2272
- styles: [`
2273
- .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2274
- 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; }
2275
- button:disabled { opacity: 0.5; cursor: default; }
2276
- .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2277
- .top-profiles .current .label,
2278
- .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2279
- .top-profiles .value { font-weight: 600; }
2280
- .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; }
2281
- .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2282
- .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2283
- .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2284
- .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); }
2285
- .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2286
- .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2287
- .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; }
2288
- .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; }
2289
- .pane-actions { display: flex; gap: 8px; align-items: center; }
2290
- .pane-actions .path-preset,
2291
- .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; }
2292
- .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2293
- .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2294
- .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; }
2295
- .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2296
- .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); }
2297
- .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; }
2298
- .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2299
- .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2300
- .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2301
- .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; }
2302
- .crumb-button:hover { background: rgba(255,255,255,0.10); }
2303
- .crumb-separator { opacity: 0.6; }
2304
- .pane-list { flex: 1; overflow: auto; padding: 4px; }
2305
- .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; }
2306
- .entry:hover { background: rgba(255,255,255,0.06); }
2307
- .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2308
- .entry.dim { opacity: 0.7; }
2309
- .icon { text-align: center; opacity: 0.85; }
2310
- .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2311
- .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2312
- .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2313
- .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2314
- .sortable { cursor: pointer; }
2315
- .entry.selected { background: rgba(80,160,255,0.18); }
2316
- .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); }
2317
- .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2318
- .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2319
- .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; }
2320
- .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2321
- .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2322
- .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; }
2323
- .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2324
- .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2325
- .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2326
- .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2327
- .transfer-path .label { min-width: 48px; }
2328
- .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; }
2329
- .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2330
- .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2331
- .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2332
- .transfer-stats .percent { font-weight: 600; }
2333
- .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2334
- .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2335
- .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2336
- .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; }
2337
- .delete-text { font-size: 13px; }
2338
- .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; }
2339
- .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2340
- .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2341
- .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); }
2342
- .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2343
- .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2583
+ styles: [`
2584
+ .sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
2585
+ 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; }
2586
+ button:disabled { opacity: 0.5; cursor: default; }
2587
+ .top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
2588
+ .top-profiles .current .label,
2589
+ .top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
2590
+ .top-profiles .value { font-weight: 600; }
2591
+ .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; }
2592
+ .top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
2593
+ .sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
2594
+ .pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
2595
+ .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); }
2596
+ .pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
2597
+ .pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
2598
+ .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; }
2599
+ .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; }
2600
+ .pane-actions { display: flex; gap: 8px; align-items: center; }
2601
+ .pane-actions .path-preset,
2602
+ .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; }
2603
+ .pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
2604
+ .pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
2605
+ .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; }
2606
+ .pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
2607
+ .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); }
2608
+ .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; }
2609
+ .show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
2610
+ .show-hidden-toggle input[type="checkbox"] { margin: 0; }
2611
+ .breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
2612
+ .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; }
2613
+ .crumb-button:hover { background: rgba(255,255,255,0.10); }
2614
+ .crumb-separator { opacity: 0.6; }
2615
+ .pane-list { flex: 1; overflow: auto; padding: 4px; }
2616
+ .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; }
2617
+ .entry:hover { background: rgba(255,255,255,0.06); }
2618
+ .entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
2619
+ .entry.dim { opacity: 0.7; }
2620
+ .icon { text-align: center; opacity: 0.85; }
2621
+ .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2622
+ .size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
2623
+ .date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
2624
+ .entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
2625
+ .sortable { cursor: pointer; }
2626
+ .entry.selected { background: rgba(80,160,255,0.18); }
2627
+ .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); }
2628
+ .pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
2629
+ .pane-actions-bar .action-inputs { display: flex; gap: 6px; }
2630
+ .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; }
2631
+ .pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
2632
+ .sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
2633
+ .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; }
2634
+ .transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
2635
+ .transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
2636
+ .transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
2637
+ .transfer-path { display: flex; gap: 4px; opacity: 0.75; }
2638
+ .transfer-path .label { min-width: 48px; }
2639
+ .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; }
2640
+ .bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
2641
+ .bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
2642
+ .transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
2643
+ .transfer-stats .percent { font-weight: 600; }
2644
+ .transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
2645
+ .btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
2646
+ .delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
2647
+ .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; }
2648
+ .delete-text { font-size: 13px; }
2649
+ .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; }
2650
+ .delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
2651
+ .delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
2652
+ .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); }
2653
+ .local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
2654
+ .local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
2344
2655
  `],
2345
2656
  }),
2346
2657
  __metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,