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/README.md +149 -142
- package/dist/index.js +711 -400
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +10 -0
- package/package.json +44 -44
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
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
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
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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.
|
|
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
|
-
//
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
690
|
-
|
|
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]
|
|
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
|
-
|
|
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
|
|
720
|
-
|
|
721
|
-
|
|
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
|
-
*
|
|
2068
|
-
(
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
<
|
|
2091
|
-
<
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
<
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
<
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
>
|
|
2161
|
-
<span class="icon"
|
|
2162
|
-
<span class="name">
|
|
2163
|
-
<span class="size"
|
|
2164
|
-
<span class="date"
|
|
2165
|
-
</div>
|
|
2166
|
-
<div
|
|
2167
|
-
class="entry"
|
|
2168
|
-
*
|
|
2169
|
-
(
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
<
|
|
2192
|
-
<
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
<
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
<
|
|
2211
|
-
<
|
|
2212
|
-
</div>
|
|
2213
|
-
|
|
2214
|
-
|
|
2215
|
-
|
|
2216
|
-
|
|
2217
|
-
|
|
2218
|
-
|
|
2219
|
-
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
|
|
2223
|
-
|
|
2224
|
-
|
|
2225
|
-
|
|
2226
|
-
|
|
2227
|
-
<div class="
|
|
2228
|
-
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
<div class="
|
|
2236
|
-
|
|
2237
|
-
<
|
|
2238
|
-
<button (click)="
|
|
2239
|
-
</div>
|
|
2240
|
-
</div>
|
|
2241
|
-
</div>
|
|
2242
|
-
|
|
2243
|
-
<div class="delete-overlay" *ngIf="
|
|
2244
|
-
<div class="delete-dialog"
|
|
2245
|
-
<div class="delete-text">{{
|
|
2246
|
-
<
|
|
2247
|
-
class="
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
</div>
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
(click)="$event.stopPropagation()"
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
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,
|