tabby-sftp-ui 0.2.3 → 0.2.5
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 +29 -7
- package/dist/index.js +549 -413
- package/dist/index.js.map +1 -1
- package/dist/sftp-manager-tab.component.d.ts +3 -1
- package/dist/sftp-recovery-provider.d.ts +8 -0
- package/package.json +44 -44
package/dist/index.js
CHANGED
|
@@ -237,6 +237,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
237
237
|
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_6__);
|
|
238
238
|
/* harmony import */ var _local_transfers__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./local-transfers */ "./src/local-transfers.ts");
|
|
239
239
|
/* harmony import */ var _sftp_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp.service */ "./src/sftp.service.ts");
|
|
240
|
+
/* harmony import */ var _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-recovery-provider */ "./src/sftp-recovery-provider.ts");
|
|
240
241
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
241
242
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
242
243
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -255,6 +256,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
255
256
|
|
|
256
257
|
|
|
257
258
|
|
|
259
|
+
|
|
258
260
|
let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__WEBPACK_IMPORTED_MODULE_6__.BaseTabComponent {
|
|
259
261
|
constructor(injector, sftp, profilesService, app) {
|
|
260
262
|
// Tabby runtime BaseTabComponent expects Injector in constructor, but typings in this SDK may differ.
|
|
@@ -266,6 +268,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
266
268
|
// injected from the SSH tab when opened via SFTP-UI button
|
|
267
269
|
this.sshSession = null;
|
|
268
270
|
this.profile = null;
|
|
271
|
+
this.recoveredStub = false;
|
|
269
272
|
this.connecting = false;
|
|
270
273
|
this.connected = false;
|
|
271
274
|
// legacy UI fields kept for now (not used when opened from SSH tab)
|
|
@@ -353,9 +356,9 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
353
356
|
// If there's no live SSH session, this tab was likely restored across
|
|
354
357
|
// restart or opened in an invalid context. Close it immediately to avoid
|
|
355
358
|
// an empty, nameless SFTP tab lingering after restart.
|
|
356
|
-
if (!this.sshSession) {
|
|
359
|
+
if (this.recoveredStub || !this.sshSession) {
|
|
357
360
|
try {
|
|
358
|
-
this.app.closeTab(this);
|
|
361
|
+
this.app.closeTab(this.topmostParent ?? this);
|
|
359
362
|
}
|
|
360
363
|
catch (e) {
|
|
361
364
|
console.error('[SFTP-UI] Failed to close invalid SFTP tab', e);
|
|
@@ -596,12 +599,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
596
599
|
const sources = this.selectedLocal.includes(e) && this.selectedLocal.length ? this.selectedLocal : [e];
|
|
597
600
|
const movePayload = sources.map(x => x.fullPath);
|
|
598
601
|
ev.dataTransfer?.setData('application/x-tabby-sftp-ui-local-move', JSON.stringify(movePayload));
|
|
599
|
-
//
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
602
|
+
// Cross-device drag (local -> remote) supports multi-select
|
|
603
|
+
const payload = {
|
|
604
|
+
kind: 'local-paths',
|
|
605
|
+
paths: sources.map(x => ({ fullPath: x.fullPath, name: x.name, isDirectory: x.isDirectory })),
|
|
606
|
+
};
|
|
607
|
+
ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
|
|
608
|
+
ev.dataTransfer?.setData('text/plain', e.fullPath);
|
|
605
609
|
ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
|
|
606
610
|
}
|
|
607
611
|
onDragStartRemote(ev, item) {
|
|
@@ -611,10 +615,17 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
611
615
|
const sources = this.selectedRemote.includes(item) && this.selectedRemote.length ? this.selectedRemote : [item];
|
|
612
616
|
const movePayload = sources.map(x => x.fullPath);
|
|
613
617
|
ev.dataTransfer?.setData('application/x-tabby-sftp-ui-remote-move', JSON.stringify(movePayload));
|
|
614
|
-
//
|
|
615
|
-
const payload =
|
|
616
|
-
|
|
617
|
-
:
|
|
618
|
+
// Cross-device drag (remote -> local) supports multi-select
|
|
619
|
+
const payload = {
|
|
620
|
+
kind: 'remote-paths',
|
|
621
|
+
paths: sources.map(x => ({
|
|
622
|
+
remotePath: x.fullPath,
|
|
623
|
+
name: x.name,
|
|
624
|
+
isDirectory: x.isDirectory,
|
|
625
|
+
size: x.size,
|
|
626
|
+
mode: x.mode,
|
|
627
|
+
})),
|
|
628
|
+
};
|
|
618
629
|
ev.dataTransfer?.setData('application/x-tabby-sftp-ui', JSON.stringify(payload));
|
|
619
630
|
ev.dataTransfer?.setData('text/plain', item.fullPath);
|
|
620
631
|
ev.dataTransfer?.setDragImage?.(ev.target ?? document.body, 0, 0);
|
|
@@ -675,23 +686,40 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
675
686
|
catch {
|
|
676
687
|
return;
|
|
677
688
|
}
|
|
678
|
-
if (payload.kind !== 'local-file') {
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
689
|
try {
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
690
|
+
if (payload.kind === 'local-file') {
|
|
691
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, payload.name);
|
|
692
|
+
const existsOnRemote = this.remoteEntries.some(e => e.name === payload.name);
|
|
693
|
+
if (existsOnRemote) {
|
|
694
|
+
const ok = await this.showReplaceConfirm(`Replace existing "${payload.name}" on remote?`);
|
|
695
|
+
if (!ok) {
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
await this.deleteRemotePathRecursive(targetRemotePath);
|
|
688
699
|
}
|
|
689
|
-
|
|
700
|
+
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
701
|
+
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
702
|
+
await this.sftpSession.upload(targetRemotePath, upload);
|
|
703
|
+
await this.refreshRemote();
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
706
|
+
if (payload.kind === 'local-paths') {
|
|
707
|
+
for (const p of payload.paths) {
|
|
708
|
+
const targetRemotePath = path__WEBPACK_IMPORTED_MODULE_0__.posix.join(this.remotePath, p.name);
|
|
709
|
+
const existsOnRemote = this.remoteEntries.some(e => e.name === p.name);
|
|
710
|
+
if (existsOnRemote) {
|
|
711
|
+
const ok = await this.showReplaceConfirm(`Replace existing "${p.name}" on remote?`);
|
|
712
|
+
if (!ok) {
|
|
713
|
+
continue;
|
|
714
|
+
}
|
|
715
|
+
await this.deleteRemotePathRecursive(targetRemotePath);
|
|
716
|
+
}
|
|
717
|
+
// uploadLocalPathToRemote handles both files and directories
|
|
718
|
+
await this.uploadLocalPathToRemote(this.remotePath, p.fullPath);
|
|
719
|
+
}
|
|
720
|
+
await this.refreshRemote();
|
|
721
|
+
return;
|
|
690
722
|
}
|
|
691
|
-
const upload = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileUpload(payload.fullPath);
|
|
692
|
-
this.trackTransfer(upload, 'upload', targetRemotePath, payload.fullPath);
|
|
693
|
-
await this.sftpSession.upload(targetRemotePath, upload);
|
|
694
|
-
await this.refreshRemote();
|
|
695
723
|
}
|
|
696
724
|
catch (e) {
|
|
697
725
|
console.error('[SFTP-UI] Upload failed', e);
|
|
@@ -753,6 +781,36 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
753
781
|
}
|
|
754
782
|
return;
|
|
755
783
|
}
|
|
784
|
+
if (payload && payload.kind === 'remote-paths') {
|
|
785
|
+
try {
|
|
786
|
+
if (!this.sftpSession) {
|
|
787
|
+
throw new Error('Not connected');
|
|
788
|
+
}
|
|
789
|
+
for (const it of payload.paths) {
|
|
790
|
+
const targetLocalPath = path__WEBPACK_IMPORTED_MODULE_0__.join(this.localPath, it.name);
|
|
791
|
+
if (fs__WEBPACK_IMPORTED_MODULE_3__.existsSync(targetLocalPath)) {
|
|
792
|
+
const ok = await this.showReplaceConfirm(it.isDirectory ? `Replace existing folder "${it.name}"?` : `Replace existing "${it.name}"?`);
|
|
793
|
+
if (!ok) {
|
|
794
|
+
continue;
|
|
795
|
+
}
|
|
796
|
+
await this.deleteLocalPathRecursive(targetLocalPath);
|
|
797
|
+
}
|
|
798
|
+
if (it.isDirectory) {
|
|
799
|
+
await this.downloadRemoteDirectoryRecursive(it.remotePath, targetLocalPath);
|
|
800
|
+
}
|
|
801
|
+
else {
|
|
802
|
+
const dl = new _local_transfers__WEBPACK_IMPORTED_MODULE_7__.LocalPathFileDownload(targetLocalPath, it.mode ?? 0o644, it.size ?? 0);
|
|
803
|
+
this.trackTransfer(dl, 'download', it.remotePath, targetLocalPath);
|
|
804
|
+
await this.sftpSession.download(it.remotePath, dl);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
await this.refreshLocal();
|
|
808
|
+
}
|
|
809
|
+
catch (e) {
|
|
810
|
+
console.error('[SFTP-UI] Download paths failed', e);
|
|
811
|
+
}
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
756
814
|
}
|
|
757
815
|
// Drag & drop from OS file manager into the local pane (copy into current local folder)
|
|
758
816
|
const osPaths = this.getDroppedOsPaths(ev);
|
|
@@ -967,7 +1025,8 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
967
1025
|
const isWin = os__WEBPACK_IMPORTED_MODULE_1__.platform() === 'win32';
|
|
968
1026
|
const isLocalPath = (p) => {
|
|
969
1027
|
if (isWin) {
|
|
970
|
-
|
|
1028
|
+
// Accept both `C:\...` and `C:/...` (some drag sources provide forward slashes)
|
|
1029
|
+
return /^[A-Za-z]:[\\/]/.test(p) || p.startsWith('\\\\');
|
|
971
1030
|
}
|
|
972
1031
|
return p.startsWith('/');
|
|
973
1032
|
};
|
|
@@ -994,6 +1053,13 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
994
1053
|
}
|
|
995
1054
|
}
|
|
996
1055
|
return x;
|
|
1056
|
+
})
|
|
1057
|
+
.map(x => {
|
|
1058
|
+
// Some sources may produce `/C:/Users/...`
|
|
1059
|
+
if (isWin && /^\/[A-Za-z]:[\\/]/.test(x)) {
|
|
1060
|
+
return x.slice(1);
|
|
1061
|
+
}
|
|
1062
|
+
return x;
|
|
997
1063
|
})
|
|
998
1064
|
.filter(x => x && isLocalPath(x));
|
|
999
1065
|
if (uris.length) {
|
|
@@ -1013,6 +1079,12 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1013
1079
|
}
|
|
1014
1080
|
}
|
|
1015
1081
|
return x;
|
|
1082
|
+
})
|
|
1083
|
+
.map(x => {
|
|
1084
|
+
if (isWin && /^\/[A-Za-z]:[\\/]/.test(x)) {
|
|
1085
|
+
return x.slice(1);
|
|
1086
|
+
}
|
|
1087
|
+
return x;
|
|
1016
1088
|
})
|
|
1017
1089
|
.filter(x => x && isLocalPath(x));
|
|
1018
1090
|
return textPaths;
|
|
@@ -1522,7 +1594,7 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1522
1594
|
getDefaultRemotePath() {
|
|
1523
1595
|
const username = (this.profile && (this.profile.options?.username || this.profile.options?.user)) || '';
|
|
1524
1596
|
if (username) {
|
|
1525
|
-
return `/home/${username}`;
|
|
1597
|
+
return username === 'root' ? `/root` : `/home/${username}`;
|
|
1526
1598
|
}
|
|
1527
1599
|
return '/';
|
|
1528
1600
|
}
|
|
@@ -1967,12 +2039,16 @@ let SftpManagerTabComponent = class SftpManagerTabComponent extends tabby_core__
|
|
|
1967
2039
|
}
|
|
1968
2040
|
super.destroy();
|
|
1969
2041
|
}
|
|
1970
|
-
//
|
|
1971
|
-
//
|
|
1972
|
-
//
|
|
2042
|
+
// A custom recovery token ensures that a SplitTab wrapper won't become empty
|
|
2043
|
+
// after restart. On recovery we restore a stub SFTP tab which immediately
|
|
2044
|
+
// closes its `topmostParent` (the SplitTab wrapper).
|
|
1973
2045
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1974
2046
|
async getRecoveryToken(_options) {
|
|
1975
|
-
return
|
|
2047
|
+
return {
|
|
2048
|
+
type: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_9__.SFTP_RECOVERY_TYPE,
|
|
2049
|
+
version: 1,
|
|
2050
|
+
state: {},
|
|
2051
|
+
};
|
|
1976
2052
|
}
|
|
1977
2053
|
async confirmDelete() {
|
|
1978
2054
|
if (!this.deleteConfirmVisible) {
|
|
@@ -2270,388 +2346,388 @@ __decorate([
|
|
|
2270
2346
|
SftpManagerTabComponent = __decorate([
|
|
2271
2347
|
(0,_angular_core__WEBPACK_IMPORTED_MODULE_5__.Component)({
|
|
2272
2348
|
selector: 'tabby-sftp-manager-tab',
|
|
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>
|
|
2349
|
+
template: `
|
|
2350
|
+
<div class="sftp-root" tabindex="0" (keydown)="onKeyDown($event)">
|
|
2351
|
+
<div class="top-profiles" *ngIf="profile || recentProfiles.length">
|
|
2352
|
+
<div class="current" *ngIf="profile">
|
|
2353
|
+
<span class="label">Device:</span>
|
|
2354
|
+
<span class="value">{{ getProfileLabel(profile) }}</span>
|
|
2355
|
+
</div>
|
|
2356
|
+
<div class="recent" *ngIf="recentProfiles.length">
|
|
2357
|
+
<span class="label">Recent:</span>
|
|
2358
|
+
<button
|
|
2359
|
+
class="profile-chip"
|
|
2360
|
+
*ngFor="let p of recentProfiles"
|
|
2361
|
+
(click)="launchProfileFromSFTP(p)"
|
|
2362
|
+
>
|
|
2363
|
+
{{ getProfileLabel(p) }}
|
|
2364
|
+
</button>
|
|
2365
|
+
</div>
|
|
2366
|
+
</div>
|
|
2367
|
+
<div class="sftp-body">
|
|
2368
|
+
<div class="pane">
|
|
2369
|
+
<div class="pane-title">
|
|
2370
|
+
<div class="pane-label">Local</div>
|
|
2371
|
+
<div class="pane-path">
|
|
2372
|
+
<input
|
|
2373
|
+
[(ngModel)]="localPathInput"
|
|
2374
|
+
(keyup.enter)="goToLocalPathInput()"
|
|
2375
|
+
/>
|
|
2376
|
+
</div>
|
|
2377
|
+
<div class="pane-actions">
|
|
2378
|
+
<select class="path-preset" (change)="onLocalPresetChange($event.target.value)">
|
|
2379
|
+
<option value="">Go to…</option>
|
|
2380
|
+
<option *ngFor="let p of localPathPresets" [value]="p.id">
|
|
2381
|
+
{{ p.label }}
|
|
2382
|
+
</option>
|
|
2383
|
+
</select>
|
|
2384
|
+
<button
|
|
2385
|
+
class="fav-toggle"
|
|
2386
|
+
[class.active]="isCurrentFavorite()"
|
|
2387
|
+
(click)="toggleCurrentFavorite()"
|
|
2388
|
+
title="Toggle favorite for this path"
|
|
2389
|
+
>
|
|
2390
|
+
★
|
|
2391
|
+
</button>
|
|
2392
|
+
<select class="path-favorite" (change)="onLocalFavoriteSelect($event.target.value)">
|
|
2393
|
+
<option value="">Favorites…</option>
|
|
2394
|
+
<option *ngFor="let f of localFavorites" [value]="f.id">
|
|
2395
|
+
{{ f.label }}
|
|
2396
|
+
</option>
|
|
2397
|
+
</select>
|
|
2398
|
+
<button (click)="localUp()" [disabled]="!canLocalUp()">Up</button>
|
|
2399
|
+
<button (click)="goToLocalPathInput()">Go</button>
|
|
2400
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2401
|
+
</div>
|
|
2402
|
+
</div>
|
|
2403
|
+
<div class="pane-filters">
|
|
2404
|
+
<div class="breadcrumbs">
|
|
2405
|
+
<ng-container *ngFor="let part of getLocalBreadcrumbs(); let i = index; let last = last">
|
|
2406
|
+
<button
|
|
2407
|
+
class="crumb-button"
|
|
2408
|
+
(click)="navigateLocalBreadcrumb(i)"
|
|
2409
|
+
(contextmenu)="onLocalBreadcrumbContextMenu(i, $event)"
|
|
2410
|
+
>
|
|
2411
|
+
{{ part.label }}
|
|
2412
|
+
</button>
|
|
2413
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2414
|
+
</ng-container>
|
|
2415
|
+
</div>
|
|
2416
|
+
<input [(ngModel)]="localFilter" placeholder="Filter files..." />
|
|
2417
|
+
<label class="show-hidden-toggle">
|
|
2418
|
+
<input type="checkbox" [(ngModel)]="showHiddenLocal" />
|
|
2419
|
+
<span>Show hidden</span>
|
|
2420
|
+
</label>
|
|
2421
|
+
</div>
|
|
2422
|
+
<div class="pane-list"
|
|
2423
|
+
(dragover)="onDragOver($event)"
|
|
2424
|
+
(drop)="onDropOnLocal($event)"
|
|
2425
|
+
>
|
|
2426
|
+
<div class="entry header">
|
|
2427
|
+
<span class="icon"></span>
|
|
2428
|
+
<span class="name sortable" (click)="setLocalSort('name')">Name</span>
|
|
2429
|
+
<span class="size sortable" (click)="setLocalSort('size')">Size</span>
|
|
2430
|
+
<span class="date sortable" (click)="setLocalSort('modified')">Modified</span>
|
|
2431
|
+
</div>
|
|
2432
|
+
<div
|
|
2433
|
+
class="entry"
|
|
2434
|
+
*ngIf="canLocalUp()"
|
|
2435
|
+
(dblclick)="localUp()"
|
|
2436
|
+
>
|
|
2437
|
+
<span class="icon">⬆</span>
|
|
2438
|
+
<span class="name">Go up</span>
|
|
2439
|
+
<span class="size"></span>
|
|
2440
|
+
<span class="date"></span>
|
|
2441
|
+
</div>
|
|
2442
|
+
<div
|
|
2443
|
+
class="entry"
|
|
2444
|
+
*ngFor="let e of getFilteredLocalEntries()"
|
|
2445
|
+
(click)="selectLocal(e, $event)"
|
|
2446
|
+
(dblclick)="openLocal(e)"
|
|
2447
|
+
(mousedown)="onLocalMouseDown(e, $event)"
|
|
2448
|
+
(contextmenu)="onLocalContextMenu(e, $event)"
|
|
2449
|
+
(dragover)="onLocalEntryDragOver(e, $event)"
|
|
2450
|
+
(drop)="onLocalEntryDrop(e, $event)"
|
|
2451
|
+
[class.drop-target]="localDropActive"
|
|
2452
|
+
[class.selected]="isLocalSelected(e)"
|
|
2453
|
+
[draggable]="true"
|
|
2454
|
+
(dragstart)="onDragStartLocal($event, e)"
|
|
2455
|
+
>
|
|
2456
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2457
|
+
<span class="name">{{ e.name }}</span>
|
|
2458
|
+
<span class="size">{{ getLocalSizeDisplay(e) }}</span>
|
|
2459
|
+
<span class="date">{{ e.mtimeMs ? (e.mtimeMs | date:'yyyy-MM-dd HH:mm') : '' }}</span>
|
|
2460
|
+
</div>
|
|
2461
|
+
</div>
|
|
2462
|
+
<div class="pane-actions-bar">
|
|
2463
|
+
<div class="selection" *ngIf="selectedLocal.length">
|
|
2464
|
+
Selected: {{ selectedLocal.length === 1 ? selectedLocal[0].name : (selectedLocal.length + ' items') }}
|
|
2465
|
+
</div>
|
|
2466
|
+
<div class="action-inputs">
|
|
2467
|
+
<input [(ngModel)]="localActionName" placeholder="Name / new name" />
|
|
2468
|
+
<input [(ngModel)]="localActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2469
|
+
</div>
|
|
2470
|
+
<div class="action-buttons">
|
|
2471
|
+
<button (click)="localRename()" [disabled]="selectedLocal.length !== 1">Rename</button>
|
|
2472
|
+
<button (click)="refreshLocal()">Refresh</button>
|
|
2473
|
+
<button (click)="localDelete()" [disabled]="!selectedLocal.length">Delete</button>
|
|
2474
|
+
<button (click)="localNewFolder()">New Folder</button>
|
|
2475
|
+
<button (click)="localEditPermissions()" [disabled]="selectedLocal.length !== 1 || !localActionPerms">Edit Permissions</button>
|
|
2476
|
+
<button (click)="localShowSize()" [disabled]="selectedLocal.length !== 1 || !selectedLocal[0].isDirectory">Show Size</button>
|
|
2477
|
+
</div>
|
|
2478
|
+
</div>
|
|
2479
|
+
</div>
|
|
2480
|
+
|
|
2481
|
+
<div class="pane">
|
|
2482
|
+
<div class="pane-title">
|
|
2483
|
+
<div class="pane-label">
|
|
2484
|
+
Remote
|
|
2485
|
+
<span *ngIf="connected && profile?.options?.host" class="pane-sub">
|
|
2486
|
+
— {{ profile.options.host }}
|
|
2487
|
+
</span>
|
|
2488
|
+
</div>
|
|
2489
|
+
<div class="pane-path">
|
|
2490
|
+
<input
|
|
2491
|
+
[(ngModel)]="remotePathInput"
|
|
2492
|
+
(keyup.enter)="goToRemotePathInput()"
|
|
2493
|
+
[disabled]="!connected"
|
|
2494
|
+
/>
|
|
2495
|
+
</div>
|
|
2496
|
+
<div class="pane-actions">
|
|
2497
|
+
<button (click)="remoteUp()" [disabled]="!connected || remotePath === '/'">Up</button>
|
|
2498
|
+
<button (click)="goToRemotePathInput()" [disabled]="!connected">Go</button>
|
|
2499
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2500
|
+
</div>
|
|
2501
|
+
</div>
|
|
2502
|
+
<div class="pane-filters">
|
|
2503
|
+
<div class="breadcrumbs" *ngIf="connected">
|
|
2504
|
+
<ng-container *ngFor="let part of getRemoteBreadcrumbs(); let i = index; let last = last">
|
|
2505
|
+
<button
|
|
2506
|
+
class="crumb-button"
|
|
2507
|
+
(click)="navigateRemoteBreadcrumb(i)"
|
|
2508
|
+
>
|
|
2509
|
+
{{ part.label }}
|
|
2510
|
+
</button>
|
|
2511
|
+
<span class="crumb-separator" *ngIf="!last">›</span>
|
|
2512
|
+
</ng-container>
|
|
2513
|
+
</div>
|
|
2514
|
+
<input [(ngModel)]="remoteFilter" placeholder="Filter files..." />
|
|
2515
|
+
<label class="show-hidden-toggle">
|
|
2516
|
+
<input type="checkbox" [(ngModel)]="showHiddenRemote" />
|
|
2517
|
+
<span>Show hidden</span>
|
|
2518
|
+
</label>
|
|
2519
|
+
</div>
|
|
2520
|
+
<div class="pane-list"
|
|
2521
|
+
(dragover)="onDragOver($event)"
|
|
2522
|
+
(drop)="onDropOnRemote($event)"
|
|
2523
|
+
>
|
|
2524
|
+
<div class="entry dim" *ngIf="!connected">
|
|
2525
|
+
<span class="name">Not connected</span>
|
|
2526
|
+
</div>
|
|
2527
|
+
<div class="entry header" *ngIf="connected">
|
|
2528
|
+
<span class="icon"></span>
|
|
2529
|
+
<span class="name sortable" (click)="setRemoteSort('name')">Name</span>
|
|
2530
|
+
<span class="size sortable" (click)="setRemoteSort('size')">Size</span>
|
|
2531
|
+
<span class="date sortable" (click)="setRemoteSort('modified')">Modified</span>
|
|
2532
|
+
</div>
|
|
2533
|
+
<div
|
|
2534
|
+
class="entry"
|
|
2535
|
+
*ngIf="connected && remotePath !== '/'"
|
|
2536
|
+
(dblclick)="remoteUp()"
|
|
2537
|
+
>
|
|
2538
|
+
<span class="icon">⬆</span>
|
|
2539
|
+
<span class="name">Go up</span>
|
|
2540
|
+
<span class="size"></span>
|
|
2541
|
+
<span class="date"></span>
|
|
2542
|
+
</div>
|
|
2543
|
+
<div
|
|
2544
|
+
class="entry"
|
|
2545
|
+
*ngFor="let e of getFilteredRemoteEntries()"
|
|
2546
|
+
(click)="selectRemote(e, $event)"
|
|
2547
|
+
(dblclick)="openRemote(e)"
|
|
2548
|
+
(mousedown)="onRemoteMouseDown(e, $event)"
|
|
2549
|
+
(contextmenu)="onRemoteContextMenu(e, $event)"
|
|
2550
|
+
(dragover)="onRemoteEntryDragOver(e, $event)"
|
|
2551
|
+
(drop)="onRemoteEntryDrop(e, $event)"
|
|
2552
|
+
[class.drop-target]="remoteDropActive"
|
|
2553
|
+
[class.selected]="isRemoteSelected(e)"
|
|
2554
|
+
[draggable]="connected"
|
|
2555
|
+
(dragstart)="onDragStartRemote($event, e)"
|
|
2556
|
+
>
|
|
2557
|
+
<span class="icon">{{ e.isDirectory ? '📁' : '📄' }}</span>
|
|
2558
|
+
<span class="name">{{ e.name }}</span>
|
|
2559
|
+
<span class="size">{{ getRemoteSizeDisplay(e) }}</span>
|
|
2560
|
+
<span class="date">{{ e.modified | date:'yyyy-MM-dd HH:mm' }}</span>
|
|
2561
|
+
</div>
|
|
2562
|
+
</div>
|
|
2563
|
+
<div class="pane-actions-bar">
|
|
2564
|
+
<div class="selection" *ngIf="selectedRemote.length">
|
|
2565
|
+
Selected: {{ selectedRemote.length === 1 ? selectedRemote[0].name : (selectedRemote.length + ' items') }}
|
|
2566
|
+
</div>
|
|
2567
|
+
<div class="action-inputs">
|
|
2568
|
+
<input [(ngModel)]="remoteActionName" placeholder="Name / new name" />
|
|
2569
|
+
<input [(ngModel)]="remoteActionPerms" placeholder="Perms (e.g. 755)" />
|
|
2570
|
+
</div>
|
|
2571
|
+
<div class="action-buttons">
|
|
2572
|
+
<button (click)="remoteRename()" [disabled]="selectedRemote.length !== 1">Rename</button>
|
|
2573
|
+
<button (click)="refreshRemote()" [disabled]="!connected">Refresh</button>
|
|
2574
|
+
<button (click)="remoteDelete()" [disabled]="!selectedRemote.length">Delete</button>
|
|
2575
|
+
<button (click)="remoteNewFolder()" [disabled]="!connected">New Folder</button>
|
|
2576
|
+
<button (click)="remoteEditPermissions()" [disabled]="selectedRemote.length !== 1 || !remoteActionPerms">Edit Permissions</button>
|
|
2577
|
+
<button (click)="remoteShowSize()" [disabled]="selectedRemote.length !== 1 || !selectedRemote[0].isDirectory">Show Size</button>
|
|
2578
|
+
<button (click)="remoteDownload()" [disabled]="!selectedRemote.length">Download</button>
|
|
2579
|
+
</div>
|
|
2580
|
+
</div>
|
|
2581
|
+
</div>
|
|
2582
|
+
</div>
|
|
2583
|
+
<div class="sftp-transfers" *ngIf="transfers.length">
|
|
2584
|
+
<div class="transfer" *ngFor="let t of transfers">
|
|
2585
|
+
<div class="transfer-main">
|
|
2586
|
+
<div class="transfer-title">
|
|
2587
|
+
<span class="direction">{{ t.direction === 'upload' ? 'Upload' : 'Download' }}</span>
|
|
2588
|
+
<span class="name">{{ t.name }}</span>
|
|
2589
|
+
</div>
|
|
2590
|
+
<div class="transfer-path">
|
|
2591
|
+
<span class="label">Remote:</span>
|
|
2592
|
+
<span class="value">{{ t.remotePath }}</span>
|
|
2593
|
+
</div>
|
|
2594
|
+
<div class="transfer-path">
|
|
2595
|
+
<span class="label">Local:</span>
|
|
2596
|
+
<span class="value">{{ t.localPath }}</span>
|
|
2597
|
+
</div>
|
|
2598
|
+
<div class="bar">
|
|
2599
|
+
<div class="fill" [style.width.%]="getTransferProgress(t.transfer)"></div>
|
|
2600
|
+
</div>
|
|
2601
|
+
</div>
|
|
2602
|
+
<div class="transfer-stats">
|
|
2603
|
+
<div class="percent">{{ getTransferProgress(t.transfer) | number:'1.0-0' }}%</div>
|
|
2604
|
+
<div class="speed">{{ formatSpeed(t.transfer.getSpeed()) }}</div>
|
|
2605
|
+
<button class="btn-cancel" (click)="cancelTransfer(t)" [disabled]="t.transfer.isComplete() || t.transfer.isCancelled()">Cancel</button>
|
|
2606
|
+
</div>
|
|
2607
|
+
</div>
|
|
2608
|
+
</div>
|
|
2609
|
+
|
|
2610
|
+
<div class="delete-overlay" *ngIf="deleteConfirmVisible">
|
|
2611
|
+
<div class="delete-dialog">
|
|
2612
|
+
<div class="delete-text">{{ deleteConfirmText }}</div>
|
|
2613
|
+
<div class="delete-buttons">
|
|
2614
|
+
<button class="danger" (click)="confirmDelete()">Delete</button>
|
|
2615
|
+
<button (click)="cancelDelete()">Cancel</button>
|
|
2616
|
+
</div>
|
|
2617
|
+
</div>
|
|
2618
|
+
</div>
|
|
2619
|
+
|
|
2620
|
+
<div class="delete-overlay" *ngIf="replaceConfirmVisible">
|
|
2621
|
+
<div class="delete-dialog">
|
|
2622
|
+
<div class="delete-text">{{ replaceConfirmText }}</div>
|
|
2623
|
+
<div class="delete-buttons">
|
|
2624
|
+
<button class="danger" (click)="confirmReplace()">Replace</button>
|
|
2625
|
+
<button (click)="cancelReplace()">Cancel</button>
|
|
2626
|
+
</div>
|
|
2627
|
+
</div>
|
|
2628
|
+
</div>
|
|
2629
|
+
|
|
2630
|
+
<div class="delete-overlay" *ngIf="inputDialogVisible">
|
|
2631
|
+
<div class="delete-dialog" (click)="$event.stopPropagation()">
|
|
2632
|
+
<div class="delete-text">{{ inputDialogTitle }}</div>
|
|
2633
|
+
<input
|
|
2634
|
+
class="dialog-input"
|
|
2635
|
+
[(ngModel)]="inputDialogValue"
|
|
2636
|
+
[placeholder]="inputDialogPlaceholder"
|
|
2637
|
+
(keyup.enter)="confirmInputDialog()"
|
|
2638
|
+
/>
|
|
2639
|
+
<div class="delete-buttons">
|
|
2640
|
+
<button class="danger" (click)="confirmInputDialog()" [disabled]="!inputDialogValue.trim()">OK</button>
|
|
2641
|
+
<button (click)="cancelInputDialog()">Cancel</button>
|
|
2642
|
+
</div>
|
|
2643
|
+
</div>
|
|
2644
|
+
</div>
|
|
2645
|
+
|
|
2646
|
+
<div
|
|
2647
|
+
class="local-menu"
|
|
2648
|
+
*ngIf="localMenuVisible"
|
|
2649
|
+
[style.left.px]="localMenuX"
|
|
2650
|
+
[style.top.px]="localMenuY"
|
|
2651
|
+
(click)="$event.stopPropagation()"
|
|
2652
|
+
>
|
|
2653
|
+
<div class="local-menu-item" *ngFor="let item of localMenuItems" (click)="onLocalMenuItemClick(item)">
|
|
2654
|
+
{{ item.label }}
|
|
2655
|
+
</div>
|
|
2656
|
+
</div>
|
|
2657
|
+
</div>
|
|
2582
2658
|
`,
|
|
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)); }
|
|
2659
|
+
styles: [`
|
|
2660
|
+
.sftp-root { display: flex; flex-direction: column; height: 100%; padding: 10px; gap: 10px; position: relative; }
|
|
2661
|
+
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; }
|
|
2662
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
2663
|
+
.top-profiles { display: flex; justify-content: space-between; align-items: center; padding: 4px 8px 8px; gap: 12px; font-size: 11px; opacity: 0.9; }
|
|
2664
|
+
.top-profiles .current .label,
|
|
2665
|
+
.top-profiles .recent .label { text-transform: uppercase; letter-spacing: 0.05em; opacity: 0.7; margin-right: 4px; }
|
|
2666
|
+
.top-profiles .value { font-weight: 600; }
|
|
2667
|
+
.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; }
|
|
2668
|
+
.top-profiles .profile-chip:hover { background: rgba(255,255,255,0.12); }
|
|
2669
|
+
.sftp-body { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; flex: 1; min-height: 0; }
|
|
2670
|
+
.pane { display: flex; flex-direction: column; border: 1px solid rgba(255,255,255,0.12); border-radius: 10px; overflow: hidden; min-height: 0; }
|
|
2671
|
+
.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); }
|
|
2672
|
+
.pane-label { font-weight: 600; display: flex; align-items: baseline; gap: 6px; }
|
|
2673
|
+
.pane-sub { font-weight: 400; font-size: 11px; opacity: 0.75; }
|
|
2674
|
+
.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; }
|
|
2675
|
+
.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; }
|
|
2676
|
+
.pane-actions { display: flex; gap: 8px; align-items: center; }
|
|
2677
|
+
.pane-actions .path-preset,
|
|
2678
|
+
.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; }
|
|
2679
|
+
.pane-actions .path-preset option { background: #151515; color: #f5f5f5; }
|
|
2680
|
+
.pane-actions .path-favorite option { background: #151515; color: #f5f5f5; }
|
|
2681
|
+
.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; }
|
|
2682
|
+
.pane-actions .fav-toggle.active { background: rgba(255,215,0,0.2); border-color: rgba(255,215,0,0.6); color: #ffd700; }
|
|
2683
|
+
.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); }
|
|
2684
|
+
.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; }
|
|
2685
|
+
.show-hidden-toggle { display: flex; align-items: center; gap: 4px; font-size: 11px; opacity: 0.8; white-space: nowrap; }
|
|
2686
|
+
.show-hidden-toggle input[type="checkbox"] { margin: 0; }
|
|
2687
|
+
.breadcrumbs { display: flex; flex-wrap: wrap; gap: 4px; font-size: 11px; opacity: 0.9; align-items: center; }
|
|
2688
|
+
.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; }
|
|
2689
|
+
.crumb-button:hover { background: rgba(255,255,255,0.10); }
|
|
2690
|
+
.crumb-separator { opacity: 0.6; }
|
|
2691
|
+
.pane-list { flex: 1; overflow: auto; padding: 4px; }
|
|
2692
|
+
.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; }
|
|
2693
|
+
.entry:hover { background: rgba(255,255,255,0.06); }
|
|
2694
|
+
.entry.drop-target { outline: 1px dashed rgba(255,255,255,0.35); background: rgba(80, 160, 255, 0.10); }
|
|
2695
|
+
.entry.dim { opacity: 0.7; }
|
|
2696
|
+
.icon { text-align: center; opacity: 0.85; }
|
|
2697
|
+
.name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2698
|
+
.size { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; text-align: right; opacity: 0.8; }
|
|
2699
|
+
.date { font-size: 11px; opacity: 0.75; text-align: right; white-space: nowrap; }
|
|
2700
|
+
.entry.header { font-weight: 600; opacity: 0.9; background: rgba(255,255,255,0.02); }
|
|
2701
|
+
.sortable { cursor: pointer; }
|
|
2702
|
+
.entry.selected { background: rgba(80,160,255,0.18); }
|
|
2703
|
+
.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); }
|
|
2704
|
+
.pane-actions-bar .selection { font-size: 11px; opacity: 0.85; }
|
|
2705
|
+
.pane-actions-bar .action-inputs { display: flex; gap: 6px; }
|
|
2706
|
+
.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; }
|
|
2707
|
+
.pane-actions-bar .action-buttons { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 4px; }
|
|
2708
|
+
.sftp-transfers { margin-top: 8px; display: flex; flex-direction: column; gap: 6px; max-height: 120px; overflow-y: auto; }
|
|
2709
|
+
.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; }
|
|
2710
|
+
.transfer-title { display: flex; gap: 6px; align-items: baseline; margin-bottom: 2px; }
|
|
2711
|
+
.transfer-title .direction { text-transform: uppercase; letter-spacing: 0.04em; opacity: 0.7; font-weight: 600; font-size: 10px; }
|
|
2712
|
+
.transfer-title .name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2713
|
+
.transfer-path { display: flex; gap: 4px; opacity: 0.75; }
|
|
2714
|
+
.transfer-path .label { min-width: 48px; }
|
|
2715
|
+
.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; }
|
|
2716
|
+
.bar { position: relative; height: 4px; border-radius: 999px; background: rgba(255,255,255,0.07); margin-top: 4px; overflow: hidden; }
|
|
2717
|
+
.bar .fill { position: absolute; left: 0; top: 0; bottom: 0; border-radius: inherit; background: linear-gradient(90deg, #4dabff, #78ffce); transition: width 0.15s linear; }
|
|
2718
|
+
.transfer-stats { display: flex; flex-direction: column; justify-content: center; align-items: flex-end; gap: 4px; opacity: 0.8; }
|
|
2719
|
+
.transfer-stats .percent { font-weight: 600; }
|
|
2720
|
+
.transfer-stats .speed { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; }
|
|
2721
|
+
.btn-cancel { padding: 2px 6px; font-size: 10px; border-radius: 999px; }
|
|
2722
|
+
.delete-overlay { position: absolute; inset: 0; background: rgba(0,0,0,0.55); display: flex; align-items: center; justify-content: center; z-index: 20; }
|
|
2723
|
+
.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; }
|
|
2724
|
+
.delete-text { font-size: 13px; }
|
|
2725
|
+
.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; }
|
|
2726
|
+
.delete-buttons { display: flex; justify-content: flex-end; gap: 8px; }
|
|
2727
|
+
.delete-buttons .danger { background: rgba(255,80,80,0.85); border-color: rgba(255,120,120,0.85); }
|
|
2728
|
+
.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); }
|
|
2729
|
+
.local-menu-item { padding: 6px 12px; font-size: 12px; cursor: pointer; white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
|
|
2730
|
+
.local-menu-item:hover { background: linear-gradient(90deg, rgba(120,200,255,0.24), rgba(120,255,206,0.15)); }
|
|
2655
2731
|
`],
|
|
2656
2732
|
}),
|
|
2657
2733
|
__metadata("design:paramtypes", [_angular_core__WEBPACK_IMPORTED_MODULE_5__.Injector,
|
|
@@ -2662,6 +2738,57 @@ SftpManagerTabComponent = __decorate([
|
|
|
2662
2738
|
|
|
2663
2739
|
|
|
2664
2740
|
|
|
2741
|
+
/***/ },
|
|
2742
|
+
|
|
2743
|
+
/***/ "./src/sftp-recovery-provider.ts"
|
|
2744
|
+
/*!***************************************!*\
|
|
2745
|
+
!*** ./src/sftp-recovery-provider.ts ***!
|
|
2746
|
+
\***************************************/
|
|
2747
|
+
(__unused_webpack_module, __webpack_exports__, __webpack_require__) {
|
|
2748
|
+
|
|
2749
|
+
__webpack_require__.r(__webpack_exports__);
|
|
2750
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2751
|
+
/* harmony export */ SFTP_RECOVERY_TYPE: () => (/* binding */ SFTP_RECOVERY_TYPE),
|
|
2752
|
+
/* harmony export */ SftpTabRecoveryProvider: () => (/* binding */ SftpTabRecoveryProvider)
|
|
2753
|
+
/* harmony export */ });
|
|
2754
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @angular/core */ "@angular/core");
|
|
2755
|
+
/* harmony import */ var _angular_core__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_angular_core__WEBPACK_IMPORTED_MODULE_0__);
|
|
2756
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! tabby-core */ "tabby-core");
|
|
2757
|
+
/* harmony import */ var tabby_core__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(tabby_core__WEBPACK_IMPORTED_MODULE_1__);
|
|
2758
|
+
/* harmony import */ var _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./sftp-manager-tab.component */ "./src/sftp-manager-tab.component.ts");
|
|
2759
|
+
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
2760
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
2761
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
2762
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
2763
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
2764
|
+
};
|
|
2765
|
+
|
|
2766
|
+
|
|
2767
|
+
|
|
2768
|
+
const SFTP_RECOVERY_TYPE = 'tabby-sftp-ui:sftp-tab';
|
|
2769
|
+
let SftpTabRecoveryProvider = class SftpTabRecoveryProvider extends tabby_core__WEBPACK_IMPORTED_MODULE_1__.TabRecoveryProvider {
|
|
2770
|
+
async applicableTo(recoveryToken) {
|
|
2771
|
+
return recoveryToken?.type === SFTP_RECOVERY_TYPE;
|
|
2772
|
+
}
|
|
2773
|
+
async recover(_recoveryToken) {
|
|
2774
|
+
// Restore a stub tab that immediately closes itself and its SplitTab wrapper.
|
|
2775
|
+
return {
|
|
2776
|
+
type: _sftp_manager_tab_component__WEBPACK_IMPORTED_MODULE_2__.SftpManagerTabComponent,
|
|
2777
|
+
inputs: {
|
|
2778
|
+
sshSession: null,
|
|
2779
|
+
profile: null,
|
|
2780
|
+
recoveredStub: true,
|
|
2781
|
+
},
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
SftpTabRecoveryProvider = __decorate([
|
|
2786
|
+
(0,_angular_core__WEBPACK_IMPORTED_MODULE_0__.Injectable)()
|
|
2787
|
+
], SftpTabRecoveryProvider);
|
|
2788
|
+
|
|
2789
|
+
|
|
2790
|
+
|
|
2791
|
+
|
|
2665
2792
|
/***/ },
|
|
2666
2793
|
|
|
2667
2794
|
/***/ "./src/sftp-terminal-decorator.ts"
|
|
@@ -2699,7 +2826,13 @@ let SftpTerminalDecorator = class SftpTerminalDecorator extends tabby_terminal__
|
|
|
2699
2826
|
attach(terminal) {
|
|
2700
2827
|
super.attach(terminal);
|
|
2701
2828
|
// Best-effort DOM injection: place button near the existing Reconnect button if present.
|
|
2829
|
+
// Only inject for SSH tabs (with a live sshSession property).
|
|
2702
2830
|
const tryInsert = () => {
|
|
2831
|
+
// Skip for local terminals (PowerShell, CMD, etc.) - only SSH tabs need this button.
|
|
2832
|
+
const sshSession = terminal.sshSession;
|
|
2833
|
+
if (!sshSession) {
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2703
2836
|
try {
|
|
2704
2837
|
const host = terminal.element?.nativeElement ?? null;
|
|
2705
2838
|
if (!host) {
|
|
@@ -3093,6 +3226,7 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
3093
3226
|
/* harmony import */ var _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(/*! ./sftp-hotkey */ "./src/sftp-hotkey.ts");
|
|
3094
3227
|
/* harmony import */ var _sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(/*! ./sftp-ui.service */ "./src/sftp-ui.service.ts");
|
|
3095
3228
|
/* harmony import */ var _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(/*! ./sftp-terminal-decorator */ "./src/sftp-terminal-decorator.ts");
|
|
3229
|
+
/* harmony import */ var _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(/*! ./sftp-recovery-provider */ "./src/sftp-recovery-provider.ts");
|
|
3096
3230
|
var __decorate = (undefined && undefined.__decorate) || function (decorators, target, key, desc) {
|
|
3097
3231
|
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3098
3232
|
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
@@ -3112,6 +3246,7 @@ var __metadata = (undefined && undefined.__metadata) || function (k, v) {
|
|
|
3112
3246
|
|
|
3113
3247
|
|
|
3114
3248
|
|
|
3249
|
+
|
|
3115
3250
|
let SftpUiModule = class SftpUiModule {
|
|
3116
3251
|
constructor(_) { }
|
|
3117
3252
|
};
|
|
@@ -3128,6 +3263,7 @@ SftpUiModule = __decorate([
|
|
|
3128
3263
|
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabContextMenuItemProvider, useClass: _sftp_context_menu__WEBPACK_IMPORTED_MODULE_6__.SftpContextMenuProvider, multi: true },
|
|
3129
3264
|
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.HotkeyProvider, useClass: _sftp_hotkey__WEBPACK_IMPORTED_MODULE_7__.SftpUiHotkeyProvider, multi: true },
|
|
3130
3265
|
{ provide: tabby_terminal__WEBPACK_IMPORTED_MODULE_4__.TerminalDecorator, useClass: _sftp_terminal_decorator__WEBPACK_IMPORTED_MODULE_9__.SftpTerminalDecorator, multi: true },
|
|
3266
|
+
{ provide: tabby_core__WEBPACK_IMPORTED_MODULE_3__.TabRecoveryProvider, useClass: _sftp_recovery_provider__WEBPACK_IMPORTED_MODULE_10__.SftpTabRecoveryProvider, multi: true },
|
|
3131
3267
|
_sftp_ui_service__WEBPACK_IMPORTED_MODULE_8__.SftpUiService,
|
|
3132
3268
|
],
|
|
3133
3269
|
}),
|