tabby-sftp-ui 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +145 -142
- package/dist/index.js +515 -389
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +3 -0
- package/package.json +44 -44
package/dist/index.js
CHANGED
|
@@ -632,24 +632,31 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
632
632
|
return;
|
|
633
633
|
}
|
|
634
634
|
// Drag & drop from OS file manager (Explorer/Finder) into the remote pane
|
|
635
|
-
const
|
|
636
|
-
if (
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
if (paths.length) {
|
|
641
|
-
try {
|
|
642
|
-
for (const p of paths) {
|
|
643
|
-
await this.uploadLocalPathToRemote(this.remotePath, p);
|
|
644
|
-
}
|
|
645
|
-
await this.refreshRemote();
|
|
646
|
-
}
|
|
647
|
-
catch (e) {
|
|
648
|
-
console.error('[SFTP-UI] Upload from OS drop failed', e);
|
|
635
|
+
const osPaths = this.getDroppedOsPaths(ev);
|
|
636
|
+
if (osPaths.length) {
|
|
637
|
+
try {
|
|
638
|
+
for (const p of osPaths) {
|
|
639
|
+
await this.uploadLocalPathToRemote(this.remotePath, p);
|
|
649
640
|
}
|
|
641
|
+
await this.refreshRemote();
|
|
642
|
+
}
|
|
643
|
+
catch (e) {
|
|
644
|
+
console.error('[SFTP-UI] Upload from OS drop failed', e);
|
|
650
645
|
}
|
|
651
646
|
return;
|
|
652
647
|
}
|
|
648
|
+
// Fallback: use Tabby's native drag parser (supports directories and HTMLFileUpload)
|
|
649
|
+
try {
|
|
650
|
+
const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
|
|
651
|
+
if (dirUpload && this.sftpSession) {
|
|
652
|
+
await this.uploadDirectoryUploadToRemote(this.remotePath, dirUpload);
|
|
653
|
+
await this.refreshRemote();
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
catch (e) {
|
|
658
|
+
console.error('[SFTP-UI] startUploadFromDragEvent failed', e);
|
|
659
|
+
}
|
|
653
660
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
654
661
|
if (!raw) {
|
|
655
662
|
return;
|
|
@@ -679,24 +686,31 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
679
686
|
ev.preventDefault();
|
|
680
687
|
this.localDropActive = false;
|
|
681
688
|
// Drag & drop from OS file manager into the local pane (copy into current local folder)
|
|
682
|
-
const
|
|
683
|
-
if (
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
if (paths.length) {
|
|
688
|
-
try {
|
|
689
|
-
for (const p of paths) {
|
|
690
|
-
await this.copyLocalPathIntoLocalDir(this.localPath, p);
|
|
691
|
-
}
|
|
692
|
-
await this.refreshLocal();
|
|
693
|
-
}
|
|
694
|
-
catch (e) {
|
|
695
|
-
console.error('[SFTP-UI] Local copy from OS drop failed', e);
|
|
689
|
+
const osPaths = this.getDroppedOsPaths(ev);
|
|
690
|
+
if (osPaths.length) {
|
|
691
|
+
try {
|
|
692
|
+
for (const p of osPaths) {
|
|
693
|
+
await this.copyLocalPathIntoLocalDir(this.localPath, p);
|
|
696
694
|
}
|
|
695
|
+
await this.refreshLocal();
|
|
696
|
+
}
|
|
697
|
+
catch (e) {
|
|
698
|
+
console.error('[SFTP-UI] Local copy from OS drop failed', e);
|
|
697
699
|
}
|
|
698
700
|
return;
|
|
699
701
|
}
|
|
702
|
+
// Fallback: use Tabby's native drag parser, then write files to disk
|
|
703
|
+
try {
|
|
704
|
+
const dirUpload = await this.platform.startUploadFromDragEvent?.(ev, true);
|
|
705
|
+
if (dirUpload) {
|
|
706
|
+
await this.writeDirectoryUploadToLocal(this.localPath, dirUpload);
|
|
707
|
+
await this.refreshLocal();
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
catch (e) {
|
|
712
|
+
console.error('[SFTP-UI] startUploadFromDragEvent (local) failed', e);
|
|
713
|
+
}
|
|
700
714
|
const raw = ev.dataTransfer?.getData('application/x-tabby-sftp-ui');
|
|
701
715
|
if (!raw) {
|
|
702
716
|
return;
|
|
@@ -770,6 +784,110 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
770
784
|
}
|
|
771
785
|
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.copyFile(srcPath, destPath);
|
|
772
786
|
}
|
|
787
|
+
async uploadDirectoryUploadToRemote(remoteDir, dirUpload) {
|
|
788
|
+
if (!this.sftpSession) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
const childrens = dirUpload?.getChildrens?.() ?? [];
|
|
792
|
+
for (const item of childrens) {
|
|
793
|
+
// DirectoryUpload
|
|
794
|
+
if (typeof item?.getChildrens === 'function') {
|
|
795
|
+
const name = item.getName?.() || 'folder';
|
|
796
|
+
const nextRemote = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
797
|
+
try {
|
|
798
|
+
await this.sftpSession.mkdir(nextRemote);
|
|
799
|
+
}
|
|
800
|
+
catch {
|
|
801
|
+
// ignore (might already exist)
|
|
802
|
+
}
|
|
803
|
+
await this.uploadDirectoryUploadToRemote(nextRemote, item);
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
// FileUpload (including HTMLFileUpload)
|
|
807
|
+
if (typeof item?.read === 'function' && typeof item?.getName === 'function') {
|
|
808
|
+
const fileUpload = item;
|
|
809
|
+
const name = fileUpload.getName();
|
|
810
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(remoteDir, name);
|
|
811
|
+
this.trackTransfer(fileUpload, 'upload', targetRemotePath, name);
|
|
812
|
+
await this.sftpSession.upload(targetRemotePath, fileUpload);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
async writeDirectoryUploadToLocal(localDir, dirUpload) {
|
|
817
|
+
const childrens = dirUpload?.getChildrens?.() ?? [];
|
|
818
|
+
for (const item of childrens) {
|
|
819
|
+
if (typeof item?.getChildrens === 'function') {
|
|
820
|
+
const name = item.getName?.() || 'folder';
|
|
821
|
+
const nextLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
|
|
822
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.mkdir(nextLocal, { recursive: true });
|
|
823
|
+
await this.writeDirectoryUploadToLocal(nextLocal, item);
|
|
824
|
+
continue;
|
|
825
|
+
}
|
|
826
|
+
if (typeof item?.readAll === 'function' && typeof item?.getName === 'function') {
|
|
827
|
+
const name = item.getName();
|
|
828
|
+
const targetLocal = path__WEBPACK_IMPORTED_MODULE_0__.join(localDir, name);
|
|
829
|
+
const buf = await item.readAll();
|
|
830
|
+
await fs_promises__WEBPACK_IMPORTED_MODULE_2__.writeFile(targetLocal, Buffer.from(buf));
|
|
831
|
+
try {
|
|
832
|
+
item.close?.();
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
// ignore
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
getDroppedOsPaths(ev) {
|
|
841
|
+
const dt = ev.dataTransfer;
|
|
842
|
+
if (!dt) {
|
|
843
|
+
return [];
|
|
844
|
+
}
|
|
845
|
+
// 1) Electron-style File.path
|
|
846
|
+
const filePaths = Array.from(dt.files ?? [])
|
|
847
|
+
.map(f => f.path)
|
|
848
|
+
.filter((p) => Boolean(p));
|
|
849
|
+
if (filePaths.length) {
|
|
850
|
+
return filePaths;
|
|
851
|
+
}
|
|
852
|
+
// 2) Sometimes paths are exposed as URIs
|
|
853
|
+
const uriList = dt.getData('text/uri-list') || '';
|
|
854
|
+
const uris = uriList
|
|
855
|
+
.split(/\r?\n/g)
|
|
856
|
+
.map(x => x.trim())
|
|
857
|
+
.filter(x => x && !x.startsWith('#'))
|
|
858
|
+
.map(x => {
|
|
859
|
+
if (x.startsWith('file://')) {
|
|
860
|
+
try {
|
|
861
|
+
return decodeURIComponent(x.replace(/^file:\/\//, ''));
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
return x.replace(/^file:\/\//, '');
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
return x;
|
|
868
|
+
})
|
|
869
|
+
.filter(x => x && (x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\')));
|
|
870
|
+
if (uris.length) {
|
|
871
|
+
return uris;
|
|
872
|
+
}
|
|
873
|
+
// 3) Plain text sometimes contains a local path
|
|
874
|
+
const text = dt.getData('text/plain') || '';
|
|
875
|
+
const textLines = text.split(/\r?\n/g).map(x => x.trim()).filter(Boolean);
|
|
876
|
+
const textPaths = textLines
|
|
877
|
+
.map(x => {
|
|
878
|
+
if (x.startsWith('file://')) {
|
|
879
|
+
try {
|
|
880
|
+
return decodeURIComponent(x.replace(/^file:\/\//, ''));
|
|
881
|
+
}
|
|
882
|
+
catch {
|
|
883
|
+
return x.replace(/^file:\/\//, '');
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
return x;
|
|
887
|
+
})
|
|
888
|
+
.filter(x => x.includes(':\\') || x.startsWith('/') || x.startsWith('\\\\'));
|
|
889
|
+
return textPaths;
|
|
890
|
+
}
|
|
773
891
|
getFilteredLocalEntries() {
|
|
774
892
|
const entriesRef = this.localEntries;
|
|
775
893
|
const filter = this.localFilter;
|
|
@@ -1667,6 +1785,10 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1667
1785
|
}
|
|
1668
1786
|
}
|
|
1669
1787
|
onKeyDown(event) {
|
|
1788
|
+
const target = event.target;
|
|
1789
|
+
const isTypingTarget = Boolean(target) && (target?.tagName === 'INPUT' ||
|
|
1790
|
+
target?.tagName === 'TEXTAREA' ||
|
|
1791
|
+
target?.isContentEditable);
|
|
1670
1792
|
if (event.key === 'Escape') {
|
|
1671
1793
|
if (this.inputDialogVisible) {
|
|
1672
1794
|
event.preventDefault();
|
|
@@ -1680,6 +1802,10 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1680
1802
|
}
|
|
1681
1803
|
}
|
|
1682
1804
|
if (event.key === 'Delete' || event.key === 'Backspace') {
|
|
1805
|
+
// Don't intercept Delete/Backspace while typing in inputs
|
|
1806
|
+
if (isTypingTarget) {
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1683
1809
|
event.preventDefault();
|
|
1684
1810
|
if (this.selectedRemote.length) {
|
|
1685
1811
|
this.remoteDelete();
|
|
@@ -1979,368 +2105,368 @@ __decorate([
|
|
|
1979
2105
|
SftpManagerTabComponent = __decorate([
|
|
1980
2106
|
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
|
|
1981
2107
|
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>
|
|
2108
|
+
template: `
|
|
2109
|
+
<div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
|
|
2110
|
+
<div class="top-profiles" *ngIf="profile || recentProfiles.length">
|
|
2111
|
+
<div class="current" *ngIf="profile">
|
|
2112
|
+
<span class="label">Device:</span>
|
|
2113
|
+
<span class="value">{{ getProfileLabel(profile) }}</span>
|
|
2114
|
+
</div>
|
|
2115
|
+
<div class="recent" *ngIf="recentProfiles.length">
|
|
2116
|
+
<span class="label">Recent:</span>
|
|
2117
|
+
<button
|
|
2118
|
+
class="profile-chip"
|
|
2119
|
+
*ngFor="let p of recentProfiles"
|
|
2120
|
+
(click)="launchProfileFromSFTP(p)"
|
|
2121
|
+
>
|
|
2122
|
+
{{ getProfileLabel(p) }}
|
|
2123
|
+
</button>
|
|
2124
|
+
</div>
|
|
2125
|
+
</div>
|
|
2126
|
+
<div class="sftp-body">
|
|
2127
|
+
<div class="pane">
|
|
2128
|
+
<div class="pane-title">
|
|
2129
|
+
<div class="pane-label">Local</div>
|
|
2130
|
+
<div class="pane-path">
|
|
2131
|
+
<input
|
|
2132
|
+
[(ngModel)]="localPathInput"
|
|
2133
|
+
(keyup.enter)="goToLocalPathInput()"
|
|
2134
|
+
/>
|
|
2135
|
+
</div>
|
|
2136
|
+
<div class="pane-actions">
|
|
2137
|
+
<select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
|
|
2138
|
+
<option value="">Go to…</option>
|
|
2139
|
+
<option *ngFor="let p of localPathPresets" [value]="p.id">
|
|
2140
|
+
{{ p.label }}
|
|
2141
|
+
</option>
|
|
2142
|
+
</select>
|
|
2143
|
+
<button
|
|
2144
|
+
class="fav-toggle"
|
|
2145
|
+
[class.active]="isCurrentFavorite()"
|
|
2146
|
+
(click)="toggleCurrentFavorite()"
|
|
2147
|
+
title="Toggle favorite for this path"
|
|
2148
|
+
>
|
|
2149
|
+
★
|
|
2150
|
+
</button>
|
|
2151
|
+
<select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
|
|
2152
|
+
<option value="">Favorites…</option>
|
|
2153
|
+
<option *ngFor="let f of localFavorites" [value]="f.id">
|
|
2154
|
+
{{ f.label }}
|
|
2155
|
+
</option>
|
|
2156
|
+
</select>
|
|
2157
|
+
<button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
|
|
2158
|
+
<button (click)="goToLocalPathInput()">Go</button>
|
|
2159
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2160
|
+
</div>
|
|
2161
|
+
</div>
|
|
2162
|
+
<div class="pane-filters">
|
|
2163
|
+
<div class="breadcrumbs">
|
|
2164
|
+
<ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
|
|
2165
|
+
<button
|
|
2166
|
+
class="crumb-button"
|
|
2167
|
+
(click)="navigateLocalBreadcrumb(i)"
|
|
2168
|
+
(contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
|
|
2169
|
+
>
|
|
2170
|
+
{{ part.label }}
|
|
2171
|
+
</button>
|
|
2172
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2173
|
+
</ng-container>
|
|
2174
|
+
</div>
|
|
2175
|
+
<input [(ngModel)]="localFilter" placeholder="Filter files..." />
|
|
2176
|
+
<label class="show-hidden-toggle">
|
|
2177
|
+
<input type="checkbox" [(ngModel)]="showHiddenLocal" />
|
|
2178
|
+
<span>Show hidden</span>
|
|
2179
|
+
</label>
|
|
2180
|
+
</div>
|
|
2181
|
+
<div class="pane-list"
|
|
2182
|
+
(dragover)="onDragOver($event)"
|
|
2183
|
+
(drop)="onDropOnLocal($event)"
|
|
2184
|
+
>
|
|
2185
|
+
<div class="entry header">
|
|
2186
|
+
<span class="icon"></span>
|
|
2187
|
+
<span class="name sortable" (click)="setLocalSort('name')">Name</span>
|
|
2188
|
+
<span class="size sortable" (click)="setLocalSort('size')">Size</span>
|
|
2189
|
+
<span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
|
|
2190
|
+
</div>
|
|
2191
|
+
<div
|
|
2192
|
+
class="entry"
|
|
2193
|
+
*ngFor="let e of getFilteredLocalEntries()"
|
|
2194
|
+
(click)="selectLocal(e, $event)"
|
|
2195
|
+
(dblclick)="openLocal(e)"
|
|
2196
|
+
(mousedown)="onLocalMouseDown(e, $event)"
|
|
2197
|
+
(contextmenu)="onLocalContextMenu(e, $event)"
|
|
2198
|
+
(dragover)="onLocalEntryDragOver(e, $event)"
|
|
2199
|
+
(drop)="onLocalEntryDrop(e, $event)"
|
|
2200
|
+
[class.drop-target]="localDropActive"
|
|
2201
|
+
[class.selected]="isLocalSelected(e)"
|
|
2202
|
+
[draggable]="true"
|
|
2203
|
+
(dragstart)="onDragStartLocal($event, e)"
|
|
2204
|
+
>
|
|
2205
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2206
|
+
<span class="name">{{ e.name }}</span>
|
|
2207
|
+
<span class="size">{{ getLocalSizeDisplay(e) }}</span>
|
|
2208
|
+
<span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
|
|
2209
|
+
</div>
|
|
2210
|
+
</div>
|
|
2211
|
+
<div class="pane-actions-bar">
|
|
2212
|
+
<div class="selection" *ngIf="selectedLocal.length">
|
|
2213
|
+
Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
|
|
2214
|
+
</div>
|
|
2215
|
+
<div class="action-inputs">
|
|
2216
|
+
<input [(ngModel)]="localActionName" placeholder="Name / new name" />
|
|
2217
|
+
<input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2218
|
+
</div>
|
|
2219
|
+
<div class="action-buttons">
|
|
2220
|
+
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
|
|
2221
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2222
|
+
<button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
|
|
2223
|
+
<button (click)="localNewFolder()">New Folder</button>
|
|
2224
|
+
<button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
|
|
2225
|
+
<button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
|
|
2226
|
+
</div>
|
|
2227
|
+
</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
|
|
2230
|
+
<div class="pane">
|
|
2231
|
+
<div class="pane-title">
|
|
2232
|
+
<div class="pane-label">
|
|
2233
|
+
Remote
|
|
2234
|
+
<span *ngIf="connected && profile?.options?.host" class="pane-sub">
|
|
2235
|
+
— {{ profile.options.host }}
|
|
2236
|
+
</span>
|
|
2237
|
+
</div>
|
|
2238
|
+
<div class="pane-path">
|
|
2239
|
+
<input
|
|
2240
|
+
[(ngModel)]="remotePathInput"
|
|
2241
|
+
(keyup.enter)="goToRemotePathInput()"
|
|
2242
|
+
[disabled]="!connected"
|
|
2243
|
+
/>
|
|
2244
|
+
</div>
|
|
2245
|
+
<div class="pane-actions">
|
|
2246
|
+
<button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
|
|
2247
|
+
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
2248
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2249
|
+
</div>
|
|
2250
|
+
</div>
|
|
2251
|
+
<div class="pane-filters">
|
|
2252
|
+
<div class="breadcrumbs" *ngIf="connected">
|
|
2253
|
+
<ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
|
|
2254
|
+
<button
|
|
2255
|
+
class="crumb-button"
|
|
2256
|
+
(click)="navigateRemoteBreadcrumb(i)"
|
|
2257
|
+
>
|
|
2258
|
+
{{ part.label }}
|
|
2259
|
+
</button>
|
|
2260
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2261
|
+
</ng-container>
|
|
2262
|
+
</div>
|
|
2263
|
+
<input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
|
|
2264
|
+
<label class="show-hidden-toggle">
|
|
2265
|
+
<input type="checkbox" [(ngModel)]="showHiddenRemote" />
|
|
2266
|
+
<span>Show hidden</span>
|
|
2267
|
+
</label>
|
|
2268
|
+
</div>
|
|
2269
|
+
<div class="pane-list"
|
|
2270
|
+
(dragover)="onDragOver($event)"
|
|
2271
|
+
(drop)="onDropOnRemote($event)"
|
|
2272
|
+
>
|
|
2273
|
+
<div class="entry dim" *ngIf="!connected">
|
|
2274
|
+
<span class="name">Not connected</span>
|
|
2275
|
+
</div>
|
|
2276
|
+
<div class="entry header" *ngIf="connected">
|
|
2277
|
+
<span class="icon"></span>
|
|
2278
|
+
<span class="name sortable" (click)="setRemoteSort('name')">Name</span>
|
|
2279
|
+
<span class="size sortable" (click)="setRemoteSort('size')">Size</span>
|
|
2280
|
+
<span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
|
|
2281
|
+
</div>
|
|
2282
|
+
<div
|
|
2283
|
+
class="entry"
|
|
2284
|
+
*ngIf="connected && remotePath !== '/'"
|
|
2285
|
+
(dblclick)="remoteUp()"
|
|
2286
|
+
>
|
|
2287
|
+
<span class="icon">⬆</span>
|
|
2288
|
+
<span class="name">Go up</span>
|
|
2289
|
+
<span class="size"></span>
|
|
2290
|
+
<span class="date"></span>
|
|
2291
|
+
</div>
|
|
2292
|
+
<div
|
|
2293
|
+
class="entry"
|
|
2294
|
+
*ngFor="let e of getFilteredRemoteEntries()"
|
|
2295
|
+
(click)="selectRemote(e, $event)"
|
|
2296
|
+
(dblclick)="openRemote(e)"
|
|
2297
|
+
(mousedown)="onRemoteMouseDown(e, $event)"
|
|
2298
|
+
(contextmenu)="onRemoteContextMenu(e, $event)"
|
|
2299
|
+
(dragover)="onRemoteEntryDragOver(e, $event)"
|
|
2300
|
+
(drop)="onRemoteEntryDrop(e, $event)"
|
|
2301
|
+
[class.drop-target]="remoteDropActive"
|
|
2302
|
+
[class.selected]="isRemoteSelected(e)"
|
|
2303
|
+
[draggable]="connected"
|
|
2304
|
+
(dragstart)="onDragStartRemote($event, e)"
|
|
2305
|
+
>
|
|
2306
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2307
|
+
<span class="name">{{ e.name }}</span>
|
|
2308
|
+
<span class="size">{{ getRemoteSizeDisplay(e) }}</span>
|
|
2309
|
+
<span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
|
|
2310
|
+
</div>
|
|
2311
|
+
</div>
|
|
2312
|
+
<div class="pane-actions-bar">
|
|
2313
|
+
<div class="selection" *ngIf="selectedRemote.length">
|
|
2314
|
+
Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
|
|
2315
|
+
</div>
|
|
2316
|
+
<div class="action-inputs">
|
|
2317
|
+
<input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
|
|
2318
|
+
<input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2319
|
+
</div>
|
|
2320
|
+
<div class="action-buttons">
|
|
2321
|
+
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
|
|
2322
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2323
|
+
<button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
|
|
2324
|
+
<button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
|
|
2325
|
+
<button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
|
|
2326
|
+
<button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
|
|
2327
|
+
<button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
|
|
2328
|
+
</div>
|
|
2329
|
+
</div>
|
|
2330
|
+
</div>
|
|
2331
|
+
</div>
|
|
2332
|
+
<div class="sftp-transfers" *ngIf="transfers.length">
|
|
2333
|
+
<div class="transfer" *ngFor="let t of transfers">
|
|
2334
|
+
<div class="transfer-main">
|
|
2335
|
+
<div class="transfer-title">
|
|
2336
|
+
<span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
|
|
2337
|
+
<span class="name">{{ t.name }}</span>
|
|
2338
|
+
</div>
|
|
2339
|
+
<div class="transfer-path">
|
|
2340
|
+
<span class="label">Remote:</span>
|
|
2341
|
+
<span class="value">{{ t.remotePath }}</span>
|
|
2342
|
+
</div>
|
|
2343
|
+
<div class="transfer-path">
|
|
2344
|
+
<span class="label">Local:</span>
|
|
2345
|
+
<span class="value">{{ t.localPath }}</span>
|
|
2346
|
+
</div>
|
|
2347
|
+
<div class="bar">
|
|
2348
|
+
<div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
|
|
2349
|
+
</div>
|
|
2350
|
+
</div>
|
|
2351
|
+
<div class="transfer-stats">
|
|
2352
|
+
<div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
|
|
2353
|
+
<div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
|
|
2354
|
+
<button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
|
|
2355
|
+
</div>
|
|
2356
|
+
</div>
|
|
2357
|
+
</div>
|
|
2358
|
+
|
|
2359
|
+
<div class="delete-overlay" *ngIf="deleteConfirmVisible">
|
|
2360
|
+
<div class="delete-dialog">
|
|
2361
|
+
<div class="delete-text">{{ deleteConfirmText }}</div>
|
|
2362
|
+
<div class="delete-buttons">
|
|
2363
|
+
<button class="danger" (click)="confirmDelete()">Delete</button>
|
|
2364
|
+
<button (click)="cancelDelete()">Cancel</button>
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
</div>
|
|
2368
|
+
|
|
2369
|
+
<div class="delete-overlay" *ngIf="inputDialogVisible">
|
|
2370
|
+
<div class="delete-dialog" (click)="$event.stopPropagation()">
|
|
2371
|
+
<div class="delete-text">{{ inputDialogTitle }}</div>
|
|
2372
|
+
<input
|
|
2373
|
+
class="dialog-input"
|
|
2374
|
+
[(ngModel)]="inputDialogValue"
|
|
2375
|
+
[placeholder]="inputDialogPlaceholder"
|
|
2376
|
+
(keyup.enter)="confirmInputDialog()"
|
|
2377
|
+
/>
|
|
2378
|
+
<div class="delete-buttons">
|
|
2379
|
+
<button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
|
|
2380
|
+
<button (click)="cancelInputDialog()">Cancel</button>
|
|
2381
|
+
</div>
|
|
2382
|
+
</div>
|
|
2383
|
+
</div>
|
|
2384
|
+
|
|
2385
|
+
<div
|
|
2386
|
+
class="local-menu"
|
|
2387
|
+
*ngIf="localMenuVisible"
|
|
2388
|
+
[style.left.px]="localMenuX"
|
|
2389
|
+
[style.top.px]="localMenuY"
|
|
2390
|
+
(click)="$event.stopPropagation()"
|
|
2391
|
+
>
|
|
2392
|
+
<div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
|
|
2393
|
+
{{ item.label }}
|
|
2394
|
+
</div>
|
|
2395
|
+
</div>
|
|
2396
|
+
</div>
|
|
2271
2397
|
`,
|
|
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)); }
|
|
2398
|
+
styles: [`
|
|
2399
|
+
.sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
|
|
2400
|
+
button { padding: 6px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.06); color: inherit; cursor: pointer; }
|
|
2401
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
2402
|
+
.top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
|
|
2403
|
+
.top-profiles .current .label,
|
|
2404
|
+
.top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
|
|
2405
|
+
.top-profiles .value { font-weight: 600; }
|
|
2406
|
+
.top-profiles .profile-chip { padding: 2px 8px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
|
|
2407
|
+
.top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
|
|
2408
|
+
.sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
|
|
2409
|
+
.pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
|
|
2410
|
+
.pane-title { display: grid; grid-template-columns: auto 1fr auto; align-items: center; gap: 10px; padding: 8px 10px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08); }
|
|
2411
|
+
.pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
|
|
2412
|
+
.pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
|
|
2413
|
+
.pane-path { opacity: 0.8; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2414
|
+
.pane-path input { width: 100%; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-family: inherit; font-size: 12px; }
|
|
2415
|
+
.pane-actions { display: flex; gap: 8px; align-items: center; }
|
|
2416
|
+
.pane-actions .path-preset,
|
|
2417
|
+
.pane-actions .path-favorite { max-width: 150px; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.22); background: rgba(20,20,20,0.95); color: inherit; font-size: 11px; }
|
|
2418
|
+
.pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
|
|
2419
|
+
.pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
|
|
2420
|
+
.pane-actions .fav-toggle { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.25); background: rgba(255,255,255,0.05); font-size: 11px; line-height: 1; }
|
|
2421
|
+
.pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
|
|
2422
|
+
.pane-filters { display: flex; align-items: center; gap: 8px; padding: 4px 8px; border-bottom: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.12); }
|
|
2423
|
+
.pane-filters input { flex: 1; padding: 4px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 12px; }
|
|
2424
|
+
.show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
|
|
2425
|
+
.show-hidden-toggle input[type="checkbox"] { margin: 0; }
|
|
2426
|
+
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
|
|
2427
|
+
.crumb-button { padding: 2px 6px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.18); background: rgba(255,255,255,0.04); color: inherit; cursor: pointer; font-size: 11px; }
|
|
2428
|
+
.crumb-button:hover { background: rgba(255,255,255,0.10); }
|
|
2429
|
+
.crumb-separator { opacity: 0.6; }
|
|
2430
|
+
.pane-list { flex: 1; overflow: auto; padding: 4px; }
|
|
2431
|
+
.entry { display: grid; grid-template-columns: 24px minmax(0, 1.5fr) 80px 140px; gap: 8px; padding: 6px 8px; border-radius: 8px; user-select: none; align-items: center; }
|
|
2432
|
+
.entry:hover { background: rgba(255,255,255,0.06); }
|
|
2433
|
+
.entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
|
|
2434
|
+
.entry.dim { opacity: 0.7; }
|
|
2435
|
+
.icon { text-align: center; opacity: 0.85; }
|
|
2436
|
+
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2437
|
+
.size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
|
|
2438
|
+
.date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
|
|
2439
|
+
.entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
|
|
2440
|
+
.sortable { cursor: pointer; }
|
|
2441
|
+
.entry.selected { background: rgba(80,160,255,0.18); }
|
|
2442
|
+
.pane-actions-bar { display: flex; flex-direction: column; gap: 4px; padding: 6px 8px; border-top: 1px solid rgba(255,255,255,0.06); background: rgba(0,0,0,0.18); }
|
|
2443
|
+
.pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
|
|
2444
|
+
.pane-actions-bar .action-inputs { display: flex; gap: 6px; }
|
|
2445
|
+
.pane-actions-bar .action-inputs input { flex: 1; padding: 3px 6px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 11px; }
|
|
2446
|
+
.pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
2447
|
+
.sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
|
|
2448
|
+
.transfer { display: grid; grid-template-columns: 1fr auto; gap: 8px; padding: 6px 8px; border-radius: 8px; background: rgba(255,255,255,0.04); border: 1px solid rgba(255,255,255,0.08); font-size: 11px; }
|
|
2449
|
+
.transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
|
|
2450
|
+
.transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
|
|
2451
|
+
.transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2452
|
+
.transfer-path { display: flex; gap: 4px; opacity: 0.75; }
|
|
2453
|
+
.transfer-path .label { min-width: 48px; }
|
|
2454
|
+
.transfer-path .value { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2455
|
+
.bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
|
|
2456
|
+
.bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
|
|
2457
|
+
.transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
|
|
2458
|
+
.transfer-stats .percent { font-weight: 600; }
|
|
2459
|
+
.transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2460
|
+
.btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
|
|
2461
|
+
.delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
|
|
2462
|
+
.delete-dialog { min-width: 260px; max-width: 360px; padding: 14px 16px; border-radius: 10px; background: rgba(20,20,20,0.96); border: 1px solid rgba(255,255,255,0.15); box-shadow: 0 18px 45px rgba(0,0,0,0.75); display: flex; flex-direction: column; gap: 10px; }
|
|
2463
|
+
.delete-text { font-size: 13px; }
|
|
2464
|
+
.dialog-input { width: 100%; padding: 8px 10px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.18); background: rgba(0,0,0,0.3); color: inherit; font-size: 13px; }
|
|
2465
|
+
.delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2466
|
+
.delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
|
|
2467
|
+
.local-menu { position: absolute; min-width: 180px; max-width: 260px; max-height: 260px; overflow-y: auto; padding: 4px 0; border-radius: 10px; background: rgba(18,18,22,0.98); border: 1px solid rgba(255,255,255,0.16); box-shadow: 0 18px 45px rgba(0,0,0,0.8); z-index: 30; backdrop-filter: blur(12px); }
|
|
2468
|
+
.local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
|
|
2469
|
+
.local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
|
|
2344
2470
|
`],
|
|
2345
2471
|
}),
|
|
2346
2472
|
__metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
|