@memberjunction/ng-file-storage 6.2.0-edge.0 → 6.2.0-edge.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/admin/storage-admin-dialog.component.d.ts +1 -1
- package/dist/lib/admin/storage-admin-dialog.component.js +2 -2
- package/dist/lib/admin/storage-admin-dialog.component.js.map +1 -1
- package/dist/lib/category-tree/category-tree.d.ts +83 -9
- package/dist/lib/category-tree/category-tree.d.ts.map +1 -1
- package/dist/lib/category-tree/category-tree.js +192 -50
- package/dist/lib/category-tree/category-tree.js.map +1 -1
- package/dist/lib/file-browser/file-browser.component.d.ts +30 -4
- package/dist/lib/file-browser/file-browser.component.d.ts.map +1 -1
- package/dist/lib/file-browser/file-browser.component.js +68 -16
- package/dist/lib/file-browser/file-browser.component.js.map +1 -1
- package/dist/lib/file-browser/file-grid.component.d.ts +404 -58
- package/dist/lib/file-browser/file-grid.component.d.ts.map +1 -1
- package/dist/lib/file-browser/file-grid.component.js +1040 -396
- package/dist/lib/file-browser/file-grid.component.js.map +1 -1
- package/dist/lib/file-browser/folder-tree.component.d.ts +65 -9
- package/dist/lib/file-browser/folder-tree.component.d.ts.map +1 -1
- package/dist/lib/file-browser/folder-tree.component.js +166 -58
- package/dist/lib/file-browser/folder-tree.component.js.map +1 -1
- package/dist/lib/file-browser/storage-providers-list.component.d.ts +55 -7
- package/dist/lib/file-browser/storage-providers-list.component.d.ts.map +1 -1
- package/dist/lib/file-browser/storage-providers-list.component.js +124 -34
- package/dist/lib/file-browser/storage-providers-list.component.js.map +1 -1
- package/dist/lib/file-upload/file-upload.d.ts +22 -2
- package/dist/lib/file-upload/file-upload.d.ts.map +1 -1
- package/dist/lib/file-upload/file-upload.js +40 -10
- package/dist/lib/file-upload/file-upload.js.map +1 -1
- package/dist/lib/files-grid/files-grid.d.ts +28 -4
- package/dist/lib/files-grid/files-grid.d.ts.map +1 -1
- package/dist/lib/files-grid/files-grid.js +71 -23
- package/dist/lib/files-grid/files-grid.js.map +1 -1
- package/dist/lib/record-attachments/record-attachments.component.d.ts +15 -7
- package/dist/lib/record-attachments/record-attachments.component.d.ts.map +1 -1
- package/dist/lib/record-attachments/record-attachments.component.js +34 -18
- package/dist/lib/record-attachments/record-attachments.component.js.map +1 -1
- package/package.json +13 -13
|
@@ -11,7 +11,7 @@ import * as i4 from "@memberjunction/ng-ui-components";
|
|
|
11
11
|
const _forTrack0 = ($index, $item) => $item.path;
|
|
12
12
|
const _forTrack1 = ($index, $item) => $item.account.ID;
|
|
13
13
|
const _forTrack2 = ($index, $item) => $item.accountID;
|
|
14
|
-
const _forTrack3 = ($index, $item) => $item.
|
|
14
|
+
const _forTrack3 = ($index, $item) => $item.Key;
|
|
15
15
|
function FileGridComponent_Conditional_1_For_10_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
16
16
|
i0.ɵɵelementStart(0, "span", 43);
|
|
17
17
|
i0.ɵɵtext(1, "/");
|
|
@@ -195,7 +195,7 @@ function FileGridComponent_Conditional_2_Template(rf, ctx) { if (rf & 1) {
|
|
|
195
195
|
i0.ɵɵadvance(2);
|
|
196
196
|
i0.ɵɵtextInterpolate2("", ctx_r1.selectedItems.length, " item", ctx_r1.selectedItems.length === 1 ? "" : "s", " selected");
|
|
197
197
|
i0.ɵɵadvance(2);
|
|
198
|
-
i0.ɵɵconditional(ctx_r1.selectedItems.length === 1 && ((tmp_3_0 = ctx_r1.getSelectedItem()) == null ? null : tmp_3_0.
|
|
198
|
+
i0.ɵɵconditional(ctx_r1.selectedItems.length === 1 && ((tmp_3_0 = ctx_r1.getSelectedItem()) == null ? null : tmp_3_0.Type) === "file" ? 7 : -1);
|
|
199
199
|
i0.ɵɵadvance();
|
|
200
200
|
i0.ɵɵconditional(ctx_r1.selectedItems.length === 1 ? 8 : -1);
|
|
201
201
|
} }
|
|
@@ -341,13 +341,13 @@ function FileGridComponent_Conditional_3_Conditional_22_Template(rf, ctx) { if (
|
|
|
341
341
|
} if (rf & 2) {
|
|
342
342
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
343
343
|
i0.ɵɵadvance(4);
|
|
344
|
-
i0.ɵɵtextInterpolate(ctx_r1.multiProviderSearchResults.
|
|
344
|
+
i0.ɵɵtextInterpolate(ctx_r1.multiProviderSearchResults.TotalResultsReturned);
|
|
345
345
|
i0.ɵɵadvance();
|
|
346
|
-
i0.ɵɵtextInterpolate1(" result", ctx_r1.multiProviderSearchResults.
|
|
346
|
+
i0.ɵɵtextInterpolate1(" result", ctx_r1.multiProviderSearchResults.TotalResultsReturned === 1 ? "" : "s", " found ");
|
|
347
347
|
i0.ɵɵadvance(2);
|
|
348
|
-
i0.ɵɵtextInterpolate2(" (", ctx_r1.multiProviderSearchResults.
|
|
348
|
+
i0.ɵɵtextInterpolate2(" (", ctx_r1.multiProviderSearchResults.SuccessfulAccounts, " account", ctx_r1.multiProviderSearchResults.SuccessfulAccounts === 1 ? "" : "s", " searched) ");
|
|
349
349
|
i0.ɵɵadvance(5);
|
|
350
|
-
i0.ɵɵrepeater(ctx_r1.multiProviderSearchResults.
|
|
350
|
+
i0.ɵɵrepeater(ctx_r1.multiProviderSearchResults.AccountResults);
|
|
351
351
|
} }
|
|
352
352
|
function FileGridComponent_Conditional_3_Template(rf, ctx) { if (rf & 1) {
|
|
353
353
|
const _r11 = i0.ɵɵgetCurrentView();
|
|
@@ -447,12 +447,12 @@ function FileGridComponent_Conditional_6_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
|
447
447
|
} if (rf & 2) {
|
|
448
448
|
const item_r19 = ctx.$implicit;
|
|
449
449
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
450
|
-
i0.ɵɵclassProp("selected", ctx_r1.selectedItems.includes(item_r19.
|
|
450
|
+
i0.ɵɵclassProp("selected", ctx_r1.selectedItems.includes(item_r19.Key));
|
|
451
451
|
i0.ɵɵproperty("title", item_r19.name);
|
|
452
452
|
i0.ɵɵadvance(3);
|
|
453
|
-
i0.ɵɵproperty("checked", ctx_r1.selectedItems.includes(item_r19.
|
|
453
|
+
i0.ɵɵproperty("checked", ctx_r1.selectedItems.includes(item_r19.Key));
|
|
454
454
|
i0.ɵɵadvance(2);
|
|
455
|
-
i0.ɵɵconditional(item_r19.
|
|
455
|
+
i0.ɵɵconditional(item_r19.Type === "file" ? 5 : -1);
|
|
456
456
|
i0.ɵɵadvance();
|
|
457
457
|
i0.ɵɵclassMap(ctx_r1.getItemColorClass(item_r19));
|
|
458
458
|
i0.ɵɵadvance();
|
|
@@ -460,9 +460,9 @@ function FileGridComponent_Conditional_6_For_2_Template(rf, ctx) { if (rf & 1) {
|
|
|
460
460
|
i0.ɵɵadvance(3);
|
|
461
461
|
i0.ɵɵtextInterpolate(item_r19.name);
|
|
462
462
|
i0.ɵɵadvance(3);
|
|
463
|
-
i0.ɵɵtextInterpolate(item_r19.
|
|
463
|
+
i0.ɵɵtextInterpolate(item_r19.Type === "folder" ? "Folder" : ctx_r1.formatFileSize(item_r19.Size));
|
|
464
464
|
i0.ɵɵadvance(2);
|
|
465
|
-
i0.ɵɵtextInterpolate(ctx_r1.formatDate(item_r19.
|
|
465
|
+
i0.ɵɵtextInterpolate(ctx_r1.formatDate(item_r19.LastModified));
|
|
466
466
|
} }
|
|
467
467
|
function FileGridComponent_Conditional_6_Template(rf, ctx) { if (rf & 1) {
|
|
468
468
|
i0.ɵɵelementStart(0, "div", 6);
|
|
@@ -516,9 +516,9 @@ function FileGridComponent_Conditional_7_For_24_Template(rf, ctx) { if (rf & 1)
|
|
|
516
516
|
} if (rf & 2) {
|
|
517
517
|
const item_r23 = ctx.$implicit;
|
|
518
518
|
const ctx_r1 = i0.ɵɵnextContext(2);
|
|
519
|
-
i0.ɵɵclassProp("selected", ctx_r1.selectedItems.includes(item_r23.
|
|
519
|
+
i0.ɵɵclassProp("selected", ctx_r1.selectedItems.includes(item_r23.Key));
|
|
520
520
|
i0.ɵɵadvance(2);
|
|
521
|
-
i0.ɵɵproperty("checked", ctx_r1.selectedItems.includes(item_r23.
|
|
521
|
+
i0.ɵɵproperty("checked", ctx_r1.selectedItems.includes(item_r23.Key));
|
|
522
522
|
i0.ɵɵadvance(3);
|
|
523
523
|
i0.ɵɵclassMap(ctx_r1.getItemColorClass(item_r23));
|
|
524
524
|
i0.ɵɵadvance();
|
|
@@ -530,11 +530,11 @@ function FileGridComponent_Conditional_7_For_24_Template(rf, ctx) { if (rf & 1)
|
|
|
530
530
|
i0.ɵɵadvance(3);
|
|
531
531
|
i0.ɵɵtextInterpolate(ctx_r1.getFileType(item_r23));
|
|
532
532
|
i0.ɵɵadvance(3);
|
|
533
|
-
i0.ɵɵtextInterpolate(item_r23.
|
|
533
|
+
i0.ɵɵtextInterpolate(item_r23.Type === "folder" ? "\u2014" : ctx_r1.formatFileSize(item_r23.Size));
|
|
534
534
|
i0.ɵɵadvance(3);
|
|
535
|
-
i0.ɵɵtextInterpolate(ctx_r1.formatDate(item_r23.
|
|
535
|
+
i0.ɵɵtextInterpolate(ctx_r1.formatDate(item_r23.LastModified));
|
|
536
536
|
i0.ɵɵadvance(3);
|
|
537
|
-
i0.ɵɵconditional(item_r23.
|
|
537
|
+
i0.ɵɵconditional(item_r23.Type === "file" ? 20 : -1);
|
|
538
538
|
} }
|
|
539
539
|
function FileGridComponent_Conditional_7_Template(rf, ctx) { if (rf & 1) {
|
|
540
540
|
const _r21 = i0.ɵɵgetCurrentView();
|
|
@@ -775,7 +775,7 @@ function FileGridComponent_Conditional_12_Template(rf, ctx) { if (rf & 1) {
|
|
|
775
775
|
i0.ɵɵadvance(3);
|
|
776
776
|
i0.ɵɵtextInterpolate(ctx_r1.previewItem.name);
|
|
777
777
|
i0.ɵɵadvance(2);
|
|
778
|
-
i0.ɵɵtextInterpolate2("", ctx_r1.formatFileSize(ctx_r1.previewItem.
|
|
778
|
+
i0.ɵɵtextInterpolate2("", ctx_r1.formatFileSize(ctx_r1.previewItem.Size), " \u2022 ", ctx_r1.formatDate(ctx_r1.previewItem.LastModified));
|
|
779
779
|
i0.ɵɵadvance(9);
|
|
780
780
|
i0.ɵɵconditional(ctx_r1.isLoadingPreview ? 19 : ctx_r1.previewUrl ? 20 : 21);
|
|
781
781
|
} }
|
|
@@ -871,11 +871,11 @@ function FileGridComponent_Conditional_14_Template(rf, ctx) { if (rf & 1) {
|
|
|
871
871
|
} if (rf & 2) {
|
|
872
872
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
873
873
|
i0.ɵɵadvance(4);
|
|
874
|
-
i0.ɵɵtextInterpolate1("Delete ", ctx_r1.itemToDelete.
|
|
874
|
+
i0.ɵɵtextInterpolate1("Delete ", ctx_r1.itemToDelete.Type === "folder" ? "Folder" : "File", "?");
|
|
875
875
|
i0.ɵɵadvance(7);
|
|
876
876
|
i0.ɵɵtextInterpolate(ctx_r1.itemToDelete.name);
|
|
877
877
|
i0.ɵɵadvance(2);
|
|
878
|
-
i0.ɵɵconditional(ctx_r1.itemToDelete.
|
|
878
|
+
i0.ɵɵconditional(ctx_r1.itemToDelete.Type === "folder" ? 13 : -1);
|
|
879
879
|
i0.ɵɵadvance(2);
|
|
880
880
|
i0.ɵɵproperty("disabled", ctx_r1.isDeleting);
|
|
881
881
|
i0.ɵɵadvance(2);
|
|
@@ -921,7 +921,7 @@ function FileGridComponent_Conditional_15_Template(rf, ctx) { if (rf & 1) {
|
|
|
921
921
|
} if (rf & 2) {
|
|
922
922
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
923
923
|
i0.ɵɵadvance(4);
|
|
924
|
-
i0.ɵɵtextInterpolate1("Rename ", ctx_r1.itemToRename.
|
|
924
|
+
i0.ɵɵtextInterpolate1("Rename ", ctx_r1.itemToRename.Type === "folder" ? "Folder" : "File");
|
|
925
925
|
i0.ɵɵadvance(6);
|
|
926
926
|
i0.ɵɵtwoWayProperty("ngModel", ctx_r1.newItemName);
|
|
927
927
|
i0.ɵɵproperty("disabled", ctx_r1.isRenaming);
|
|
@@ -975,7 +975,7 @@ function FileGridComponent_Conditional_16_Template(rf, ctx) { if (rf & 1) {
|
|
|
975
975
|
} if (rf & 2) {
|
|
976
976
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
977
977
|
i0.ɵɵadvance(4);
|
|
978
|
-
i0.ɵɵtextInterpolate1("Copy ", ctx_r1.itemToCopy.
|
|
978
|
+
i0.ɵɵtextInterpolate1("Copy ", ctx_r1.itemToCopy.Type === "folder" ? "Folder" : "File");
|
|
979
979
|
i0.ɵɵadvance(7);
|
|
980
980
|
i0.ɵɵtextInterpolate(ctx_r1.itemToCopy.name);
|
|
981
981
|
i0.ɵɵadvance(3);
|
|
@@ -1031,7 +1031,7 @@ function FileGridComponent_Conditional_17_Template(rf, ctx) { if (rf & 1) {
|
|
|
1031
1031
|
} if (rf & 2) {
|
|
1032
1032
|
const ctx_r1 = i0.ɵɵnextContext();
|
|
1033
1033
|
i0.ɵɵadvance(4);
|
|
1034
|
-
i0.ɵɵtextInterpolate1("Move ", ctx_r1.itemToMove.
|
|
1034
|
+
i0.ɵɵtextInterpolate1("Move ", ctx_r1.itemToMove.Type === "folder" ? "Folder" : "File");
|
|
1035
1035
|
i0.ɵɵadvance(7);
|
|
1036
1036
|
i0.ɵɵtextInterpolate(ctx_r1.itemToMove.name);
|
|
1037
1037
|
i0.ɵɵadvance(3);
|
|
@@ -1160,28 +1160,76 @@ export class FileGridComponent {
|
|
|
1160
1160
|
/**
|
|
1161
1161
|
* The storage account to list files from (includes provider details)
|
|
1162
1162
|
*/
|
|
1163
|
-
|
|
1163
|
+
Account = null;
|
|
1164
|
+
/** @deprecated Use {@link Account}. */
|
|
1165
|
+
set account(value) {
|
|
1166
|
+
this.Account = value;
|
|
1167
|
+
}
|
|
1168
|
+
/** @deprecated Use {@link Account}. */
|
|
1169
|
+
get account() {
|
|
1170
|
+
return this.Account;
|
|
1171
|
+
}
|
|
1164
1172
|
/**
|
|
1165
1173
|
* The current folder path to display
|
|
1166
1174
|
*/
|
|
1167
|
-
|
|
1175
|
+
FolderPath = '';
|
|
1176
|
+
/** @deprecated Use {@link FolderPath}. */
|
|
1177
|
+
set folderPath(value) {
|
|
1178
|
+
this.FolderPath = value;
|
|
1179
|
+
}
|
|
1180
|
+
/** @deprecated Use {@link FolderPath}. */
|
|
1181
|
+
get folderPath() {
|
|
1182
|
+
return this.FolderPath;
|
|
1183
|
+
}
|
|
1168
1184
|
/**
|
|
1169
1185
|
* Emits when a folder is double-clicked for navigation
|
|
1170
1186
|
*/
|
|
1171
|
-
|
|
1187
|
+
FolderNavigate = new EventEmitter();
|
|
1188
|
+
/**
|
|
1189
|
+
* @deprecated Use {@link FolderNavigate}.
|
|
1190
|
+
*
|
|
1191
|
+
* The same emitter under the old binding name, so a template still binding
|
|
1192
|
+
* (folderNavigate) keeps working. Must stay AFTER FolderNavigate: class fields
|
|
1193
|
+
* initialise in order, and the other way round this captures undefined.
|
|
1194
|
+
*/
|
|
1195
|
+
folderNavigate = this.FolderNavigate;
|
|
1172
1196
|
/**
|
|
1173
1197
|
* Emits when the folder structure has changed (e.g., new folder created)
|
|
1174
1198
|
* This signals that the folder tree should refresh
|
|
1175
1199
|
*/
|
|
1176
|
-
|
|
1200
|
+
FolderStructureChanged = new EventEmitter();
|
|
1201
|
+
/**
|
|
1202
|
+
* @deprecated Use {@link FolderStructureChanged}.
|
|
1203
|
+
*
|
|
1204
|
+
* The same emitter under the old binding name, so a template still binding
|
|
1205
|
+
* (folderStructureChanged) keeps working. Must stay AFTER FolderStructureChanged: class fields
|
|
1206
|
+
* initialise in order, and the other way round this captures undefined.
|
|
1207
|
+
*/
|
|
1208
|
+
folderStructureChanged = this.FolderStructureChanged;
|
|
1177
1209
|
/**
|
|
1178
1210
|
* List of files and folders in the current directory
|
|
1179
1211
|
*/
|
|
1180
|
-
|
|
1212
|
+
Items = [];
|
|
1213
|
+
/** @deprecated Use {@link Items}. */
|
|
1214
|
+
get items() {
|
|
1215
|
+
return this.Items;
|
|
1216
|
+
}
|
|
1217
|
+
/** @deprecated Use {@link Items}. */
|
|
1218
|
+
set items(value) {
|
|
1219
|
+
this.Items = value;
|
|
1220
|
+
}
|
|
1181
1221
|
/**
|
|
1182
1222
|
* Currently selected item keys in the grid (Kendo stores keys, not full objects)
|
|
1183
1223
|
*/
|
|
1184
|
-
|
|
1224
|
+
SelectedItems = [];
|
|
1225
|
+
/** @deprecated Use {@link SelectedItems}. */
|
|
1226
|
+
get selectedItems() {
|
|
1227
|
+
return this.SelectedItems;
|
|
1228
|
+
}
|
|
1229
|
+
/** @deprecated Use {@link SelectedItems}. */
|
|
1230
|
+
set selectedItems(value) {
|
|
1231
|
+
this.SelectedItems = value;
|
|
1232
|
+
}
|
|
1185
1233
|
cdr = inject(ChangeDetectorRef);
|
|
1186
1234
|
/**
|
|
1187
1235
|
* Loading state indicator
|
|
@@ -1194,94 +1242,438 @@ export class FileGridComponent {
|
|
|
1194
1242
|
/**
|
|
1195
1243
|
* View mode: 'grid' or 'list'
|
|
1196
1244
|
*/
|
|
1197
|
-
|
|
1245
|
+
ViewMode = 'list';
|
|
1246
|
+
/** @deprecated Use {@link ViewMode}. */
|
|
1247
|
+
get viewMode() {
|
|
1248
|
+
return this.ViewMode;
|
|
1249
|
+
}
|
|
1250
|
+
/** @deprecated Use {@link ViewMode}. */
|
|
1251
|
+
set viewMode(value) {
|
|
1252
|
+
this.ViewMode = value;
|
|
1253
|
+
}
|
|
1198
1254
|
/**
|
|
1199
1255
|
* Sort configuration for the grid
|
|
1200
1256
|
*/
|
|
1201
|
-
|
|
1257
|
+
Sort = [
|
|
1202
1258
|
{ field: 'name', dir: 'asc' }
|
|
1203
1259
|
];
|
|
1260
|
+
/** @deprecated Use {@link Sort}. */
|
|
1261
|
+
get sort() {
|
|
1262
|
+
return this.Sort;
|
|
1263
|
+
}
|
|
1264
|
+
/** @deprecated Use {@link Sort}. */
|
|
1265
|
+
set sort(value) {
|
|
1266
|
+
this.Sort = value;
|
|
1267
|
+
}
|
|
1204
1268
|
/**
|
|
1205
1269
|
* Search query for filtering files/folders
|
|
1206
1270
|
*/
|
|
1207
|
-
|
|
1271
|
+
SearchQuery = '';
|
|
1272
|
+
/** @deprecated Use {@link SearchQuery}. */
|
|
1273
|
+
get searchQuery() {
|
|
1274
|
+
return this.SearchQuery;
|
|
1275
|
+
}
|
|
1276
|
+
/** @deprecated Use {@link SearchQuery}. */
|
|
1277
|
+
set searchQuery(value) {
|
|
1278
|
+
this.SearchQuery = value;
|
|
1279
|
+
}
|
|
1208
1280
|
/**
|
|
1209
1281
|
* File type filter ('all', 'files', 'folders')
|
|
1210
1282
|
*/
|
|
1211
|
-
|
|
1283
|
+
FileTypeFilter = 'all';
|
|
1284
|
+
/** @deprecated Use {@link FileTypeFilter}. */
|
|
1285
|
+
get fileTypeFilter() {
|
|
1286
|
+
return this.FileTypeFilter;
|
|
1287
|
+
}
|
|
1288
|
+
/** @deprecated Use {@link FileTypeFilter}. */
|
|
1289
|
+
set fileTypeFilter(value) {
|
|
1290
|
+
this.FileTypeFilter = value;
|
|
1291
|
+
}
|
|
1212
1292
|
/**
|
|
1213
1293
|
* Filtered list of items based on search and filter criteria
|
|
1214
1294
|
*/
|
|
1215
|
-
|
|
1295
|
+
FilteredItems = [];
|
|
1296
|
+
/** @deprecated Use {@link FilteredItems}. */
|
|
1297
|
+
get filteredItems() {
|
|
1298
|
+
return this.FilteredItems;
|
|
1299
|
+
}
|
|
1300
|
+
/** @deprecated Use {@link FilteredItems}. */
|
|
1301
|
+
set filteredItems(value) {
|
|
1302
|
+
this.FilteredItems = value;
|
|
1303
|
+
}
|
|
1216
1304
|
/**
|
|
1217
1305
|
* Drag-and-drop state
|
|
1218
1306
|
*/
|
|
1219
|
-
|
|
1307
|
+
IsDragging = false;
|
|
1308
|
+
/** @deprecated Use {@link IsDragging}. */
|
|
1309
|
+
get isDragging() {
|
|
1310
|
+
return this.IsDragging;
|
|
1311
|
+
}
|
|
1312
|
+
/** @deprecated Use {@link IsDragging}. */
|
|
1313
|
+
set isDragging(value) {
|
|
1314
|
+
this.IsDragging = value;
|
|
1315
|
+
}
|
|
1220
1316
|
/**
|
|
1221
1317
|
* Upload progress state
|
|
1222
1318
|
*/
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1319
|
+
IsUploading = false;
|
|
1320
|
+
/** @deprecated Use {@link IsUploading}. */
|
|
1321
|
+
get isUploading() {
|
|
1322
|
+
return this.IsUploading;
|
|
1323
|
+
}
|
|
1324
|
+
/** @deprecated Use {@link IsUploading}. */
|
|
1325
|
+
set isUploading(value) {
|
|
1326
|
+
this.IsUploading = value;
|
|
1327
|
+
}
|
|
1328
|
+
UploadProgress = 0;
|
|
1329
|
+
/** @deprecated Use {@link UploadProgress}. */
|
|
1330
|
+
get uploadProgress() {
|
|
1331
|
+
return this.UploadProgress;
|
|
1332
|
+
}
|
|
1333
|
+
/** @deprecated Use {@link UploadProgress}. */
|
|
1334
|
+
set uploadProgress(value) {
|
|
1335
|
+
this.UploadProgress = value;
|
|
1336
|
+
}
|
|
1337
|
+
UploadingFileName = '';
|
|
1338
|
+
/** @deprecated Use {@link UploadingFileName}. */
|
|
1339
|
+
get uploadingFileName() {
|
|
1340
|
+
return this.UploadingFileName;
|
|
1341
|
+
}
|
|
1342
|
+
/** @deprecated Use {@link UploadingFileName}. */
|
|
1343
|
+
set uploadingFileName(value) {
|
|
1344
|
+
this.UploadingFileName = value;
|
|
1345
|
+
}
|
|
1226
1346
|
/**
|
|
1227
1347
|
* New folder dialog state
|
|
1228
1348
|
*/
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1349
|
+
ShowNewFolderDialog = false;
|
|
1350
|
+
/** @deprecated Use {@link ShowNewFolderDialog}. */
|
|
1351
|
+
get showNewFolderDialog() {
|
|
1352
|
+
return this.ShowNewFolderDialog;
|
|
1353
|
+
}
|
|
1354
|
+
/** @deprecated Use {@link ShowNewFolderDialog}. */
|
|
1355
|
+
set showNewFolderDialog(value) {
|
|
1356
|
+
this.ShowNewFolderDialog = value;
|
|
1357
|
+
}
|
|
1358
|
+
NewFolderName = '';
|
|
1359
|
+
/** @deprecated Use {@link NewFolderName}. */
|
|
1360
|
+
get newFolderName() {
|
|
1361
|
+
return this.NewFolderName;
|
|
1362
|
+
}
|
|
1363
|
+
/** @deprecated Use {@link NewFolderName}. */
|
|
1364
|
+
set newFolderName(value) {
|
|
1365
|
+
this.NewFolderName = value;
|
|
1366
|
+
}
|
|
1367
|
+
IsCreatingFolder = false;
|
|
1368
|
+
/** @deprecated Use {@link IsCreatingFolder}. */
|
|
1369
|
+
get isCreatingFolder() {
|
|
1370
|
+
return this.IsCreatingFolder;
|
|
1371
|
+
}
|
|
1372
|
+
/** @deprecated Use {@link IsCreatingFolder}. */
|
|
1373
|
+
set isCreatingFolder(value) {
|
|
1374
|
+
this.IsCreatingFolder = value;
|
|
1375
|
+
}
|
|
1232
1376
|
/**
|
|
1233
1377
|
* Delete confirmation dialog state
|
|
1234
1378
|
*/
|
|
1235
1379
|
showDeleteDialog = false;
|
|
1236
|
-
|
|
1237
|
-
|
|
1380
|
+
ItemToDelete = null;
|
|
1381
|
+
/** @deprecated Use {@link ItemToDelete}. */
|
|
1382
|
+
get itemToDelete() {
|
|
1383
|
+
return this.ItemToDelete;
|
|
1384
|
+
}
|
|
1385
|
+
/** @deprecated Use {@link ItemToDelete}. */
|
|
1386
|
+
set itemToDelete(value) {
|
|
1387
|
+
this.ItemToDelete = value;
|
|
1388
|
+
}
|
|
1389
|
+
IsDeleting = false;
|
|
1390
|
+
/** @deprecated Use {@link IsDeleting}. */
|
|
1391
|
+
get isDeleting() {
|
|
1392
|
+
return this.IsDeleting;
|
|
1393
|
+
}
|
|
1394
|
+
/** @deprecated Use {@link IsDeleting}. */
|
|
1395
|
+
set isDeleting(value) {
|
|
1396
|
+
this.IsDeleting = value;
|
|
1397
|
+
}
|
|
1238
1398
|
/**
|
|
1239
1399
|
* Rename dialog state
|
|
1240
1400
|
*/
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1401
|
+
ShowRenameDialog = false;
|
|
1402
|
+
/** @deprecated Use {@link ShowRenameDialog}. */
|
|
1403
|
+
get showRenameDialog() {
|
|
1404
|
+
return this.ShowRenameDialog;
|
|
1405
|
+
}
|
|
1406
|
+
/** @deprecated Use {@link ShowRenameDialog}. */
|
|
1407
|
+
set showRenameDialog(value) {
|
|
1408
|
+
this.ShowRenameDialog = value;
|
|
1409
|
+
}
|
|
1410
|
+
ItemToRename = null;
|
|
1411
|
+
/** @deprecated Use {@link ItemToRename}. */
|
|
1412
|
+
get itemToRename() {
|
|
1413
|
+
return this.ItemToRename;
|
|
1414
|
+
}
|
|
1415
|
+
/** @deprecated Use {@link ItemToRename}. */
|
|
1416
|
+
set itemToRename(value) {
|
|
1417
|
+
this.ItemToRename = value;
|
|
1418
|
+
}
|
|
1419
|
+
NewItemName = '';
|
|
1420
|
+
/** @deprecated Use {@link NewItemName}. */
|
|
1421
|
+
get newItemName() {
|
|
1422
|
+
return this.NewItemName;
|
|
1423
|
+
}
|
|
1424
|
+
/** @deprecated Use {@link NewItemName}. */
|
|
1425
|
+
set newItemName(value) {
|
|
1426
|
+
this.NewItemName = value;
|
|
1427
|
+
}
|
|
1428
|
+
IsRenaming = false;
|
|
1429
|
+
/** @deprecated Use {@link IsRenaming}. */
|
|
1430
|
+
get isRenaming() {
|
|
1431
|
+
return this.IsRenaming;
|
|
1432
|
+
}
|
|
1433
|
+
/** @deprecated Use {@link IsRenaming}. */
|
|
1434
|
+
set isRenaming(value) {
|
|
1435
|
+
this.IsRenaming = value;
|
|
1436
|
+
}
|
|
1245
1437
|
/**
|
|
1246
1438
|
* Copy dialog state
|
|
1247
1439
|
*/
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1440
|
+
ShowCopyDialog = false;
|
|
1441
|
+
/** @deprecated Use {@link ShowCopyDialog}. */
|
|
1442
|
+
get showCopyDialog() {
|
|
1443
|
+
return this.ShowCopyDialog;
|
|
1444
|
+
}
|
|
1445
|
+
/** @deprecated Use {@link ShowCopyDialog}. */
|
|
1446
|
+
set showCopyDialog(value) {
|
|
1447
|
+
this.ShowCopyDialog = value;
|
|
1448
|
+
}
|
|
1449
|
+
ItemToCopy = null;
|
|
1450
|
+
/** @deprecated Use {@link ItemToCopy}. */
|
|
1451
|
+
get itemToCopy() {
|
|
1452
|
+
return this.ItemToCopy;
|
|
1453
|
+
}
|
|
1454
|
+
/** @deprecated Use {@link ItemToCopy}. */
|
|
1455
|
+
set itemToCopy(value) {
|
|
1456
|
+
this.ItemToCopy = value;
|
|
1457
|
+
}
|
|
1458
|
+
CopyDestinationPath = '';
|
|
1459
|
+
/** @deprecated Use {@link CopyDestinationPath}. */
|
|
1460
|
+
get copyDestinationPath() {
|
|
1461
|
+
return this.CopyDestinationPath;
|
|
1462
|
+
}
|
|
1463
|
+
/** @deprecated Use {@link CopyDestinationPath}. */
|
|
1464
|
+
set copyDestinationPath(value) {
|
|
1465
|
+
this.CopyDestinationPath = value;
|
|
1466
|
+
}
|
|
1467
|
+
IsCopying = false;
|
|
1468
|
+
/** @deprecated Use {@link IsCopying}. */
|
|
1469
|
+
get isCopying() {
|
|
1470
|
+
return this.IsCopying;
|
|
1471
|
+
}
|
|
1472
|
+
/** @deprecated Use {@link IsCopying}. */
|
|
1473
|
+
set isCopying(value) {
|
|
1474
|
+
this.IsCopying = value;
|
|
1475
|
+
}
|
|
1252
1476
|
/**
|
|
1253
1477
|
* Move dialog state
|
|
1254
1478
|
*/
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1479
|
+
ShowMoveDialog = false;
|
|
1480
|
+
/** @deprecated Use {@link ShowMoveDialog}. */
|
|
1481
|
+
get showMoveDialog() {
|
|
1482
|
+
return this.ShowMoveDialog;
|
|
1483
|
+
}
|
|
1484
|
+
/** @deprecated Use {@link ShowMoveDialog}. */
|
|
1485
|
+
set showMoveDialog(value) {
|
|
1486
|
+
this.ShowMoveDialog = value;
|
|
1487
|
+
}
|
|
1488
|
+
ItemToMove = null;
|
|
1489
|
+
/** @deprecated Use {@link ItemToMove}. */
|
|
1490
|
+
get itemToMove() {
|
|
1491
|
+
return this.ItemToMove;
|
|
1492
|
+
}
|
|
1493
|
+
/** @deprecated Use {@link ItemToMove}. */
|
|
1494
|
+
set itemToMove(value) {
|
|
1495
|
+
this.ItemToMove = value;
|
|
1496
|
+
}
|
|
1497
|
+
MoveDestinationPath = '';
|
|
1498
|
+
/** @deprecated Use {@link MoveDestinationPath}. */
|
|
1499
|
+
get moveDestinationPath() {
|
|
1500
|
+
return this.MoveDestinationPath;
|
|
1501
|
+
}
|
|
1502
|
+
/** @deprecated Use {@link MoveDestinationPath}. */
|
|
1503
|
+
set moveDestinationPath(value) {
|
|
1504
|
+
this.MoveDestinationPath = value;
|
|
1505
|
+
}
|
|
1506
|
+
IsMoving = false;
|
|
1507
|
+
/** @deprecated Use {@link IsMoving}. */
|
|
1508
|
+
get isMoving() {
|
|
1509
|
+
return this.IsMoving;
|
|
1510
|
+
}
|
|
1511
|
+
/** @deprecated Use {@link IsMoving}. */
|
|
1512
|
+
set isMoving(value) {
|
|
1513
|
+
this.IsMoving = value;
|
|
1514
|
+
}
|
|
1259
1515
|
/**
|
|
1260
1516
|
* Copy to provider dialog state
|
|
1261
1517
|
*/
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1518
|
+
ShowCopyToProviderDialog = false;
|
|
1519
|
+
/** @deprecated Use {@link ShowCopyToProviderDialog}. */
|
|
1520
|
+
get showCopyToProviderDialog() {
|
|
1521
|
+
return this.ShowCopyToProviderDialog;
|
|
1522
|
+
}
|
|
1523
|
+
/** @deprecated Use {@link ShowCopyToProviderDialog}. */
|
|
1524
|
+
set showCopyToProviderDialog(value) {
|
|
1525
|
+
this.ShowCopyToProviderDialog = value;
|
|
1526
|
+
}
|
|
1527
|
+
ItemToCopyToProvider = null;
|
|
1528
|
+
/** @deprecated Use {@link ItemToCopyToProvider}. */
|
|
1529
|
+
get itemToCopyToProvider() {
|
|
1530
|
+
return this.ItemToCopyToProvider;
|
|
1531
|
+
}
|
|
1532
|
+
/** @deprecated Use {@link ItemToCopyToProvider}. */
|
|
1533
|
+
set itemToCopyToProvider(value) {
|
|
1534
|
+
this.ItemToCopyToProvider = value;
|
|
1535
|
+
}
|
|
1536
|
+
AvailableAccounts = [];
|
|
1537
|
+
/** @deprecated Use {@link AvailableAccounts}. */
|
|
1538
|
+
get availableAccounts() {
|
|
1539
|
+
return this.AvailableAccounts;
|
|
1540
|
+
}
|
|
1541
|
+
/** @deprecated Use {@link AvailableAccounts}. */
|
|
1542
|
+
set availableAccounts(value) {
|
|
1543
|
+
this.AvailableAccounts = value;
|
|
1544
|
+
}
|
|
1545
|
+
SelectedDestinationAccounts = new Set();
|
|
1546
|
+
/** @deprecated Use {@link SelectedDestinationAccounts}. */
|
|
1547
|
+
get selectedDestinationAccounts() {
|
|
1548
|
+
return this.SelectedDestinationAccounts;
|
|
1549
|
+
}
|
|
1550
|
+
/** @deprecated Use {@link SelectedDestinationAccounts}. */
|
|
1551
|
+
set selectedDestinationAccounts(value) {
|
|
1552
|
+
this.SelectedDestinationAccounts = value;
|
|
1553
|
+
}
|
|
1554
|
+
CopyToAccountDestinationPath = '';
|
|
1555
|
+
/** @deprecated Use {@link CopyToAccountDestinationPath}. */
|
|
1556
|
+
get copyToAccountDestinationPath() {
|
|
1557
|
+
return this.CopyToAccountDestinationPath;
|
|
1558
|
+
}
|
|
1559
|
+
/** @deprecated Use {@link CopyToAccountDestinationPath}. */
|
|
1560
|
+
set copyToAccountDestinationPath(value) {
|
|
1561
|
+
this.CopyToAccountDestinationPath = value;
|
|
1562
|
+
}
|
|
1563
|
+
IsCopyingToAccount = false;
|
|
1564
|
+
/** @deprecated Use {@link IsCopyingToAccount}. */
|
|
1565
|
+
get isCopyingToAccount() {
|
|
1566
|
+
return this.IsCopyingToAccount;
|
|
1567
|
+
}
|
|
1568
|
+
/** @deprecated Use {@link IsCopyingToAccount}. */
|
|
1569
|
+
set isCopyingToAccount(value) {
|
|
1570
|
+
this.IsCopyingToAccount = value;
|
|
1571
|
+
}
|
|
1572
|
+
CopyToAccountProgress = null;
|
|
1573
|
+
/** @deprecated Use {@link CopyToAccountProgress}. */
|
|
1574
|
+
get copyToAccountProgress() {
|
|
1575
|
+
return this.CopyToAccountProgress;
|
|
1576
|
+
}
|
|
1577
|
+
/** @deprecated Use {@link CopyToAccountProgress}. */
|
|
1578
|
+
set copyToAccountProgress(value) {
|
|
1579
|
+
this.CopyToAccountProgress = value;
|
|
1580
|
+
}
|
|
1269
1581
|
/**
|
|
1270
1582
|
* Multi-provider search state
|
|
1271
1583
|
*/
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1584
|
+
IsMultiProviderSearchMode = false;
|
|
1585
|
+
/** @deprecated Use {@link IsMultiProviderSearchMode}. */
|
|
1586
|
+
get isMultiProviderSearchMode() {
|
|
1587
|
+
return this.IsMultiProviderSearchMode;
|
|
1588
|
+
}
|
|
1589
|
+
/** @deprecated Use {@link IsMultiProviderSearchMode}. */
|
|
1590
|
+
set isMultiProviderSearchMode(value) {
|
|
1591
|
+
this.IsMultiProviderSearchMode = value;
|
|
1592
|
+
}
|
|
1593
|
+
MultiProviderSearchQuery = '';
|
|
1594
|
+
/** @deprecated Use {@link MultiProviderSearchQuery}. */
|
|
1595
|
+
get multiProviderSearchQuery() {
|
|
1596
|
+
return this.MultiProviderSearchQuery;
|
|
1597
|
+
}
|
|
1598
|
+
/** @deprecated Use {@link MultiProviderSearchQuery}. */
|
|
1599
|
+
set multiProviderSearchQuery(value) {
|
|
1600
|
+
this.MultiProviderSearchQuery = value;
|
|
1601
|
+
}
|
|
1602
|
+
SelectedSearchProviders = new Set();
|
|
1603
|
+
/** @deprecated Use {@link SelectedSearchProviders}. */
|
|
1604
|
+
get selectedSearchProviders() {
|
|
1605
|
+
return this.SelectedSearchProviders;
|
|
1606
|
+
}
|
|
1607
|
+
/** @deprecated Use {@link SelectedSearchProviders}. */
|
|
1608
|
+
set selectedSearchProviders(value) {
|
|
1609
|
+
this.SelectedSearchProviders = value;
|
|
1610
|
+
}
|
|
1611
|
+
IsSearching = false;
|
|
1612
|
+
/** @deprecated Use {@link IsSearching}. */
|
|
1613
|
+
get isSearching() {
|
|
1614
|
+
return this.IsSearching;
|
|
1615
|
+
}
|
|
1616
|
+
/** @deprecated Use {@link IsSearching}. */
|
|
1617
|
+
set isSearching(value) {
|
|
1618
|
+
this.IsSearching = value;
|
|
1619
|
+
}
|
|
1620
|
+
MultiProviderSearchResults = null;
|
|
1621
|
+
/** @deprecated Use {@link MultiProviderSearchResults}. */
|
|
1622
|
+
get multiProviderSearchResults() {
|
|
1623
|
+
return this.MultiProviderSearchResults;
|
|
1624
|
+
}
|
|
1625
|
+
/** @deprecated Use {@link MultiProviderSearchResults}. */
|
|
1626
|
+
set multiProviderSearchResults(value) {
|
|
1627
|
+
this.MultiProviderSearchResults = value;
|
|
1628
|
+
}
|
|
1277
1629
|
/**
|
|
1278
1630
|
* Preview modal state
|
|
1279
1631
|
*/
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1632
|
+
ShowPreviewModal = false;
|
|
1633
|
+
/** @deprecated Use {@link ShowPreviewModal}. */
|
|
1634
|
+
get showPreviewModal() {
|
|
1635
|
+
return this.ShowPreviewModal;
|
|
1636
|
+
}
|
|
1637
|
+
/** @deprecated Use {@link ShowPreviewModal}. */
|
|
1638
|
+
set showPreviewModal(value) {
|
|
1639
|
+
this.ShowPreviewModal = value;
|
|
1640
|
+
}
|
|
1641
|
+
PreviewItem = null;
|
|
1642
|
+
/** @deprecated Use {@link PreviewItem}. */
|
|
1643
|
+
get previewItem() {
|
|
1644
|
+
return this.PreviewItem;
|
|
1645
|
+
}
|
|
1646
|
+
/** @deprecated Use {@link PreviewItem}. */
|
|
1647
|
+
set previewItem(value) {
|
|
1648
|
+
this.PreviewItem = value;
|
|
1649
|
+
}
|
|
1650
|
+
PreviewUrl = null;
|
|
1651
|
+
/** @deprecated Use {@link PreviewUrl}. */
|
|
1652
|
+
get previewUrl() {
|
|
1653
|
+
return this.PreviewUrl;
|
|
1654
|
+
}
|
|
1655
|
+
/** @deprecated Use {@link PreviewUrl}. */
|
|
1656
|
+
set previewUrl(value) {
|
|
1657
|
+
this.PreviewUrl = value;
|
|
1658
|
+
}
|
|
1659
|
+
IsLoadingPreview = false;
|
|
1660
|
+
/** @deprecated Use {@link IsLoadingPreview}. */
|
|
1661
|
+
get isLoadingPreview() {
|
|
1662
|
+
return this.IsLoadingPreview;
|
|
1663
|
+
}
|
|
1664
|
+
/** @deprecated Use {@link IsLoadingPreview}. */
|
|
1665
|
+
set isLoadingPreview(value) {
|
|
1666
|
+
this.IsLoadingPreview = value;
|
|
1667
|
+
}
|
|
1668
|
+
PreviewMediaType = 'unknown';
|
|
1669
|
+
/** @deprecated Use {@link PreviewMediaType}. */
|
|
1670
|
+
get previewMediaType() {
|
|
1671
|
+
return this.PreviewMediaType;
|
|
1672
|
+
}
|
|
1673
|
+
/** @deprecated Use {@link PreviewMediaType}. */
|
|
1674
|
+
set previewMediaType(value) {
|
|
1675
|
+
this.PreviewMediaType = value;
|
|
1676
|
+
}
|
|
1285
1677
|
/**
|
|
1286
1678
|
* GraphQL client for file storage operations
|
|
1287
1679
|
*/
|
|
@@ -1306,21 +1698,21 @@ export class FileGridComponent {
|
|
|
1306
1698
|
* Loads files and folders from the current path
|
|
1307
1699
|
*/
|
|
1308
1700
|
async loadItems() {
|
|
1309
|
-
if (!this.
|
|
1310
|
-
this.
|
|
1701
|
+
if (!this.Account) {
|
|
1702
|
+
this.Items = [];
|
|
1311
1703
|
return;
|
|
1312
1704
|
}
|
|
1313
|
-
const previousItemCount = this.
|
|
1314
|
-
const previousItemNames = this.
|
|
1705
|
+
const previousItemCount = this.Items.length;
|
|
1706
|
+
const previousItemNames = this.Items.map(i => i.name);
|
|
1315
1707
|
this.isLoading = true;
|
|
1316
1708
|
this.cdr.detectChanges();
|
|
1317
1709
|
this.errorMessage = null;
|
|
1318
1710
|
try {
|
|
1319
|
-
console.log('[FileGrid] Loading items for account:', this.
|
|
1711
|
+
console.log('[FileGrid] Loading items for account:', this.Account.account.ID, 'path:', this.FolderPath);
|
|
1320
1712
|
console.log('[FileGrid] Previous items:', { count: previousItemCount, names: previousItemNames });
|
|
1321
|
-
const listResult = await this.storageClient.ListObjects(this.
|
|
1713
|
+
const listResult = await this.storageClient.ListObjects(this.Account.account.ID, this.FolderPath || '', '/');
|
|
1322
1714
|
console.log('[FileGrid] ListObjects result:', listResult);
|
|
1323
|
-
this.
|
|
1715
|
+
this.Items = [];
|
|
1324
1716
|
// Add folders from prefixes first (so they appear before files)
|
|
1325
1717
|
if (listResult.prefixes) {
|
|
1326
1718
|
for (const prefix of listResult.prefixes) {
|
|
@@ -1330,13 +1722,13 @@ export class FileGridComponent {
|
|
|
1330
1722
|
? prefix.slice(0, -1).split('/').pop()
|
|
1331
1723
|
: prefix.split('/').pop();
|
|
1332
1724
|
if (folderName) {
|
|
1333
|
-
this.
|
|
1334
|
-
|
|
1725
|
+
this.Items.push({
|
|
1726
|
+
Key: prefix,
|
|
1335
1727
|
name: folderName,
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1728
|
+
Type: 'folder',
|
|
1729
|
+
Size: 0,
|
|
1730
|
+
LastModified: new Date(),
|
|
1731
|
+
ContentType: 'application/x-directory'
|
|
1340
1732
|
});
|
|
1341
1733
|
}
|
|
1342
1734
|
}
|
|
@@ -1348,14 +1740,14 @@ export class FileGridComponent {
|
|
|
1348
1740
|
if (obj.isDirectory) {
|
|
1349
1741
|
continue;
|
|
1350
1742
|
}
|
|
1351
|
-
this.
|
|
1352
|
-
|
|
1743
|
+
this.Items.push({
|
|
1744
|
+
Key: obj.fullPath,
|
|
1353
1745
|
name: obj.name,
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1746
|
+
Type: 'file',
|
|
1747
|
+
Size: obj.size,
|
|
1748
|
+
LastModified: obj.lastModified,
|
|
1749
|
+
ContentType: obj.contentType,
|
|
1750
|
+
Etag: obj.etag
|
|
1359
1751
|
});
|
|
1360
1752
|
}
|
|
1361
1753
|
}
|
|
@@ -1375,46 +1767,58 @@ export class FileGridComponent {
|
|
|
1375
1767
|
* Applies search and filter criteria to the items list
|
|
1376
1768
|
*/
|
|
1377
1769
|
applyFilters() {
|
|
1378
|
-
let filtered = [...this.
|
|
1770
|
+
let filtered = [...this.Items];
|
|
1379
1771
|
// Apply file type filter
|
|
1380
|
-
if (this.
|
|
1381
|
-
filtered = filtered.filter(item => item.
|
|
1772
|
+
if (this.FileTypeFilter === 'files') {
|
|
1773
|
+
filtered = filtered.filter(item => item.Type === 'file');
|
|
1382
1774
|
}
|
|
1383
|
-
else if (this.
|
|
1384
|
-
filtered = filtered.filter(item => item.
|
|
1775
|
+
else if (this.FileTypeFilter === 'folders') {
|
|
1776
|
+
filtered = filtered.filter(item => item.Type === 'folder');
|
|
1385
1777
|
}
|
|
1386
1778
|
// Apply search query
|
|
1387
|
-
if (this.
|
|
1388
|
-
const query = this.
|
|
1779
|
+
if (this.SearchQuery.trim()) {
|
|
1780
|
+
const query = this.SearchQuery.toLowerCase();
|
|
1389
1781
|
filtered = filtered.filter(item => item.name.toLowerCase().includes(query));
|
|
1390
1782
|
}
|
|
1391
|
-
this.
|
|
1783
|
+
this.FilteredItems = filtered;
|
|
1392
1784
|
}
|
|
1393
1785
|
/**
|
|
1394
1786
|
* Handles search query changes
|
|
1395
1787
|
*/
|
|
1396
|
-
|
|
1397
|
-
this.
|
|
1788
|
+
OnSearchChange(query) {
|
|
1789
|
+
this.SearchQuery = query;
|
|
1398
1790
|
this.applyFilters();
|
|
1399
1791
|
}
|
|
1792
|
+
/** @deprecated Use {@link OnSearchChange}. */
|
|
1793
|
+
onSearchChange(query) {
|
|
1794
|
+
return this.OnSearchChange(query);
|
|
1795
|
+
}
|
|
1400
1796
|
/**
|
|
1401
1797
|
* Clears the search query
|
|
1402
1798
|
*/
|
|
1403
|
-
|
|
1404
|
-
this.
|
|
1799
|
+
ClearSearch() {
|
|
1800
|
+
this.SearchQuery = '';
|
|
1405
1801
|
this.applyFilters();
|
|
1406
1802
|
}
|
|
1803
|
+
/** @deprecated Use {@link ClearSearch}. */
|
|
1804
|
+
clearSearch() {
|
|
1805
|
+
return this.ClearSearch();
|
|
1806
|
+
}
|
|
1407
1807
|
/**
|
|
1408
1808
|
* Handles file type filter changes
|
|
1409
1809
|
*/
|
|
1410
|
-
|
|
1411
|
-
this.
|
|
1810
|
+
OnFileTypeFilterChange(filterType) {
|
|
1811
|
+
this.FileTypeFilter = filterType;
|
|
1412
1812
|
this.applyFilters();
|
|
1413
1813
|
}
|
|
1814
|
+
/** @deprecated Use {@link OnFileTypeFilterChange}. */
|
|
1815
|
+
onFileTypeFilterChange(filterType) {
|
|
1816
|
+
return this.OnFileTypeFilterChange(filterType);
|
|
1817
|
+
}
|
|
1414
1818
|
/**
|
|
1415
1819
|
* Formats a file size in bytes to a human-readable string
|
|
1416
1820
|
*/
|
|
1417
|
-
|
|
1821
|
+
FormatFileSize(bytes) {
|
|
1418
1822
|
if (bytes === 0)
|
|
1419
1823
|
return '0 B';
|
|
1420
1824
|
const k = 1024;
|
|
@@ -1422,6 +1826,10 @@ export class FileGridComponent {
|
|
|
1422
1826
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1423
1827
|
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i];
|
|
1424
1828
|
}
|
|
1829
|
+
/** @deprecated Use {@link FormatFileSize}. */
|
|
1830
|
+
formatFileSize(bytes) {
|
|
1831
|
+
return this.FormatFileSize(bytes);
|
|
1832
|
+
}
|
|
1425
1833
|
/**
|
|
1426
1834
|
* Formats a date to a readable string
|
|
1427
1835
|
*/
|
|
@@ -1431,11 +1839,11 @@ export class FileGridComponent {
|
|
|
1431
1839
|
/**
|
|
1432
1840
|
* Gets the icon class for a file or folder
|
|
1433
1841
|
*/
|
|
1434
|
-
|
|
1842
|
+
GetItemIcon(item) {
|
|
1435
1843
|
if (!item || !item.name) {
|
|
1436
1844
|
return 'fa-solid fa-file';
|
|
1437
1845
|
}
|
|
1438
|
-
if (item.
|
|
1846
|
+
if (item.Type === 'folder') {
|
|
1439
1847
|
return 'fa-solid fa-folder';
|
|
1440
1848
|
}
|
|
1441
1849
|
// Determine file icon based on content type or extension
|
|
@@ -1474,68 +1882,84 @@ export class FileGridComponent {
|
|
|
1474
1882
|
return 'fa-solid fa-file';
|
|
1475
1883
|
}
|
|
1476
1884
|
}
|
|
1885
|
+
/** @deprecated Use {@link GetItemIcon}. */
|
|
1886
|
+
getItemIcon(item) {
|
|
1887
|
+
return this.GetItemIcon(item);
|
|
1888
|
+
}
|
|
1477
1889
|
/**
|
|
1478
1890
|
* Handles item selection change (not used - Kendo handles selection internally)
|
|
1479
1891
|
*/
|
|
1892
|
+
OnSelectionChange(selectedKeys) {
|
|
1893
|
+
this.SelectedItems = selectedKeys;
|
|
1894
|
+
}
|
|
1895
|
+
/** @deprecated Use {@link OnSelectionChange}. */
|
|
1480
1896
|
onSelectionChange(selectedKeys) {
|
|
1481
|
-
this.
|
|
1897
|
+
return this.OnSelectionChange(selectedKeys);
|
|
1482
1898
|
}
|
|
1483
1899
|
/**
|
|
1484
1900
|
* Handles tile click in grid view for selection
|
|
1485
1901
|
*/
|
|
1486
|
-
|
|
1902
|
+
OnTileClick(item, event) {
|
|
1487
1903
|
if (event.ctrlKey || event.metaKey) {
|
|
1488
1904
|
// Multi-select: toggle item key
|
|
1489
|
-
const index = this.
|
|
1905
|
+
const index = this.SelectedItems.indexOf(item.Key);
|
|
1490
1906
|
if (index >= 0) {
|
|
1491
|
-
this.
|
|
1907
|
+
this.SelectedItems.splice(index, 1);
|
|
1492
1908
|
}
|
|
1493
1909
|
else {
|
|
1494
|
-
this.
|
|
1910
|
+
this.SelectedItems.push(item.Key);
|
|
1495
1911
|
}
|
|
1496
1912
|
}
|
|
1497
|
-
else if (event.shiftKey && this.
|
|
1913
|
+
else if (event.shiftKey && this.SelectedItems.length > 0) {
|
|
1498
1914
|
// Range select: select from last selected to current
|
|
1499
|
-
const lastSelectedKey = this.
|
|
1500
|
-
const lastSelected = this.
|
|
1915
|
+
const lastSelectedKey = this.SelectedItems[this.SelectedItems.length - 1];
|
|
1916
|
+
const lastSelected = this.Items.find(i => i.Key === lastSelectedKey);
|
|
1501
1917
|
if (lastSelected) {
|
|
1502
|
-
const lastIndex = this.
|
|
1503
|
-
const currentIndex = this.
|
|
1918
|
+
const lastIndex = this.Items.indexOf(lastSelected);
|
|
1919
|
+
const currentIndex = this.Items.indexOf(item);
|
|
1504
1920
|
const start = Math.min(lastIndex, currentIndex);
|
|
1505
1921
|
const end = Math.max(lastIndex, currentIndex);
|
|
1506
|
-
this.
|
|
1922
|
+
this.SelectedItems = this.Items.slice(start, end + 1).map(i => i.Key);
|
|
1507
1923
|
}
|
|
1508
1924
|
}
|
|
1509
1925
|
else {
|
|
1510
1926
|
// Single select: replace selection
|
|
1511
|
-
this.
|
|
1927
|
+
this.SelectedItems = [item.Key];
|
|
1512
1928
|
}
|
|
1513
1929
|
}
|
|
1930
|
+
/** @deprecated Use {@link OnTileClick}. */
|
|
1931
|
+
onTileClick(item, event) {
|
|
1932
|
+
return this.OnTileClick(item, event);
|
|
1933
|
+
}
|
|
1514
1934
|
/**
|
|
1515
1935
|
* Handles double-click on an item
|
|
1516
1936
|
* For folders, navigate into them. For files, open in-browser preview.
|
|
1517
1937
|
*/
|
|
1518
|
-
|
|
1519
|
-
if (item.
|
|
1938
|
+
OnItemDoubleClick(item) {
|
|
1939
|
+
if (item.Type === 'folder') {
|
|
1520
1940
|
// Navigate into folder by emitting the folder path
|
|
1521
|
-
console.log('[FileGrid] Navigating to folder:', item.
|
|
1522
|
-
this.
|
|
1941
|
+
console.log('[FileGrid] Navigating to folder:', item.Key);
|
|
1942
|
+
this.FolderNavigate.emit(item.Key);
|
|
1523
1943
|
}
|
|
1524
1944
|
else {
|
|
1525
1945
|
// Open in-browser preview
|
|
1526
|
-
this.
|
|
1946
|
+
this.OpenPreview(item);
|
|
1527
1947
|
}
|
|
1528
1948
|
}
|
|
1949
|
+
/** @deprecated Use {@link OnItemDoubleClick}. */
|
|
1950
|
+
onItemDoubleClick(item) {
|
|
1951
|
+
return this.OnItemDoubleClick(item);
|
|
1952
|
+
}
|
|
1529
1953
|
/**
|
|
1530
1954
|
* Downloads a file by creating a pre-authenticated download URL
|
|
1531
1955
|
*/
|
|
1532
|
-
async
|
|
1533
|
-
if (!this.
|
|
1956
|
+
async DownloadFile(item) {
|
|
1957
|
+
if (!this.Account) {
|
|
1534
1958
|
return;
|
|
1535
1959
|
}
|
|
1536
1960
|
try {
|
|
1537
|
-
console.log('[FileGrid] Downloading file:', item.
|
|
1538
|
-
const downloadUrl = await this.storageClient.CreatePreAuthDownloadUrl(this.
|
|
1961
|
+
console.log('[FileGrid] Downloading file:', item.Key);
|
|
1962
|
+
const downloadUrl = await this.storageClient.CreatePreAuthDownloadUrl(this.Account.account.ID, item.Key);
|
|
1539
1963
|
console.log('[FileGrid] Download URL created:', downloadUrl ? 'success' : 'failed');
|
|
1540
1964
|
if (downloadUrl) {
|
|
1541
1965
|
// Open file in new browser tab for viewing/download
|
|
@@ -1552,54 +1976,78 @@ export class FileGridComponent {
|
|
|
1552
1976
|
this.notifications.CreateSimpleNotification(`Failed to download ${item.name}: ${errorMessage}`, 'error');
|
|
1553
1977
|
}
|
|
1554
1978
|
}
|
|
1979
|
+
/** @deprecated Use {@link DownloadFile}. */
|
|
1980
|
+
async downloadFile(item) {
|
|
1981
|
+
return this.DownloadFile(item);
|
|
1982
|
+
}
|
|
1555
1983
|
/**
|
|
1556
1984
|
* Toggles between grid and list view
|
|
1557
1985
|
*/
|
|
1986
|
+
ToggleViewMode() {
|
|
1987
|
+
this.ViewMode = this.ViewMode === 'grid' ? 'list' : 'grid';
|
|
1988
|
+
}
|
|
1989
|
+
/** @deprecated Use {@link ToggleViewMode}. */
|
|
1558
1990
|
toggleViewMode() {
|
|
1559
|
-
|
|
1991
|
+
return this.ToggleViewMode();
|
|
1560
1992
|
}
|
|
1561
1993
|
/**
|
|
1562
1994
|
* Refreshes the current directory
|
|
1563
1995
|
*/
|
|
1564
|
-
|
|
1996
|
+
Refresh() {
|
|
1565
1997
|
this.loadItems();
|
|
1566
1998
|
}
|
|
1999
|
+
/** @deprecated Use {@link Refresh}. */
|
|
2000
|
+
refresh() {
|
|
2001
|
+
return this.Refresh();
|
|
2002
|
+
}
|
|
1567
2003
|
/**
|
|
1568
2004
|
* Navigates up to the parent directory
|
|
1569
2005
|
*/
|
|
1570
|
-
|
|
1571
|
-
if (!this.
|
|
2006
|
+
NavigateUp() {
|
|
2007
|
+
if (!this.FolderPath) {
|
|
1572
2008
|
// Already at root
|
|
1573
2009
|
return;
|
|
1574
2010
|
}
|
|
1575
2011
|
// Remove trailing slash if present
|
|
1576
|
-
const cleanPath = this.
|
|
1577
|
-
? this.
|
|
1578
|
-
: this.
|
|
2012
|
+
const cleanPath = this.FolderPath.endsWith('/')
|
|
2013
|
+
? this.FolderPath.slice(0, -1)
|
|
2014
|
+
: this.FolderPath;
|
|
1579
2015
|
// Get parent path by removing last segment
|
|
1580
2016
|
const segments = cleanPath.split('/').filter(s => s.length > 0);
|
|
1581
2017
|
segments.pop(); // Remove last segment
|
|
1582
2018
|
const parentPath = segments.length > 0 ? segments.join('/') + '/' : '';
|
|
1583
2019
|
// Emit navigation event to update the folder tree and path
|
|
1584
|
-
this.
|
|
2020
|
+
this.FolderNavigate.emit(parentPath);
|
|
2021
|
+
}
|
|
2022
|
+
/** @deprecated Use {@link NavigateUp}. */
|
|
2023
|
+
navigateUp() {
|
|
2024
|
+
return this.NavigateUp();
|
|
1585
2025
|
}
|
|
1586
2026
|
/**
|
|
1587
2027
|
* Checks if we can navigate up (not at root)
|
|
1588
2028
|
*/
|
|
2029
|
+
CanNavigateUp() {
|
|
2030
|
+
return this.FolderPath !== '' && this.FolderPath !== '/';
|
|
2031
|
+
}
|
|
2032
|
+
/** @deprecated Use {@link CanNavigateUp}. */
|
|
1589
2033
|
canNavigateUp() {
|
|
1590
|
-
return this.
|
|
2034
|
+
return this.CanNavigateUp();
|
|
1591
2035
|
}
|
|
1592
2036
|
/**
|
|
1593
2037
|
* Handles sort change event from the grid
|
|
1594
2038
|
*/
|
|
2039
|
+
OnSortChange(sort) {
|
|
2040
|
+
this.Sort = sort;
|
|
2041
|
+
}
|
|
2042
|
+
/** @deprecated Use {@link OnSortChange}. */
|
|
1595
2043
|
onSortChange(sort) {
|
|
1596
|
-
this.sort
|
|
2044
|
+
return this.OnSortChange(sort);
|
|
1597
2045
|
}
|
|
1598
2046
|
/**
|
|
1599
2047
|
* Gets a human-readable file type based on extension
|
|
1600
2048
|
*/
|
|
1601
|
-
|
|
1602
|
-
if (item.
|
|
2049
|
+
GetFileType(item) {
|
|
2050
|
+
if (item.Type === 'folder') {
|
|
1603
2051
|
return 'Folder';
|
|
1604
2052
|
}
|
|
1605
2053
|
const extension = item.name.split('.').pop()?.toLowerCase() || '';
|
|
@@ -1645,10 +2093,14 @@ export class FileGridComponent {
|
|
|
1645
2093
|
};
|
|
1646
2094
|
return typeMap[extension] || `${extension.toUpperCase()} File`;
|
|
1647
2095
|
}
|
|
2096
|
+
/** @deprecated Use {@link GetFileType}. */
|
|
2097
|
+
getFileType(item) {
|
|
2098
|
+
return this.GetFileType(item);
|
|
2099
|
+
}
|
|
1648
2100
|
/**
|
|
1649
2101
|
* Handles upload button click - triggers file input
|
|
1650
2102
|
*/
|
|
1651
|
-
|
|
2103
|
+
OnUploadClick() {
|
|
1652
2104
|
const fileInput = document.createElement('input');
|
|
1653
2105
|
fileInput.type = 'file';
|
|
1654
2106
|
fileInput.multiple = true;
|
|
@@ -1660,49 +2112,69 @@ export class FileGridComponent {
|
|
|
1660
2112
|
};
|
|
1661
2113
|
fileInput.click();
|
|
1662
2114
|
}
|
|
2115
|
+
/** @deprecated Use {@link OnUploadClick}. */
|
|
2116
|
+
onUploadClick() {
|
|
2117
|
+
return this.OnUploadClick();
|
|
2118
|
+
}
|
|
1663
2119
|
/**
|
|
1664
2120
|
* Handles drag enter event
|
|
1665
2121
|
*/
|
|
1666
|
-
|
|
2122
|
+
OnDragEnter(event) {
|
|
1667
2123
|
event.preventDefault();
|
|
1668
2124
|
event.stopPropagation();
|
|
1669
|
-
this.
|
|
2125
|
+
this.IsDragging = true;
|
|
2126
|
+
}
|
|
2127
|
+
/** @deprecated Use {@link OnDragEnter}. */
|
|
2128
|
+
onDragEnter(event) {
|
|
2129
|
+
return this.OnDragEnter(event);
|
|
1670
2130
|
}
|
|
1671
2131
|
/**
|
|
1672
2132
|
* Handles drag over event
|
|
1673
2133
|
*/
|
|
1674
|
-
|
|
2134
|
+
OnDragOver(event) {
|
|
1675
2135
|
event.preventDefault();
|
|
1676
2136
|
event.stopPropagation();
|
|
1677
2137
|
}
|
|
2138
|
+
/** @deprecated Use {@link OnDragOver}. */
|
|
2139
|
+
onDragOver(event) {
|
|
2140
|
+
return this.OnDragOver(event);
|
|
2141
|
+
}
|
|
1678
2142
|
/**
|
|
1679
2143
|
* Handles drag leave event
|
|
1680
2144
|
*/
|
|
1681
|
-
|
|
2145
|
+
OnDragLeave(event) {
|
|
1682
2146
|
event.preventDefault();
|
|
1683
2147
|
event.stopPropagation();
|
|
1684
2148
|
// Only hide if leaving the container itself, not child elements
|
|
1685
2149
|
if (event.currentTarget === event.target) {
|
|
1686
|
-
this.
|
|
2150
|
+
this.IsDragging = false;
|
|
1687
2151
|
}
|
|
1688
2152
|
}
|
|
2153
|
+
/** @deprecated Use {@link OnDragLeave}. */
|
|
2154
|
+
onDragLeave(event) {
|
|
2155
|
+
return this.OnDragLeave(event);
|
|
2156
|
+
}
|
|
1689
2157
|
/**
|
|
1690
2158
|
* Handles drop event
|
|
1691
2159
|
*/
|
|
1692
|
-
|
|
2160
|
+
OnDrop(event) {
|
|
1693
2161
|
event.preventDefault();
|
|
1694
2162
|
event.stopPropagation();
|
|
1695
|
-
this.
|
|
2163
|
+
this.IsDragging = false;
|
|
1696
2164
|
const files = event.dataTransfer?.files;
|
|
1697
2165
|
if (files && files.length > 0) {
|
|
1698
2166
|
this.uploadFiles(Array.from(files));
|
|
1699
2167
|
}
|
|
1700
2168
|
}
|
|
2169
|
+
/** @deprecated Use {@link OnDrop}. */
|
|
2170
|
+
onDrop(event) {
|
|
2171
|
+
return this.OnDrop(event);
|
|
2172
|
+
}
|
|
1701
2173
|
/**
|
|
1702
2174
|
* Uploads multiple files to the current folder
|
|
1703
2175
|
*/
|
|
1704
2176
|
async uploadFiles(files) {
|
|
1705
|
-
if (!this.
|
|
2177
|
+
if (!this.Account) {
|
|
1706
2178
|
return;
|
|
1707
2179
|
}
|
|
1708
2180
|
console.log('[FileGrid] Uploading files:', files.map(f => f.name));
|
|
@@ -1717,16 +2189,16 @@ export class FileGridComponent {
|
|
|
1717
2189
|
* Uploads a single file using server-side MJ Storage (supports all providers)
|
|
1718
2190
|
*/
|
|
1719
2191
|
async uploadSingleFile(file, current, total) {
|
|
1720
|
-
if (!this.
|
|
2192
|
+
if (!this.Account) {
|
|
1721
2193
|
return;
|
|
1722
2194
|
}
|
|
1723
|
-
this.
|
|
1724
|
-
this.
|
|
1725
|
-
this.
|
|
2195
|
+
this.IsUploading = true;
|
|
2196
|
+
this.UploadingFileName = `${file.name} (${current}/${total})`;
|
|
2197
|
+
this.UploadProgress = 0;
|
|
1726
2198
|
try {
|
|
1727
2199
|
let cleanPath = '';
|
|
1728
|
-
if (this.
|
|
1729
|
-
cleanPath = this.
|
|
2200
|
+
if (this.FolderPath && this.FolderPath !== '/') {
|
|
2201
|
+
cleanPath = this.FolderPath.endsWith('/') ? this.FolderPath.slice(0, -1) : this.FolderPath;
|
|
1730
2202
|
if (cleanPath.startsWith('/')) {
|
|
1731
2203
|
cleanPath = cleanPath.substring(1);
|
|
1732
2204
|
}
|
|
@@ -1737,13 +2209,13 @@ export class FileGridComponent {
|
|
|
1737
2209
|
FileName: file.name,
|
|
1738
2210
|
Base64Data: base64Data,
|
|
1739
2211
|
MimeType: file.type || 'application/octet-stream',
|
|
1740
|
-
AccountID: this.
|
|
2212
|
+
AccountID: this.Account.account.ID,
|
|
1741
2213
|
PathPrefix: cleanPath || undefined,
|
|
1742
2214
|
});
|
|
1743
2215
|
if (!result.Success) {
|
|
1744
2216
|
throw new Error(result.ErrorMessage || 'Upload failed');
|
|
1745
2217
|
}
|
|
1746
|
-
this.
|
|
2218
|
+
this.UploadProgress = 100;
|
|
1747
2219
|
console.log('[FileGrid] File uploaded successfully via MJ Storage:', file.name);
|
|
1748
2220
|
}
|
|
1749
2221
|
catch (error) {
|
|
@@ -1758,9 +2230,9 @@ export class FileGridComponent {
|
|
|
1758
2230
|
}, 5000);
|
|
1759
2231
|
}
|
|
1760
2232
|
finally {
|
|
1761
|
-
this.
|
|
1762
|
-
this.
|
|
1763
|
-
this.
|
|
2233
|
+
this.IsUploading = false;
|
|
2234
|
+
this.UploadProgress = 0;
|
|
2235
|
+
this.UploadingFileName = '';
|
|
1764
2236
|
}
|
|
1765
2237
|
}
|
|
1766
2238
|
/**
|
|
@@ -1791,7 +2263,7 @@ export class FileGridComponent {
|
|
|
1791
2263
|
const xhr = new XMLHttpRequest();
|
|
1792
2264
|
xhr.upload.addEventListener('progress', (event) => {
|
|
1793
2265
|
if (event.lengthComputable) {
|
|
1794
|
-
this.
|
|
2266
|
+
this.UploadProgress = Math.round((event.loaded / event.total) * 100);
|
|
1795
2267
|
}
|
|
1796
2268
|
});
|
|
1797
2269
|
xhr.addEventListener('load', () => {
|
|
@@ -1826,49 +2298,57 @@ export class FileGridComponent {
|
|
|
1826
2298
|
/**
|
|
1827
2299
|
* Opens the new folder dialog
|
|
1828
2300
|
*/
|
|
2301
|
+
OnNewFolderClick() {
|
|
2302
|
+
this.NewFolderName = '';
|
|
2303
|
+
this.ShowNewFolderDialog = true;
|
|
2304
|
+
}
|
|
2305
|
+
/** @deprecated Use {@link OnNewFolderClick}. */
|
|
1829
2306
|
onNewFolderClick() {
|
|
1830
|
-
this.
|
|
1831
|
-
this.showNewFolderDialog = true;
|
|
2307
|
+
return this.OnNewFolderClick();
|
|
1832
2308
|
}
|
|
1833
2309
|
/**
|
|
1834
2310
|
* Closes the new folder dialog
|
|
1835
2311
|
*/
|
|
2312
|
+
OnCancelNewFolder() {
|
|
2313
|
+
this.ShowNewFolderDialog = false;
|
|
2314
|
+
this.NewFolderName = '';
|
|
2315
|
+
}
|
|
2316
|
+
/** @deprecated Use {@link OnCancelNewFolder}. */
|
|
1836
2317
|
onCancelNewFolder() {
|
|
1837
|
-
this.
|
|
1838
|
-
this.newFolderName = '';
|
|
2318
|
+
return this.OnCancelNewFolder();
|
|
1839
2319
|
}
|
|
1840
2320
|
/**
|
|
1841
2321
|
* Creates a new folder in the current directory
|
|
1842
2322
|
*/
|
|
1843
|
-
async
|
|
1844
|
-
if (!this.
|
|
2323
|
+
async OnCreateFolder() {
|
|
2324
|
+
if (!this.Account || !this.NewFolderName.trim()) {
|
|
1845
2325
|
return;
|
|
1846
2326
|
}
|
|
1847
|
-
this.
|
|
2327
|
+
this.IsCreatingFolder = true;
|
|
1848
2328
|
try {
|
|
1849
2329
|
// Construct the full folder path
|
|
1850
2330
|
// Remove trailing slash from folderPath to avoid double slashes
|
|
1851
2331
|
let folderPath;
|
|
1852
|
-
if (this.
|
|
1853
|
-
const cleanPath = this.
|
|
1854
|
-
? this.
|
|
1855
|
-
: this.
|
|
1856
|
-
folderPath = `${cleanPath}/${this.
|
|
2332
|
+
if (this.FolderPath) {
|
|
2333
|
+
const cleanPath = this.FolderPath.endsWith('/')
|
|
2334
|
+
? this.FolderPath.slice(0, -1)
|
|
2335
|
+
: this.FolderPath;
|
|
2336
|
+
folderPath = `${cleanPath}/${this.NewFolderName.trim()}`;
|
|
1857
2337
|
}
|
|
1858
2338
|
else {
|
|
1859
|
-
folderPath = this.
|
|
2339
|
+
folderPath = this.NewFolderName.trim();
|
|
1860
2340
|
}
|
|
1861
2341
|
console.log('[FileGrid] Creating folder:', folderPath);
|
|
1862
|
-
const success = await this.storageClient.CreateDirectory(this.
|
|
2342
|
+
const success = await this.storageClient.CreateDirectory(this.Account.account.ID, folderPath);
|
|
1863
2343
|
if (success) {
|
|
1864
2344
|
console.log('[FileGrid] Folder created successfully:', folderPath);
|
|
1865
|
-
const createdName = this.
|
|
2345
|
+
const createdName = this.NewFolderName.trim();
|
|
1866
2346
|
// Close dialog and refresh
|
|
1867
|
-
this.
|
|
1868
|
-
this.
|
|
2347
|
+
this.ShowNewFolderDialog = false;
|
|
2348
|
+
this.NewFolderName = '';
|
|
1869
2349
|
this.notifications.CreateSimpleNotification(`Created folder "${createdName}"`, 'success');
|
|
1870
2350
|
// Notify parent that folder structure changed
|
|
1871
|
-
this.
|
|
2351
|
+
this.FolderStructureChanged.emit();
|
|
1872
2352
|
// Refresh the file grid
|
|
1873
2353
|
this.loadItems();
|
|
1874
2354
|
}
|
|
@@ -1882,63 +2362,75 @@ export class FileGridComponent {
|
|
|
1882
2362
|
this.notifications.CreateSimpleNotification(`Failed to create folder: ${errorMessage}`, 'error');
|
|
1883
2363
|
}
|
|
1884
2364
|
finally {
|
|
1885
|
-
this.
|
|
2365
|
+
this.IsCreatingFolder = false;
|
|
1886
2366
|
}
|
|
1887
2367
|
}
|
|
2368
|
+
/** @deprecated Use {@link OnCreateFolder}. */
|
|
2369
|
+
async onCreateFolder() {
|
|
2370
|
+
return this.OnCreateFolder();
|
|
2371
|
+
}
|
|
1888
2372
|
/**
|
|
1889
2373
|
* Opens the delete confirmation dialog for the selected items
|
|
1890
2374
|
*/
|
|
1891
|
-
|
|
1892
|
-
if (this.
|
|
2375
|
+
OnDeleteClick() {
|
|
2376
|
+
if (this.SelectedItems.length === 0) {
|
|
1893
2377
|
return;
|
|
1894
2378
|
}
|
|
1895
2379
|
// selectedItems contains keys (strings), not full objects
|
|
1896
2380
|
// Find the actual item object from the items array
|
|
1897
|
-
const selectedKey = this.
|
|
1898
|
-
const item = this.
|
|
2381
|
+
const selectedKey = this.SelectedItems[0];
|
|
2382
|
+
const item = this.Items.find(i => i.Key === selectedKey);
|
|
1899
2383
|
if (!item) {
|
|
1900
2384
|
console.error('[FileGrid] Could not find selected item with key:', selectedKey);
|
|
1901
2385
|
return;
|
|
1902
2386
|
}
|
|
1903
|
-
this.
|
|
2387
|
+
this.ItemToDelete = item;
|
|
1904
2388
|
this.showDeleteDialog = true;
|
|
1905
2389
|
}
|
|
2390
|
+
/** @deprecated Use {@link OnDeleteClick}. */
|
|
2391
|
+
onDeleteClick() {
|
|
2392
|
+
return this.OnDeleteClick();
|
|
2393
|
+
}
|
|
1906
2394
|
/**
|
|
1907
2395
|
* Closes the delete confirmation dialog
|
|
1908
2396
|
*/
|
|
1909
|
-
|
|
2397
|
+
OnCancelDelete() {
|
|
1910
2398
|
this.showDeleteDialog = false;
|
|
1911
|
-
this.
|
|
2399
|
+
this.ItemToDelete = null;
|
|
2400
|
+
}
|
|
2401
|
+
/** @deprecated Use {@link OnCancelDelete}. */
|
|
2402
|
+
onCancelDelete() {
|
|
2403
|
+
return this.OnCancelDelete();
|
|
1912
2404
|
}
|
|
1913
2405
|
/**
|
|
1914
2406
|
* Deletes the selected item after confirmation
|
|
1915
2407
|
*/
|
|
1916
|
-
async
|
|
1917
|
-
if (!this.
|
|
2408
|
+
async OnConfirmDelete() {
|
|
2409
|
+
if (!this.Account || !this.ItemToDelete) {
|
|
1918
2410
|
return;
|
|
1919
2411
|
}
|
|
1920
|
-
this.
|
|
2412
|
+
this.IsDeleting = true;
|
|
1921
2413
|
try {
|
|
1922
2414
|
// Construct the full path to the item
|
|
1923
|
-
const itemPath = this.constructItemPath(this.
|
|
2415
|
+
const itemPath = this.constructItemPath(this.ItemToDelete);
|
|
1924
2416
|
console.log('[FileGrid] Deleting item:', itemPath);
|
|
1925
|
-
const success = await this.storageClient.DeleteObject(this.
|
|
2417
|
+
const success = await this.storageClient.DeleteObject(this.Account.account.ID, itemPath);
|
|
1926
2418
|
console.log('[FileGrid] Delete result:', { success });
|
|
1927
2419
|
if (success) {
|
|
1928
2420
|
console.log('[FileGrid] Item deleted successfully:', itemPath);
|
|
1929
2421
|
// Check if we deleted a folder (before clearing itemToDelete)
|
|
1930
|
-
const wasFolder = this.
|
|
1931
|
-
const deletedName = this.
|
|
2422
|
+
const wasFolder = this.ItemToDelete.Type === 'folder';
|
|
2423
|
+
const deletedName = this.ItemToDelete.name;
|
|
1932
2424
|
// Close dialog
|
|
1933
2425
|
this.showDeleteDialog = false;
|
|
1934
|
-
this.
|
|
2426
|
+
this.ItemToDelete = null;
|
|
1935
2427
|
// Clear selection
|
|
1936
|
-
this.
|
|
2428
|
+
this.SelectedItems = [];
|
|
1937
2429
|
this.notifications.CreateSimpleNotification(`Deleted "${deletedName}"`, 'info');
|
|
1938
2430
|
// If we deleted a folder, notify parent that folder structure changed
|
|
1939
2431
|
if (wasFolder) {
|
|
1940
2432
|
console.log('[FileGrid] Emitting folderStructureChanged event');
|
|
1941
|
-
this.
|
|
2433
|
+
this.FolderStructureChanged.emit();
|
|
1942
2434
|
}
|
|
1943
2435
|
// Refresh the file grid
|
|
1944
2436
|
console.log('[FileGrid] Calling loadItems() to refresh grid');
|
|
@@ -1946,7 +2438,7 @@ export class FileGridComponent {
|
|
|
1946
2438
|
}
|
|
1947
2439
|
else {
|
|
1948
2440
|
console.error('[FileGrid] Delete operation returned false');
|
|
1949
|
-
this.notifications.CreateSimpleNotification(`Failed to delete ${this.
|
|
2441
|
+
this.notifications.CreateSimpleNotification(`Failed to delete ${this.ItemToDelete.Type}`, 'error');
|
|
1950
2442
|
}
|
|
1951
2443
|
}
|
|
1952
2444
|
catch (error) {
|
|
@@ -1955,76 +2447,88 @@ export class FileGridComponent {
|
|
|
1955
2447
|
this.notifications.CreateSimpleNotification(`Failed to delete: ${errorMessage}`, 'error');
|
|
1956
2448
|
}
|
|
1957
2449
|
finally {
|
|
1958
|
-
this.
|
|
2450
|
+
this.IsDeleting = false;
|
|
1959
2451
|
}
|
|
1960
2452
|
}
|
|
2453
|
+
/** @deprecated Use {@link OnConfirmDelete}. */
|
|
2454
|
+
async onConfirmDelete() {
|
|
2455
|
+
return this.OnConfirmDelete();
|
|
2456
|
+
}
|
|
1961
2457
|
/**
|
|
1962
2458
|
* Opens the rename dialog for the selected item
|
|
1963
2459
|
*/
|
|
1964
|
-
|
|
1965
|
-
if (this.
|
|
2460
|
+
OnRenameClick() {
|
|
2461
|
+
if (this.SelectedItems.length !== 1) {
|
|
1966
2462
|
return;
|
|
1967
2463
|
}
|
|
1968
2464
|
// selectedItems contains keys (strings), not full objects
|
|
1969
2465
|
// Find the actual item object from the items array
|
|
1970
|
-
const selectedKey = this.
|
|
1971
|
-
const item = this.
|
|
2466
|
+
const selectedKey = this.SelectedItems[0];
|
|
2467
|
+
const item = this.Items.find(i => i.Key === selectedKey);
|
|
1972
2468
|
if (!item) {
|
|
1973
2469
|
console.error('[FileGrid] Could not find selected item with key:', selectedKey);
|
|
1974
2470
|
return;
|
|
1975
2471
|
}
|
|
1976
|
-
this.
|
|
1977
|
-
this.
|
|
1978
|
-
this.
|
|
2472
|
+
this.ItemToRename = item;
|
|
2473
|
+
this.NewItemName = item.name;
|
|
2474
|
+
this.ShowRenameDialog = true;
|
|
2475
|
+
}
|
|
2476
|
+
/** @deprecated Use {@link OnRenameClick}. */
|
|
2477
|
+
onRenameClick() {
|
|
2478
|
+
return this.OnRenameClick();
|
|
1979
2479
|
}
|
|
1980
2480
|
/**
|
|
1981
2481
|
* Closes the rename dialog
|
|
1982
2482
|
*/
|
|
2483
|
+
OnCancelRename() {
|
|
2484
|
+
this.ShowRenameDialog = false;
|
|
2485
|
+
this.ItemToRename = null;
|
|
2486
|
+
this.NewItemName = '';
|
|
2487
|
+
}
|
|
2488
|
+
/** @deprecated Use {@link OnCancelRename}. */
|
|
1983
2489
|
onCancelRename() {
|
|
1984
|
-
this.
|
|
1985
|
-
this.itemToRename = null;
|
|
1986
|
-
this.newItemName = '';
|
|
2490
|
+
return this.OnCancelRename();
|
|
1987
2491
|
}
|
|
1988
2492
|
/**
|
|
1989
2493
|
* Renames the selected item after confirmation
|
|
1990
2494
|
*/
|
|
1991
|
-
async
|
|
1992
|
-
if (!this.
|
|
2495
|
+
async OnConfirmRename() {
|
|
2496
|
+
if (!this.Account || !this.ItemToRename || !this.NewItemName.trim()) {
|
|
1993
2497
|
return;
|
|
1994
2498
|
}
|
|
1995
2499
|
// Check if name actually changed
|
|
1996
|
-
if (this.
|
|
1997
|
-
this.
|
|
2500
|
+
if (this.NewItemName.trim() === this.ItemToRename.name) {
|
|
2501
|
+
this.OnCancelRename();
|
|
1998
2502
|
return;
|
|
1999
2503
|
}
|
|
2000
|
-
this.
|
|
2504
|
+
this.IsRenaming = true;
|
|
2001
2505
|
try {
|
|
2002
2506
|
// Construct the old and new paths
|
|
2003
|
-
const oldPath = this.constructItemPath(this.
|
|
2507
|
+
const oldPath = this.constructItemPath(this.ItemToRename);
|
|
2004
2508
|
// Build the new path by replacing the old name with the new name
|
|
2005
2509
|
const pathParts = oldPath.split('/');
|
|
2006
|
-
pathParts[pathParts.length - 1] = this.
|
|
2510
|
+
pathParts[pathParts.length - 1] = this.NewItemName.trim();
|
|
2007
2511
|
const newPath = pathParts.join('/');
|
|
2008
2512
|
console.log('[FileGrid] Renaming item:', { oldPath, newPath });
|
|
2009
|
-
const success = await this.storageClient.MoveObject(this.
|
|
2513
|
+
const success = await this.storageClient.MoveObject(this.Account.account.ID, oldPath, newPath);
|
|
2010
2514
|
console.log('[FileGrid] Rename result:', { success });
|
|
2011
2515
|
if (success) {
|
|
2012
2516
|
console.log('[FileGrid] Item renamed successfully:', { oldPath, newPath });
|
|
2013
2517
|
// Check if we renamed a folder (before clearing itemToRename)
|
|
2014
|
-
const wasFolder = this.
|
|
2015
|
-
const oldName = this.
|
|
2016
|
-
const newName = this.
|
|
2518
|
+
const wasFolder = this.ItemToRename.Type === 'folder';
|
|
2519
|
+
const oldName = this.ItemToRename.name;
|
|
2520
|
+
const newName = this.NewItemName.trim();
|
|
2017
2521
|
// Close dialog
|
|
2018
|
-
this.
|
|
2019
|
-
this.
|
|
2020
|
-
this.
|
|
2522
|
+
this.ShowRenameDialog = false;
|
|
2523
|
+
this.ItemToRename = null;
|
|
2524
|
+
this.NewItemName = '';
|
|
2021
2525
|
// Clear selection
|
|
2022
|
-
this.
|
|
2526
|
+
this.SelectedItems = [];
|
|
2023
2527
|
this.notifications.CreateSimpleNotification(`Renamed "${oldName}" to "${newName}"`, 'success');
|
|
2024
2528
|
// If we renamed a folder, notify parent that folder structure changed
|
|
2025
2529
|
if (wasFolder) {
|
|
2026
2530
|
console.log('[FileGrid] Emitting folderStructureChanged event');
|
|
2027
|
-
this.
|
|
2531
|
+
this.FolderStructureChanged.emit();
|
|
2028
2532
|
}
|
|
2029
2533
|
// Refresh the file grid
|
|
2030
2534
|
console.log('[FileGrid] Calling loadItems() to refresh grid');
|
|
@@ -2032,7 +2536,7 @@ export class FileGridComponent {
|
|
|
2032
2536
|
}
|
|
2033
2537
|
else {
|
|
2034
2538
|
console.error('[FileGrid] Rename operation returned false');
|
|
2035
|
-
this.notifications.CreateSimpleNotification(`Failed to rename ${this.
|
|
2539
|
+
this.notifications.CreateSimpleNotification(`Failed to rename ${this.ItemToRename.Type}`, 'error');
|
|
2036
2540
|
}
|
|
2037
2541
|
}
|
|
2038
2542
|
catch (error) {
|
|
@@ -2041,35 +2545,39 @@ export class FileGridComponent {
|
|
|
2041
2545
|
this.notifications.CreateSimpleNotification(`Failed to rename: ${errorMessage}`, 'error');
|
|
2042
2546
|
}
|
|
2043
2547
|
finally {
|
|
2044
|
-
this.
|
|
2548
|
+
this.IsRenaming = false;
|
|
2045
2549
|
}
|
|
2046
2550
|
}
|
|
2551
|
+
/** @deprecated Use {@link OnConfirmRename}. */
|
|
2552
|
+
async onConfirmRename() {
|
|
2553
|
+
return this.OnConfirmRename();
|
|
2554
|
+
}
|
|
2047
2555
|
/**
|
|
2048
2556
|
* Downloads the selected file
|
|
2049
2557
|
*/
|
|
2050
|
-
async
|
|
2051
|
-
if (this.
|
|
2558
|
+
async OnDownloadClick() {
|
|
2559
|
+
if (this.SelectedItems.length !== 1) {
|
|
2052
2560
|
return;
|
|
2053
2561
|
}
|
|
2054
2562
|
// Find the selected item
|
|
2055
|
-
const selectedKey = this.
|
|
2056
|
-
const item = this.
|
|
2563
|
+
const selectedKey = this.SelectedItems[0];
|
|
2564
|
+
const item = this.Items.find(i => i.Key === selectedKey);
|
|
2057
2565
|
if (!item) {
|
|
2058
2566
|
console.error('[FileGrid] Could not find selected item with key:', selectedKey);
|
|
2059
2567
|
return;
|
|
2060
2568
|
}
|
|
2061
2569
|
// Can only download files, not folders
|
|
2062
|
-
if (item.
|
|
2570
|
+
if (item.Type === 'folder') {
|
|
2063
2571
|
this.notifications.CreateSimpleNotification('Cannot download folders. Please select a file.', 'warning');
|
|
2064
2572
|
return;
|
|
2065
2573
|
}
|
|
2066
|
-
if (!this.
|
|
2574
|
+
if (!this.Account) {
|
|
2067
2575
|
return;
|
|
2068
2576
|
}
|
|
2069
2577
|
try {
|
|
2070
2578
|
const itemPath = this.constructItemPath(item);
|
|
2071
2579
|
console.log('[FileGrid] Creating download URL for:', itemPath);
|
|
2072
|
-
const downloadUrl = await this.storageClient.CreatePreAuthDownloadUrl(this.
|
|
2580
|
+
const downloadUrl = await this.storageClient.CreatePreAuthDownloadUrl(this.Account.account.ID, itemPath);
|
|
2073
2581
|
if (downloadUrl) {
|
|
2074
2582
|
console.log('[FileGrid] Download URL created, opening in new tab');
|
|
2075
2583
|
window.open(downloadUrl, '_blank', 'noopener,noreferrer');
|
|
@@ -2085,49 +2593,61 @@ export class FileGridComponent {
|
|
|
2085
2593
|
this.notifications.CreateSimpleNotification(`Failed to download: ${errorMessage}`, 'error');
|
|
2086
2594
|
}
|
|
2087
2595
|
}
|
|
2596
|
+
/** @deprecated Use {@link OnDownloadClick}. */
|
|
2597
|
+
async onDownloadClick() {
|
|
2598
|
+
return this.OnDownloadClick();
|
|
2599
|
+
}
|
|
2088
2600
|
/**
|
|
2089
2601
|
* Opens the copy dialog for the selected item
|
|
2090
2602
|
*/
|
|
2091
|
-
|
|
2092
|
-
if (this.
|
|
2603
|
+
OnCopyClick() {
|
|
2604
|
+
if (this.SelectedItems.length !== 1) {
|
|
2093
2605
|
return;
|
|
2094
2606
|
}
|
|
2095
|
-
const selectedKey = this.
|
|
2096
|
-
const item = this.
|
|
2607
|
+
const selectedKey = this.SelectedItems[0];
|
|
2608
|
+
const item = this.Items.find(i => i.Key === selectedKey);
|
|
2097
2609
|
if (!item) {
|
|
2098
2610
|
console.error('[FileGrid] Could not find selected item with key:', selectedKey);
|
|
2099
2611
|
return;
|
|
2100
2612
|
}
|
|
2101
|
-
this.
|
|
2102
|
-
this.
|
|
2103
|
-
this.
|
|
2613
|
+
this.ItemToCopy = item;
|
|
2614
|
+
this.CopyDestinationPath = this.FolderPath + item.name + '-copy';
|
|
2615
|
+
this.ShowCopyDialog = true;
|
|
2616
|
+
}
|
|
2617
|
+
/** @deprecated Use {@link OnCopyClick}. */
|
|
2618
|
+
onCopyClick() {
|
|
2619
|
+
return this.OnCopyClick();
|
|
2104
2620
|
}
|
|
2105
2621
|
/**
|
|
2106
2622
|
* Cancels the copy operation
|
|
2107
2623
|
*/
|
|
2624
|
+
OnCancelCopy() {
|
|
2625
|
+
this.ShowCopyDialog = false;
|
|
2626
|
+
this.ItemToCopy = null;
|
|
2627
|
+
this.CopyDestinationPath = '';
|
|
2628
|
+
}
|
|
2629
|
+
/** @deprecated Use {@link OnCancelCopy}. */
|
|
2108
2630
|
onCancelCopy() {
|
|
2109
|
-
this.
|
|
2110
|
-
this.itemToCopy = null;
|
|
2111
|
-
this.copyDestinationPath = '';
|
|
2631
|
+
return this.OnCancelCopy();
|
|
2112
2632
|
}
|
|
2113
2633
|
/**
|
|
2114
2634
|
* Confirms and executes the copy operation
|
|
2115
2635
|
*/
|
|
2116
|
-
async
|
|
2117
|
-
if (!this.
|
|
2636
|
+
async OnConfirmCopy() {
|
|
2637
|
+
if (!this.Account || !this.ItemToCopy || !this.CopyDestinationPath.trim()) {
|
|
2118
2638
|
return;
|
|
2119
2639
|
}
|
|
2120
|
-
this.
|
|
2640
|
+
this.IsCopying = true;
|
|
2121
2641
|
try {
|
|
2122
|
-
const sourcePath = this.constructItemPath(this.
|
|
2123
|
-
const success = await this.storageClient.CopyObject(this.
|
|
2642
|
+
const sourcePath = this.constructItemPath(this.ItemToCopy);
|
|
2643
|
+
const success = await this.storageClient.CopyObject(this.Account.account.ID, sourcePath, this.CopyDestinationPath.trim());
|
|
2124
2644
|
if (success) {
|
|
2125
2645
|
// Close dialog
|
|
2126
|
-
this.
|
|
2127
|
-
this.
|
|
2128
|
-
this.
|
|
2646
|
+
this.ShowCopyDialog = false;
|
|
2647
|
+
this.ItemToCopy = null;
|
|
2648
|
+
this.CopyDestinationPath = '';
|
|
2129
2649
|
// Clear selection
|
|
2130
|
-
this.
|
|
2650
|
+
this.SelectedItems = [];
|
|
2131
2651
|
this.notifications.CreateSimpleNotification('Item copied successfully', 'success');
|
|
2132
2652
|
// Refresh the file grid
|
|
2133
2653
|
this.loadItems();
|
|
@@ -2142,61 +2662,73 @@ export class FileGridComponent {
|
|
|
2142
2662
|
this.notifications.CreateSimpleNotification(`Failed to copy: ${errorMessage}`, 'error');
|
|
2143
2663
|
}
|
|
2144
2664
|
finally {
|
|
2145
|
-
this.
|
|
2665
|
+
this.IsCopying = false;
|
|
2146
2666
|
}
|
|
2147
2667
|
}
|
|
2668
|
+
/** @deprecated Use {@link OnConfirmCopy}. */
|
|
2669
|
+
async onConfirmCopy() {
|
|
2670
|
+
return this.OnConfirmCopy();
|
|
2671
|
+
}
|
|
2148
2672
|
/**
|
|
2149
2673
|
* Opens the move dialog for the selected item
|
|
2150
2674
|
*/
|
|
2151
|
-
|
|
2152
|
-
if (this.
|
|
2675
|
+
OnMoveClick() {
|
|
2676
|
+
if (this.SelectedItems.length !== 1) {
|
|
2153
2677
|
return;
|
|
2154
2678
|
}
|
|
2155
|
-
const selectedKey = this.
|
|
2156
|
-
const item = this.
|
|
2679
|
+
const selectedKey = this.SelectedItems[0];
|
|
2680
|
+
const item = this.Items.find(i => i.Key === selectedKey);
|
|
2157
2681
|
if (!item) {
|
|
2158
2682
|
console.error('[FileGrid] Could not find selected item with key:', selectedKey);
|
|
2159
2683
|
return;
|
|
2160
2684
|
}
|
|
2161
|
-
this.
|
|
2685
|
+
this.ItemToMove = item;
|
|
2162
2686
|
// Suggest a different folder path
|
|
2163
|
-
this.
|
|
2164
|
-
this.
|
|
2687
|
+
this.MoveDestinationPath = this.FolderPath;
|
|
2688
|
+
this.ShowMoveDialog = true;
|
|
2689
|
+
}
|
|
2690
|
+
/** @deprecated Use {@link OnMoveClick}. */
|
|
2691
|
+
onMoveClick() {
|
|
2692
|
+
return this.OnMoveClick();
|
|
2165
2693
|
}
|
|
2166
2694
|
/**
|
|
2167
2695
|
* Cancels the move operation
|
|
2168
2696
|
*/
|
|
2697
|
+
OnCancelMove() {
|
|
2698
|
+
this.ShowMoveDialog = false;
|
|
2699
|
+
this.ItemToMove = null;
|
|
2700
|
+
this.MoveDestinationPath = '';
|
|
2701
|
+
}
|
|
2702
|
+
/** @deprecated Use {@link OnCancelMove}. */
|
|
2169
2703
|
onCancelMove() {
|
|
2170
|
-
this.
|
|
2171
|
-
this.itemToMove = null;
|
|
2172
|
-
this.moveDestinationPath = '';
|
|
2704
|
+
return this.OnCancelMove();
|
|
2173
2705
|
}
|
|
2174
2706
|
/**
|
|
2175
2707
|
* Confirms and executes the move operation
|
|
2176
2708
|
*/
|
|
2177
|
-
async
|
|
2178
|
-
if (!this.
|
|
2709
|
+
async OnConfirmMove() {
|
|
2710
|
+
if (!this.Account || !this.ItemToMove || !this.MoveDestinationPath.trim()) {
|
|
2179
2711
|
return;
|
|
2180
2712
|
}
|
|
2181
|
-
this.
|
|
2713
|
+
this.IsMoving = true;
|
|
2182
2714
|
try {
|
|
2183
|
-
const sourcePath = this.constructItemPath(this.
|
|
2184
|
-
const destPath = this.
|
|
2185
|
-
? this.
|
|
2186
|
-
: this.
|
|
2187
|
-
const success = await this.storageClient.MoveObject(this.
|
|
2715
|
+
const sourcePath = this.constructItemPath(this.ItemToMove);
|
|
2716
|
+
const destPath = this.MoveDestinationPath.trim().endsWith('/')
|
|
2717
|
+
? this.MoveDestinationPath.trim() + this.ItemToMove.name
|
|
2718
|
+
: this.MoveDestinationPath.trim();
|
|
2719
|
+
const success = await this.storageClient.MoveObject(this.Account.account.ID, sourcePath, destPath);
|
|
2188
2720
|
if (success) {
|
|
2189
|
-
const wasFolder = this.
|
|
2721
|
+
const wasFolder = this.ItemToMove.Type === 'folder';
|
|
2190
2722
|
// Close dialog
|
|
2191
|
-
this.
|
|
2192
|
-
this.
|
|
2193
|
-
this.
|
|
2723
|
+
this.ShowMoveDialog = false;
|
|
2724
|
+
this.ItemToMove = null;
|
|
2725
|
+
this.MoveDestinationPath = '';
|
|
2194
2726
|
// Clear selection
|
|
2195
|
-
this.
|
|
2727
|
+
this.SelectedItems = [];
|
|
2196
2728
|
this.notifications.CreateSimpleNotification('Item moved successfully', 'success');
|
|
2197
2729
|
// If we moved a folder, notify parent that folder structure changed
|
|
2198
2730
|
if (wasFolder) {
|
|
2199
|
-
this.
|
|
2731
|
+
this.FolderStructureChanged.emit();
|
|
2200
2732
|
}
|
|
2201
2733
|
// Refresh the file grid
|
|
2202
2734
|
this.loadItems();
|
|
@@ -2211,32 +2743,36 @@ export class FileGridComponent {
|
|
|
2211
2743
|
this.notifications.CreateSimpleNotification(`Failed to move: ${errorMessage}`, 'error');
|
|
2212
2744
|
}
|
|
2213
2745
|
finally {
|
|
2214
|
-
this.
|
|
2746
|
+
this.IsMoving = false;
|
|
2215
2747
|
}
|
|
2216
2748
|
}
|
|
2749
|
+
/** @deprecated Use {@link OnConfirmMove}. */
|
|
2750
|
+
async onConfirmMove() {
|
|
2751
|
+
return this.OnConfirmMove();
|
|
2752
|
+
}
|
|
2217
2753
|
/**
|
|
2218
2754
|
* Constructs the full path for an item
|
|
2219
2755
|
*/
|
|
2220
2756
|
constructItemPath(item) {
|
|
2221
2757
|
// Validate that we have either key or name
|
|
2222
|
-
if (!item.
|
|
2758
|
+
if (!item.Key && !item.name) {
|
|
2223
2759
|
console.error('[FileGrid] Cannot construct path - item has no key or name:', item);
|
|
2224
2760
|
throw new Error('Cannot construct path for item without key or name');
|
|
2225
2761
|
}
|
|
2226
2762
|
// If item.key exists and is at root level, use it directly
|
|
2227
|
-
if (!this.
|
|
2228
|
-
return item.
|
|
2763
|
+
if (!this.FolderPath || this.FolderPath === '/') {
|
|
2764
|
+
return item.Key || item.name;
|
|
2229
2765
|
}
|
|
2230
2766
|
// If the item key already includes the folder path, use it as is
|
|
2231
|
-
if (item.
|
|
2232
|
-
return item.
|
|
2767
|
+
if (item.Key && item.Key.startsWith(this.FolderPath)) {
|
|
2768
|
+
return item.Key;
|
|
2233
2769
|
}
|
|
2234
2770
|
// Otherwise, combine folder path with item name
|
|
2235
|
-
const normalizedFolderPath = this.
|
|
2236
|
-
? this.
|
|
2237
|
-
: this.
|
|
2771
|
+
const normalizedFolderPath = this.FolderPath.endsWith('/')
|
|
2772
|
+
? this.FolderPath.slice(0, -1)
|
|
2773
|
+
: this.FolderPath;
|
|
2238
2774
|
// Make sure we have a name to use
|
|
2239
|
-
const itemName = item.name || item.
|
|
2775
|
+
const itemName = item.name || item.Key;
|
|
2240
2776
|
if (!itemName) {
|
|
2241
2777
|
console.error('[FileGrid] Cannot construct path - item has no usable name:', item);
|
|
2242
2778
|
throw new Error('Cannot construct path for item without name');
|
|
@@ -2246,96 +2782,116 @@ export class FileGridComponent {
|
|
|
2246
2782
|
/**
|
|
2247
2783
|
* Gets the currently selected item (if exactly one is selected)
|
|
2248
2784
|
*/
|
|
2249
|
-
|
|
2250
|
-
if (this.
|
|
2785
|
+
GetSelectedItem() {
|
|
2786
|
+
if (this.SelectedItems.length !== 1) {
|
|
2251
2787
|
return null;
|
|
2252
2788
|
}
|
|
2253
|
-
return this.
|
|
2789
|
+
return this.Items.find(item => item.Key === this.SelectedItems[0]) || null;
|
|
2790
|
+
}
|
|
2791
|
+
/** @deprecated Use {@link GetSelectedItem}. */
|
|
2792
|
+
getSelectedItem() {
|
|
2793
|
+
return this.GetSelectedItem();
|
|
2254
2794
|
}
|
|
2255
2795
|
/**
|
|
2256
2796
|
* Opens the copy to account dialog
|
|
2257
2797
|
*/
|
|
2258
|
-
async
|
|
2259
|
-
const item = this.
|
|
2260
|
-
if (!item || item.
|
|
2798
|
+
async OnCopyToAccountClick() {
|
|
2799
|
+
const item = this.GetSelectedItem();
|
|
2800
|
+
if (!item || item.Type === 'folder') {
|
|
2261
2801
|
return;
|
|
2262
2802
|
}
|
|
2263
2803
|
try {
|
|
2264
2804
|
const engine = FileStorageEngineBase.Instance;
|
|
2265
2805
|
await engine.Config(false); // Use cached data if available
|
|
2266
2806
|
// Build available accounts (excluding current account)
|
|
2267
|
-
this.
|
|
2268
|
-
.filter(a => !UUIDsEqual(a.account.ID, this.
|
|
2269
|
-
if (this.
|
|
2807
|
+
this.AvailableAccounts = engine.AccountsWithProviders
|
|
2808
|
+
.filter(a => !UUIDsEqual(a.account.ID, this.Account?.account.ID));
|
|
2809
|
+
if (this.AvailableAccounts.length === 0) {
|
|
2270
2810
|
this.notifications.CreateSimpleNotification('No other storage accounts available', 'warning');
|
|
2271
2811
|
return;
|
|
2272
2812
|
}
|
|
2273
|
-
this.
|
|
2274
|
-
this.
|
|
2275
|
-
this.
|
|
2276
|
-
this.
|
|
2277
|
-
this.
|
|
2813
|
+
this.ItemToCopyToProvider = item;
|
|
2814
|
+
this.CopyToAccountDestinationPath = item.name; // Default to same filename
|
|
2815
|
+
this.SelectedDestinationAccounts.clear();
|
|
2816
|
+
this.CopyToAccountProgress = null;
|
|
2817
|
+
this.ShowCopyToProviderDialog = true;
|
|
2278
2818
|
}
|
|
2279
2819
|
catch (error) {
|
|
2280
2820
|
console.error('[FileGrid] Error loading accounts:', error);
|
|
2281
2821
|
this.notifications.CreateSimpleNotification('Failed to load storage accounts', 'error');
|
|
2282
2822
|
}
|
|
2283
2823
|
}
|
|
2824
|
+
/** @deprecated Use {@link OnCopyToAccountClick}. */
|
|
2825
|
+
async onCopyToAccountClick() {
|
|
2826
|
+
return this.OnCopyToAccountClick();
|
|
2827
|
+
}
|
|
2284
2828
|
/**
|
|
2285
2829
|
* Cancels the copy to account dialog
|
|
2286
2830
|
*/
|
|
2831
|
+
OnCancelCopyToAccount() {
|
|
2832
|
+
this.ShowCopyToProviderDialog = false;
|
|
2833
|
+
this.ItemToCopyToProvider = null;
|
|
2834
|
+
this.SelectedDestinationAccounts.clear();
|
|
2835
|
+
this.CopyToAccountDestinationPath = '';
|
|
2836
|
+
this.CopyToAccountProgress = null;
|
|
2837
|
+
}
|
|
2838
|
+
/** @deprecated Use {@link OnCancelCopyToAccount}. */
|
|
2287
2839
|
onCancelCopyToAccount() {
|
|
2288
|
-
this.
|
|
2289
|
-
this.itemToCopyToProvider = null;
|
|
2290
|
-
this.selectedDestinationAccounts.clear();
|
|
2291
|
-
this.copyToAccountDestinationPath = '';
|
|
2292
|
-
this.copyToAccountProgress = null;
|
|
2840
|
+
return this.OnCancelCopyToAccount();
|
|
2293
2841
|
}
|
|
2294
2842
|
/**
|
|
2295
2843
|
* Toggles selection of a destination account for copying
|
|
2296
2844
|
*/
|
|
2297
|
-
|
|
2298
|
-
if (this.
|
|
2299
|
-
this.
|
|
2845
|
+
ToggleDestinationAccount(accountId) {
|
|
2846
|
+
if (this.SelectedDestinationAccounts.has(accountId)) {
|
|
2847
|
+
this.SelectedDestinationAccounts.delete(accountId);
|
|
2300
2848
|
}
|
|
2301
2849
|
else {
|
|
2302
|
-
this.
|
|
2850
|
+
this.SelectedDestinationAccounts.add(accountId);
|
|
2303
2851
|
}
|
|
2304
2852
|
}
|
|
2853
|
+
/** @deprecated Use {@link ToggleDestinationAccount}. */
|
|
2854
|
+
toggleDestinationAccount(accountId) {
|
|
2855
|
+
return this.ToggleDestinationAccount(accountId);
|
|
2856
|
+
}
|
|
2305
2857
|
/**
|
|
2306
2858
|
* Checks if an account is selected as a destination
|
|
2307
2859
|
*/
|
|
2860
|
+
IsDestinationAccountSelected(accountId) {
|
|
2861
|
+
return this.SelectedDestinationAccounts.has(accountId);
|
|
2862
|
+
}
|
|
2863
|
+
/** @deprecated Use {@link IsDestinationAccountSelected}. */
|
|
2308
2864
|
isDestinationAccountSelected(accountId) {
|
|
2309
|
-
return this.
|
|
2865
|
+
return this.IsDestinationAccountSelected(accountId);
|
|
2310
2866
|
}
|
|
2311
2867
|
/**
|
|
2312
2868
|
* Executes the cross-account copy to multiple selected accounts
|
|
2313
2869
|
*/
|
|
2314
|
-
async
|
|
2315
|
-
if (!this.
|
|
2870
|
+
async OnConfirmCopyToAccount() {
|
|
2871
|
+
if (!this.ItemToCopyToProvider || this.SelectedDestinationAccounts.size === 0 || !this.Account) {
|
|
2316
2872
|
return;
|
|
2317
2873
|
}
|
|
2318
|
-
this.
|
|
2874
|
+
this.IsCopyingToAccount = true;
|
|
2319
2875
|
// Get selected accounts
|
|
2320
|
-
const selectedAccounts = this.
|
|
2876
|
+
const selectedAccounts = this.AvailableAccounts.filter(a => this.SelectedDestinationAccounts.has(a.account.ID));
|
|
2321
2877
|
const successfulCopies = [];
|
|
2322
2878
|
const failedCopies = [];
|
|
2323
2879
|
try {
|
|
2324
2880
|
// Construct source path and normalize it
|
|
2325
|
-
const rawSourcePath = this.constructItemPath(this.
|
|
2881
|
+
const rawSourcePath = this.constructItemPath(this.ItemToCopyToProvider);
|
|
2326
2882
|
const sourcePath = rawSourcePath.replace(/^\/+|\/+$/g, '').replace(/\/+/g, '/');
|
|
2327
2883
|
console.log('[FileGrid] Cross-account copy sourcePath:', { raw: rawSourcePath, normalized: sourcePath });
|
|
2328
2884
|
// Copy to each selected account
|
|
2329
2885
|
for (let i = 0; i < selectedAccounts.length; i++) {
|
|
2330
2886
|
const destAccount = selectedAccounts[i];
|
|
2331
|
-
this.
|
|
2887
|
+
this.CopyToAccountProgress = {
|
|
2332
2888
|
current: i + 1,
|
|
2333
2889
|
total: selectedAccounts.length,
|
|
2334
2890
|
currentAccount: destAccount.account.Name
|
|
2335
2891
|
};
|
|
2336
2892
|
try {
|
|
2337
2893
|
console.log('[FileGrid] Copying to account:', destAccount.account.Name);
|
|
2338
|
-
const copyResult = await this.storageClient.CopyObjectBetweenAccounts(this.
|
|
2894
|
+
const copyResult = await this.storageClient.CopyObjectBetweenAccounts(this.Account.account.ID, destAccount.account.ID, sourcePath, this.CopyToAccountDestinationPath);
|
|
2339
2895
|
if (copyResult.success) {
|
|
2340
2896
|
successfulCopies.push(destAccount.account.Name);
|
|
2341
2897
|
}
|
|
@@ -2359,11 +2915,11 @@ export class FileGridComponent {
|
|
|
2359
2915
|
this.notifications.CreateSimpleNotification(`Copied to ${successfulCopies.length} account(s). Failed: ${failedCopies.map(f => f.account).join(', ')}`, 'warning');
|
|
2360
2916
|
}
|
|
2361
2917
|
// Close dialog
|
|
2362
|
-
this.
|
|
2363
|
-
this.
|
|
2364
|
-
this.
|
|
2365
|
-
this.
|
|
2366
|
-
this.
|
|
2918
|
+
this.ShowCopyToProviderDialog = false;
|
|
2919
|
+
this.ItemToCopyToProvider = null;
|
|
2920
|
+
this.SelectedDestinationAccounts.clear();
|
|
2921
|
+
this.CopyToAccountDestinationPath = '';
|
|
2922
|
+
this.CopyToAccountProgress = null;
|
|
2367
2923
|
}
|
|
2368
2924
|
catch (error) {
|
|
2369
2925
|
console.error('[FileGrid] Error copying to accounts:', error);
|
|
@@ -2371,34 +2927,42 @@ export class FileGridComponent {
|
|
|
2371
2927
|
this.notifications.CreateSimpleNotification(`Copy failed: ${errorMessage}`, 'error');
|
|
2372
2928
|
}
|
|
2373
2929
|
finally {
|
|
2374
|
-
this.
|
|
2375
|
-
this.
|
|
2930
|
+
this.IsCopyingToAccount = false;
|
|
2931
|
+
this.CopyToAccountProgress = null;
|
|
2376
2932
|
}
|
|
2377
2933
|
}
|
|
2934
|
+
/** @deprecated Use {@link OnConfirmCopyToAccount}. */
|
|
2935
|
+
async onConfirmCopyToAccount() {
|
|
2936
|
+
return this.OnConfirmCopyToAccount();
|
|
2937
|
+
}
|
|
2378
2938
|
// ==========================================
|
|
2379
2939
|
// Multi-Account Search Methods
|
|
2380
2940
|
// ==========================================
|
|
2381
2941
|
/**
|
|
2382
2942
|
* Toggles multi-account search mode
|
|
2383
2943
|
*/
|
|
2384
|
-
|
|
2385
|
-
this.
|
|
2386
|
-
if (this.
|
|
2944
|
+
ToggleMultiAccountSearchMode() {
|
|
2945
|
+
this.IsMultiProviderSearchMode = !this.IsMultiProviderSearchMode;
|
|
2946
|
+
if (this.IsMultiProviderSearchMode) {
|
|
2387
2947
|
// Load available accounts if not already loaded
|
|
2388
|
-
if (this.
|
|
2948
|
+
if (this.AvailableAccounts.length === 0) {
|
|
2389
2949
|
this.loadAvailableAccountsForSearch();
|
|
2390
2950
|
}
|
|
2391
2951
|
// Pre-select current account if available
|
|
2392
|
-
if (this.
|
|
2393
|
-
this.
|
|
2952
|
+
if (this.Account) {
|
|
2953
|
+
this.SelectedSearchProviders.add(this.Account.account.ID);
|
|
2394
2954
|
}
|
|
2395
2955
|
}
|
|
2396
2956
|
else {
|
|
2397
2957
|
// Clear search results when exiting search mode
|
|
2398
|
-
this.
|
|
2399
|
-
this.
|
|
2958
|
+
this.MultiProviderSearchResults = null;
|
|
2959
|
+
this.MultiProviderSearchQuery = '';
|
|
2400
2960
|
}
|
|
2401
2961
|
}
|
|
2962
|
+
/** @deprecated Use {@link ToggleMultiAccountSearchMode}. */
|
|
2963
|
+
toggleMultiAccountSearchMode() {
|
|
2964
|
+
return this.ToggleMultiAccountSearchMode();
|
|
2965
|
+
}
|
|
2402
2966
|
/**
|
|
2403
2967
|
* Loads available accounts for search selection
|
|
2404
2968
|
*/
|
|
@@ -2406,7 +2970,7 @@ export class FileGridComponent {
|
|
|
2406
2970
|
try {
|
|
2407
2971
|
const engine = FileStorageEngineBase.Instance;
|
|
2408
2972
|
await engine.Config(false); // Use cached data if available
|
|
2409
|
-
this.
|
|
2973
|
+
this.AvailableAccounts = engine.AccountsWithProviders;
|
|
2410
2974
|
}
|
|
2411
2975
|
catch (error) {
|
|
2412
2976
|
console.error('[FileGrid] Error loading accounts for search:', error);
|
|
@@ -2415,45 +2979,57 @@ export class FileGridComponent {
|
|
|
2415
2979
|
/**
|
|
2416
2980
|
* Toggles account selection for search
|
|
2417
2981
|
*/
|
|
2418
|
-
|
|
2419
|
-
if (this.
|
|
2420
|
-
this.
|
|
2982
|
+
ToggleSearchAccount(accountID) {
|
|
2983
|
+
if (this.SelectedSearchProviders.has(accountID)) {
|
|
2984
|
+
this.SelectedSearchProviders.delete(accountID);
|
|
2421
2985
|
}
|
|
2422
2986
|
else {
|
|
2423
|
-
this.
|
|
2987
|
+
this.SelectedSearchProviders.add(accountID);
|
|
2424
2988
|
}
|
|
2425
2989
|
}
|
|
2990
|
+
/** @deprecated Use {@link ToggleSearchAccount}. */
|
|
2991
|
+
toggleSearchAccount(accountID) {
|
|
2992
|
+
return this.ToggleSearchAccount(accountID);
|
|
2993
|
+
}
|
|
2426
2994
|
/**
|
|
2427
2995
|
* Checks if an account is selected for search
|
|
2428
2996
|
*/
|
|
2997
|
+
IsAccountSelectedForSearch(accountID) {
|
|
2998
|
+
return this.SelectedSearchProviders.has(accountID);
|
|
2999
|
+
}
|
|
3000
|
+
/** @deprecated Use {@link IsAccountSelectedForSearch}. */
|
|
2429
3001
|
isAccountSelectedForSearch(accountID) {
|
|
2430
|
-
return this.
|
|
3002
|
+
return this.IsAccountSelectedForSearch(accountID);
|
|
2431
3003
|
}
|
|
2432
3004
|
/**
|
|
2433
3005
|
* Checks if an account's provider supports search
|
|
2434
3006
|
*/
|
|
2435
|
-
|
|
3007
|
+
AccountSupportsSearch(accountWithProvider) {
|
|
2436
3008
|
return accountWithProvider.provider.SupportsSearch === true;
|
|
2437
3009
|
}
|
|
3010
|
+
/** @deprecated Use {@link AccountSupportsSearch}. */
|
|
3011
|
+
accountSupportsSearch(accountWithProvider) {
|
|
3012
|
+
return this.AccountSupportsSearch(accountWithProvider);
|
|
3013
|
+
}
|
|
2438
3014
|
/**
|
|
2439
3015
|
* Executes multi-account search
|
|
2440
3016
|
*/
|
|
2441
|
-
async
|
|
2442
|
-
if (!this.
|
|
3017
|
+
async ExecuteMultiAccountSearch() {
|
|
3018
|
+
if (!this.MultiProviderSearchQuery.trim() || this.SelectedSearchProviders.size === 0) {
|
|
2443
3019
|
return;
|
|
2444
3020
|
}
|
|
2445
|
-
this.
|
|
2446
|
-
this.
|
|
3021
|
+
this.IsSearching = true;
|
|
3022
|
+
this.MultiProviderSearchResults = null;
|
|
2447
3023
|
try {
|
|
2448
3024
|
console.log('[FileGrid] Executing multi-account search:', {
|
|
2449
|
-
accountIds: Array.from(this.
|
|
2450
|
-
query: this.
|
|
3025
|
+
accountIds: Array.from(this.SelectedSearchProviders),
|
|
3026
|
+
query: this.MultiProviderSearchQuery
|
|
2451
3027
|
});
|
|
2452
|
-
const searchResult = await this.storageClient.SearchFiles(Array.from(this.
|
|
3028
|
+
const searchResult = await this.storageClient.SearchFiles(Array.from(this.SelectedSearchProviders), this.MultiProviderSearchQuery, { maxResultsPerAccount: 50 });
|
|
2453
3029
|
console.log('[FileGrid] Multi-account search result:', searchResult);
|
|
2454
3030
|
// Map the client result to the component's expected format
|
|
2455
|
-
this.
|
|
2456
|
-
|
|
3031
|
+
this.MultiProviderSearchResults = {
|
|
3032
|
+
AccountResults: searchResult.accountResults.map((ar) => ({
|
|
2457
3033
|
accountID: ar.accountId,
|
|
2458
3034
|
accountName: ar.accountName,
|
|
2459
3035
|
success: ar.success,
|
|
@@ -2473,9 +3049,9 @@ export class FileGridComponent {
|
|
|
2473
3049
|
hasMore: ar.hasMore,
|
|
2474
3050
|
nextPageToken: ar.nextPageToken
|
|
2475
3051
|
})),
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
3052
|
+
TotalResultsReturned: searchResult.totalResultsReturned,
|
|
3053
|
+
SuccessfulAccounts: searchResult.successfulAccounts,
|
|
3054
|
+
FailedAccounts: searchResult.failedAccounts
|
|
2479
3055
|
};
|
|
2480
3056
|
}
|
|
2481
3057
|
catch (error) {
|
|
@@ -2488,20 +3064,28 @@ export class FileGridComponent {
|
|
|
2488
3064
|
}, 5000);
|
|
2489
3065
|
}
|
|
2490
3066
|
finally {
|
|
2491
|
-
this.
|
|
3067
|
+
this.IsSearching = false;
|
|
2492
3068
|
}
|
|
2493
3069
|
}
|
|
3070
|
+
/** @deprecated Use {@link ExecuteMultiAccountSearch}. */
|
|
3071
|
+
async executeMultiAccountSearch() {
|
|
3072
|
+
return this.ExecuteMultiAccountSearch();
|
|
3073
|
+
}
|
|
2494
3074
|
/**
|
|
2495
3075
|
* Clears multi-provider search results
|
|
2496
3076
|
*/
|
|
3077
|
+
ClearMultiProviderSearch() {
|
|
3078
|
+
this.MultiProviderSearchQuery = '';
|
|
3079
|
+
this.MultiProviderSearchResults = null;
|
|
3080
|
+
}
|
|
3081
|
+
/** @deprecated Use {@link ClearMultiProviderSearch}. */
|
|
2497
3082
|
clearMultiProviderSearch() {
|
|
2498
|
-
this.
|
|
2499
|
-
this.multiProviderSearchResults = null;
|
|
3083
|
+
return this.ClearMultiProviderSearch();
|
|
2500
3084
|
}
|
|
2501
3085
|
/**
|
|
2502
3086
|
* Gets icon for a search result based on content type
|
|
2503
3087
|
*/
|
|
2504
|
-
|
|
3088
|
+
GetSearchResultIcon(result) {
|
|
2505
3089
|
const extension = result.name.split('.').pop()?.toLowerCase() || '';
|
|
2506
3090
|
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'svg'].includes(extension)) {
|
|
2507
3091
|
return 'fa-solid fa-file-image';
|
|
@@ -2531,24 +3115,36 @@ export class FileGridComponent {
|
|
|
2531
3115
|
return 'fa-solid fa-file';
|
|
2532
3116
|
}
|
|
2533
3117
|
}
|
|
3118
|
+
/** @deprecated Use {@link GetSearchResultIcon}. */
|
|
3119
|
+
getSearchResultIcon(result) {
|
|
3120
|
+
return this.GetSearchResultIcon(result);
|
|
3121
|
+
}
|
|
2534
3122
|
/**
|
|
2535
3123
|
* Formats file size for search results
|
|
2536
3124
|
*/
|
|
3125
|
+
FormatSearchResultSize(bytes) {
|
|
3126
|
+
return this.FormatFileSize(bytes);
|
|
3127
|
+
}
|
|
3128
|
+
/** @deprecated Use {@link FormatSearchResultSize}. */
|
|
2537
3129
|
formatSearchResultSize(bytes) {
|
|
2538
|
-
return this.
|
|
3130
|
+
return this.FormatSearchResultSize(bytes);
|
|
2539
3131
|
}
|
|
2540
3132
|
/**
|
|
2541
3133
|
* Formats date for search results
|
|
2542
3134
|
*/
|
|
2543
|
-
|
|
3135
|
+
FormatSearchResultDate(dateStr) {
|
|
2544
3136
|
return new Date(dateStr).toLocaleString();
|
|
2545
3137
|
}
|
|
3138
|
+
/** @deprecated Use {@link FormatSearchResultDate}. */
|
|
3139
|
+
formatSearchResultDate(dateStr) {
|
|
3140
|
+
return this.FormatSearchResultDate(dateStr);
|
|
3141
|
+
}
|
|
2546
3142
|
/**
|
|
2547
3143
|
* Determines media category for a file item
|
|
2548
3144
|
*/
|
|
2549
|
-
|
|
3145
|
+
GetMediaType(item) {
|
|
2550
3146
|
const ext = item.name.split('.').pop()?.toLowerCase() || '';
|
|
2551
|
-
const mime = item.
|
|
3147
|
+
const mime = item.ContentType?.toLowerCase() || '';
|
|
2552
3148
|
if (mime.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico'].includes(ext)) {
|
|
2553
3149
|
return 'image';
|
|
2554
3150
|
}
|
|
@@ -2572,21 +3168,29 @@ export class FileGridComponent {
|
|
|
2572
3168
|
}
|
|
2573
3169
|
return 'unknown';
|
|
2574
3170
|
}
|
|
3171
|
+
/** @deprecated Use {@link GetMediaType}. */
|
|
3172
|
+
getMediaType(item) {
|
|
3173
|
+
return this.GetMediaType(item);
|
|
3174
|
+
}
|
|
2575
3175
|
/**
|
|
2576
3176
|
* Gets specific badge class for media type
|
|
2577
3177
|
*/
|
|
2578
|
-
|
|
2579
|
-
if (item.
|
|
3178
|
+
GetItemColorClass(item) {
|
|
3179
|
+
if (item.Type === 'folder') {
|
|
2580
3180
|
return 'mj-file-thumb--folder';
|
|
2581
3181
|
}
|
|
2582
|
-
const mediaType = this.
|
|
3182
|
+
const mediaType = this.GetMediaType(item);
|
|
2583
3183
|
return `mj-file-thumb--${mediaType}`;
|
|
2584
3184
|
}
|
|
3185
|
+
/** @deprecated Use {@link GetItemColorClass}. */
|
|
3186
|
+
getItemColorClass(item) {
|
|
3187
|
+
return this.GetItemColorClass(item);
|
|
3188
|
+
}
|
|
2585
3189
|
/**
|
|
2586
3190
|
* Generates clickable path breadcrumbs
|
|
2587
3191
|
*/
|
|
2588
|
-
|
|
2589
|
-
const raw = (this.
|
|
3192
|
+
GetPathBreadcrumbs() {
|
|
3193
|
+
const raw = (this.FolderPath || '').trim();
|
|
2590
3194
|
if (!raw || raw === '/') {
|
|
2591
3195
|
return [{ name: 'Root', path: '/' }];
|
|
2592
3196
|
}
|
|
@@ -2602,87 +3206,119 @@ export class FileGridComponent {
|
|
|
2602
3206
|
}
|
|
2603
3207
|
return crumbs;
|
|
2604
3208
|
}
|
|
3209
|
+
/** @deprecated Use {@link GetPathBreadcrumbs}. */
|
|
3210
|
+
getPathBreadcrumbs() {
|
|
3211
|
+
return this.GetPathBreadcrumbs();
|
|
3212
|
+
}
|
|
2605
3213
|
/**
|
|
2606
3214
|
* Navigates to a specific breadcrumb path
|
|
2607
3215
|
*/
|
|
3216
|
+
NavigateToBreadcrumb(path) {
|
|
3217
|
+
this.FolderNavigate.emit(path);
|
|
3218
|
+
}
|
|
3219
|
+
/** @deprecated Use {@link NavigateToBreadcrumb}. */
|
|
2608
3220
|
navigateToBreadcrumb(path) {
|
|
2609
|
-
this.
|
|
3221
|
+
return this.NavigateToBreadcrumb(path);
|
|
2610
3222
|
}
|
|
2611
3223
|
/**
|
|
2612
3224
|
* Toggles selection of a file/folder item
|
|
2613
3225
|
*/
|
|
2614
|
-
|
|
3226
|
+
ToggleItemSelection(item, event) {
|
|
2615
3227
|
if (event) {
|
|
2616
3228
|
event.stopPropagation();
|
|
2617
3229
|
}
|
|
2618
|
-
const idx = this.
|
|
3230
|
+
const idx = this.SelectedItems.indexOf(item.Key);
|
|
2619
3231
|
if (idx >= 0) {
|
|
2620
|
-
this.
|
|
3232
|
+
this.SelectedItems = this.SelectedItems.filter((k) => k !== item.Key);
|
|
2621
3233
|
}
|
|
2622
3234
|
else {
|
|
2623
|
-
this.
|
|
3235
|
+
this.SelectedItems = [...this.SelectedItems, item.Key];
|
|
2624
3236
|
}
|
|
2625
3237
|
this.cdr.markForCheck();
|
|
2626
3238
|
}
|
|
3239
|
+
/** @deprecated Use {@link ToggleItemSelection}. */
|
|
3240
|
+
toggleItemSelection(item, event) {
|
|
3241
|
+
return this.ToggleItemSelection(item, event);
|
|
3242
|
+
}
|
|
2627
3243
|
/**
|
|
2628
3244
|
* Selects all current filtered items
|
|
2629
3245
|
*/
|
|
2630
|
-
|
|
2631
|
-
this.
|
|
3246
|
+
SelectAll() {
|
|
3247
|
+
this.SelectedItems = this.FilteredItems.map((i) => i.Key);
|
|
2632
3248
|
this.cdr.markForCheck();
|
|
2633
3249
|
}
|
|
3250
|
+
/** @deprecated Use {@link SelectAll}. */
|
|
3251
|
+
selectAll() {
|
|
3252
|
+
return this.SelectAll();
|
|
3253
|
+
}
|
|
2634
3254
|
/**
|
|
2635
3255
|
* Clears selection
|
|
2636
3256
|
*/
|
|
2637
|
-
|
|
2638
|
-
this.
|
|
3257
|
+
DeselectAll() {
|
|
3258
|
+
this.SelectedItems = [];
|
|
2639
3259
|
this.cdr.markForCheck();
|
|
2640
3260
|
}
|
|
3261
|
+
/** @deprecated Use {@link DeselectAll}. */
|
|
3262
|
+
deselectAll() {
|
|
3263
|
+
return this.DeselectAll();
|
|
3264
|
+
}
|
|
2641
3265
|
/**
|
|
2642
3266
|
* Checks if all items are selected
|
|
2643
3267
|
*/
|
|
3268
|
+
AreAllSelected() {
|
|
3269
|
+
return (this.FilteredItems.length > 0 &&
|
|
3270
|
+
this.SelectedItems.length === this.FilteredItems.length);
|
|
3271
|
+
}
|
|
3272
|
+
/** @deprecated Use {@link AreAllSelected}. */
|
|
2644
3273
|
areAllSelected() {
|
|
2645
|
-
return
|
|
2646
|
-
this.selectedItems.length === this.filteredItems.length);
|
|
3274
|
+
return this.AreAllSelected();
|
|
2647
3275
|
}
|
|
2648
3276
|
/**
|
|
2649
3277
|
* Opens in-browser preview modal
|
|
2650
3278
|
*/
|
|
2651
|
-
async
|
|
2652
|
-
if (item.
|
|
3279
|
+
async OpenPreview(item) {
|
|
3280
|
+
if (item.Type === 'folder' || !this.Account) {
|
|
2653
3281
|
return;
|
|
2654
3282
|
}
|
|
2655
|
-
this.
|
|
2656
|
-
this.
|
|
2657
|
-
this.
|
|
2658
|
-
this.
|
|
2659
|
-
this.
|
|
3283
|
+
this.PreviewItem = item;
|
|
3284
|
+
this.PreviewMediaType = this.GetMediaType(item);
|
|
3285
|
+
this.ShowPreviewModal = true;
|
|
3286
|
+
this.IsLoadingPreview = true;
|
|
3287
|
+
this.PreviewUrl = null;
|
|
2660
3288
|
this.cdr.markForCheck();
|
|
2661
3289
|
try {
|
|
2662
3290
|
const itemPath = this.constructItemPath(item);
|
|
2663
|
-
const url = await this.storageClient.CreatePreAuthDownloadUrl(this.
|
|
2664
|
-
this.
|
|
3291
|
+
const url = await this.storageClient.CreatePreAuthDownloadUrl(this.Account.account.ID, itemPath);
|
|
3292
|
+
this.PreviewUrl = url;
|
|
2665
3293
|
}
|
|
2666
3294
|
catch (err) {
|
|
2667
3295
|
console.error('[FileGrid] Preview error:', err);
|
|
2668
3296
|
}
|
|
2669
3297
|
finally {
|
|
2670
|
-
this.
|
|
3298
|
+
this.IsLoadingPreview = false;
|
|
2671
3299
|
this.cdr.markForCheck();
|
|
2672
3300
|
}
|
|
2673
3301
|
}
|
|
3302
|
+
/** @deprecated Use {@link OpenPreview}. */
|
|
3303
|
+
async openPreview(item) {
|
|
3304
|
+
return this.OpenPreview(item);
|
|
3305
|
+
}
|
|
2674
3306
|
/**
|
|
2675
3307
|
* Closes the preview modal
|
|
2676
3308
|
*/
|
|
2677
|
-
|
|
2678
|
-
this.
|
|
2679
|
-
this.
|
|
2680
|
-
this.
|
|
2681
|
-
this.
|
|
3309
|
+
ClosePreview() {
|
|
3310
|
+
this.ShowPreviewModal = false;
|
|
3311
|
+
this.PreviewItem = null;
|
|
3312
|
+
this.PreviewUrl = null;
|
|
3313
|
+
this.IsLoadingPreview = false;
|
|
2682
3314
|
this.cdr.markForCheck();
|
|
2683
3315
|
}
|
|
3316
|
+
/** @deprecated Use {@link ClosePreview}. */
|
|
3317
|
+
closePreview() {
|
|
3318
|
+
return this.ClosePreview();
|
|
3319
|
+
}
|
|
2684
3320
|
static ɵfac = function FileGridComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || FileGridComponent)(); };
|
|
2685
|
-
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: FileGridComponent, selectors: [["mj-file-grid"]], inputs: { account: "account", folderPath: "folderPath" }, outputs: { folderNavigate: "folderNavigate", folderStructureChanged: "folderStructureChanged" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 19, vars: 18, consts: [[1, "file-grid-container", 3, "dragenter", "dragover", "dragleave", "drop"], [1, "command-bar", "breadcrumb-bar"], [1, "selection-bar"], [1, "multi-search-card", "multi-provider-search-panel"], [1, "grid-loading-container", "loading-state"], ["Variant", "error", "Icon", "fa-solid fa-triangle-exclamation", "Title", "Unable to load files", "ActionText", "Retry", "ActionIcon", "fa-solid fa-arrows-rotate", 1, "file-grid-empty-fill", 3, "Message"], [1, "file-cards-grid", "file-grid", "file-grid-wrapper"], [1, "file-table-container", "file-table-wrapper"], ["Icon", "fa-solid fa-folder-open", "Title", "This folder is empty", "Message", "Drop files here to upload or create a new folder", "ActionText", "Upload Files", "ActionIcon", "fa-solid fa-cloud-arrow-up"], [1, "status-bar"], [1, "drag-drop-overlay"], [1, "upload-progress-bar-wrap"], [1, "mj-modal-backdrop"], [1, "mj-modal-backdrop", "modal-overlay"], [1, "command-bar-left"], [1, "nav-btn-group", "nav-buttons"], ["type", "button", "title", "Go up to parent folder", 1, "mj-tool-btn", "nav-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-arrow-up"], ["type", "button", "title", "Refresh current folder", 1, "mj-tool-btn", "nav-btn", 3, "click"], [1, "fa-solid", "fa-arrows-rotate"], ["aria-label", "Folder Path", 1, "breadcrumb-trail", "breadcrumb-path"], [1, "fa-solid", "fa-folder-open", "trail-root-icon"], [1, "command-bar-right"], [1, "search-pill", "search-bar"], [1, "fa-solid", "fa-magnifying-glass", "search-icon"], ["type", "text", "placeholder", "Search files...", 1, "search-input", 3, "ngModelChange", "ngModel", "disabled"], ["type", "button", "title", "Clear search", 1, "search-clear-btn"], [1, "type-filter-select", "file-type-filter", 3, "ngModelChange", "ngModel", "disabled"], ["value", "all"], ["value", "files"], ["value", "folders"], ["type", "button", "title", "Search across all storage accounts", 1, "mj-btn-search-toggle", "multi-search-btn", 3, "click"], [1, "fa-solid", "fa-layer-group"], [1, "view-mode-toggle", "view-toggle"], ["type", "button", "title", "List View", 1, "view-mode-btn", "view-btn", 3, "click"], [1, "fa-solid", "fa-list"], ["type", "button", "title", "Grid View", 1, "view-mode-btn", "view-btn", 3, "click"], [1, "fa-solid", "fa-table-cells-large"], ["type", "button", "title", "Create New Folder", 1, "mj-action-btn", "mj-action-btn--secondary", "toolbar-btn", 3, "click"], [1, "fa-solid", "fa-folder-plus"], ["type", "button", "title", "Upload Files", 1, "mj-action-btn", "mj-action-btn--primary", "toolbar-btn", 3, "click"], [1, "fa-solid", "fa-cloud-arrow-up"], ["type", "button", 1, "crumb-btn", 3, "click", "title"], [1, "crumb-separator"], ["type", "button", "title", "Clear search", 1, "search-clear-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "selection-bar-left"], [1, "selection-checkbox-wrap", 3, "click"], ["type", "checkbox", 3, "change", "checked"], [1, "selection-count"], [1, "selection-bar-right"], ["type", "button", "title", "Delete selected item(s)", 1, "sel-action-btn", "sel-action-btn--danger", 3, "click"], [1, "fa-solid", "fa-trash"], ["type", "button", "title", "Deselect all", 1, "sel-action-btn", "sel-action-btn--ghost", 3, "click"], ["type", "button", "title", "Preview file", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-eye"], ["type", "button", "title", "Download file", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-download"], ["type", "button", "title", "Copy to another storage account", 1, "sel-action-btn", 3, "click"], ["type", "button", "title", "Rename item", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-pen"], ["type", "button", "title", "Copy to path", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-copy"], ["type", "button", "title", "Move item", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-arrows-up-down-left-right"], [1, "multi-search-header", "search-panel-header"], [1, "multi-search-title"], ["type", "button", "title", "Close search", 1, "modal-close-btn", "close-panel-btn", 3, "click"], [1, "multi-search-body", "search-panel-body"], [1, "multi-search-bar", "search-input-row"], ["type", "text", "placeholder", "Type query to search across connected accounts...", 1, "multi-search-text", "multi-search-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", "search-execute-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-magnifying-glass"], [1, "multi-search-accounts", "provider-selection"], [1, "account-select-label", "provider-selection-label"], [1, "account-pill-list", "provider-checkboxes"], [1, "account-select-pill", "provider-checkbox", 3, "disabled", "checked"], [1, "multi-search-results", "search-results-container"], [1, "account-select-pill", "provider-checkbox"], ["type", "checkbox", 3, "change", "checked", "disabled"], [1, "pill-account-name", "provider-name"], [1, "pill-provider-tag", "provider-type"], [1, "pill-no-search", "no-search-badge"], [1, "results-meta-bar", "search-results-summary"], [1, "meta-count", "result-count"], [1, "meta-stats", "provider-stats"], ["type", "button", 1, "clear-results-btn", 3, "click"], [1, "results-groups", "provider-results-list"], [1, "result-group", "provider-result-group", 3, "failed"], [1, "result-group", "provider-result-group"], [1, "result-group-head", "provider-result-header"], [1, "fa-solid", "fa-cloud-bolt", "group-icon"], [1, "group-account-name", "provider-name"], [1, "group-count-badge", "result-count"], [1, "group-error-badge", "error-badge"], [1, "result-items-grid", "provider-result-files"], [1, "group-empty-text", "no-results-message"], [1, "search-item-row", "search-result-item"], [1, "search-item-icon", "file-icon"], [1, "search-item-info", "file-info"], [1, "search-item-name", "file-name"], [1, "search-item-path", "file-path"], [1, "search-item-size", "file-size"], [1, "search-item-date", "file-date"], ["text", "Loading files..."], ["Variant", "error", "Icon", "fa-solid fa-triangle-exclamation", "Title", "Unable to load files", "ActionText", "Retry", "ActionIcon", "fa-solid fa-arrows-rotate", 1, "file-grid-empty-fill", 3, "Action", "Message"], [1, "file-card", "grid-item", 3, "selected", "title"], [1, "file-card", "grid-item", 3, "click", "dblclick", "title"], [1, "file-card-header"], [1, "file-card-checkbox", 3, "click"], [1, "file-card-quick-actions", 3, "click"], [1, "file-card-icon-container", "grid-icon"], [1, "file-card-body"], [1, "file-card-name", "grid-name"], [1, "file-card-meta", "grid-meta"], [1, "meta-size"], [1, "meta-date"], ["type", "button", "title", "Preview", 1, "quick-action-btn", 3, "click"], ["type", "button", "title", "Download", 1, "quick-action-btn", 3, "click"], [1, "file-table"], [1, "th-check"], [1, "th-name", "col-name", 3, "click"], [1, "fa-solid", "sort-arrow", "sort-icon"], [1, "th-type", "col-type"], [1, "th-size", "col-size", 3, "click"], [1, "th-date", "col-modified", 3, "click"], [1, "th-actions"], [1, "file-table-row", "file-row", 3, "selected"], [1, "file-table-row", "file-row", 3, "click", "dblclick"], [1, "td-check", 3, "click"], [1, "td-name", "col-name"], [1, "row-name-wrap"], [1, "row-icon-badge"], [1, "item-icon"], [1, "row-file-name", "item-name", 3, "title"], [1, "td-type", "col-type"], [1, "type-badge", "item-type"], [1, "td-size", "col-size"], [1, "size-text", "item-size"], [1, "td-date", "col-modified"], [1, "date-text", "item-date"], [1, "td-actions", 3, "click"], [1, "row-action-buttons"], ["type", "button", "title", "Preview", 1, "row-act-btn", 3, "click"], ["type", "button", "title", "Download", 1, "row-act-btn", 3, "click"], ["Icon", "fa-solid fa-folder-open", "Title", "This folder is empty", "Message", "Drop files here to upload or create a new folder", "ActionText", "Upload Files", "ActionIcon", "fa-solid fa-cloud-arrow-up", 3, "Action"], [1, "status-count"], ["class", "status-account-badge", 4, "ngIf"], [1, "status-account-badge"], [1, "fa-solid", "fa-cloud"], [1, "drag-drop-content"], [1, "fa-solid", "fa-cloud-arrow-up", "drag-icon"], [1, "upload-progress-card"], [1, "upload-info"], [1, "fa-solid", "fa-spinner", "fa-spin", "upload-spinner"], [1, "upload-text"], [1, "progress-track"], [1, "progress-fill"], [1, "mj-modal-backdrop", 3, "click"], [1, "mj-modal-window", "preview-modal-window", 3, "click"], [1, "mj-modal-header"], [1, "modal-title-wrap"], [1, "modal-title"], [1, "modal-subtitle"], [1, "modal-header-actions"], ["type", "button", "title", "Download file", 1, "mj-action-btn", "mj-action-btn--secondary", 3, "click"], ["type", "button", "title", "Close preview", 1, "modal-close-btn", 3, "click"], [1, "preview-modal-body"], [1, "preview-loading"], [1, "preview-fallback"], ["text", "Loading preview..."], [1, "preview-media-wrap", "image-preview-wrap"], [1, "preview-media-wrap", "pdf-preview-wrap"], [1, "preview-media-wrap", "video-preview-wrap"], [1, "preview-media-wrap", "audio-preview-wrap"], [1, "preview-img", 3, "src", "alt"], ["title", "PDF Preview", 1, "preview-iframe", 3, "src"], ["controls", "", "autoplay", "", 1, "preview-video", 3, "src"], [1, "fa-solid", "fa-music", "preview-audio-icon"], ["controls", "", 1, "preview-audio", 3, "src"], [1, "preview-fallback-icon"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", 3, "click"], [1, "fa-solid", "fa-triangle-exclamation", "preview-fallback-icon"], [1, "mj-modal-backdrop", "modal-overlay", 3, "click"], [1, "mj-modal-window", "modal-compact", "modal-dialog", 3, "click"], [1, "mj-modal-header", "modal-header"], ["type", "button", 1, "modal-close-btn", "modal-close", 3, "click"], [1, "mj-modal-body", "modal-body"], [1, "form-label"], ["type", "text", "placeholder", "Enter folder name", "autofocus", "", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-footer", "modal-footer"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--secondary", 3, "click", "disabled"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", 3, "click", "disabled"], [1, "modal-body-text"], [1, "modal-warning-banner", "warning"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--danger", "danger", 3, "click", "disabled"], [1, "fa-solid", "fa-triangle-exclamation"], ["type", "text", "placeholder", "Enter new name", "autofocus", "", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-window", "modal-compact", 3, "click"], ["type", "button", 1, "modal-close-btn", 3, "click"], [1, "mj-modal-body"], ["type", "text", "placeholder", "path/to/destination", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-footer"], ["type", "text", "placeholder", "destination/folder/", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-window", 3, "click"], [1, "account-picker-list"], [1, "account-picker-item", 3, "for"], [1, "form-label", 2, "margin-top", "14px"], ["type", "text", "placeholder", "Filename", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "copy-progress-indicator"], ["type", "checkbox", 3, "change", "id", "checked", "disabled"], [1, "picker-name"], [1, "picker-badge"]], template: function FileGridComponent_Template(rf, ctx) { if (rf & 1) {
|
|
3321
|
+
static ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: FileGridComponent, selectors: [["mj-file-grid"]], inputs: { Account: "Account", account: "account", FolderPath: "FolderPath", folderPath: "folderPath" }, outputs: { FolderNavigate: "FolderNavigate", folderNavigate: "folderNavigate", FolderStructureChanged: "FolderStructureChanged", folderStructureChanged: "folderStructureChanged" }, standalone: false, features: [i0.ɵɵNgOnChangesFeature], decls: 19, vars: 18, consts: [[1, "file-grid-container", 3, "dragenter", "dragover", "dragleave", "drop"], [1, "command-bar", "breadcrumb-bar"], [1, "selection-bar"], [1, "multi-search-card", "multi-provider-search-panel"], [1, "grid-loading-container", "loading-state"], ["Variant", "error", "Icon", "fa-solid fa-triangle-exclamation", "Title", "Unable to load files", "ActionText", "Retry", "ActionIcon", "fa-solid fa-arrows-rotate", 1, "file-grid-empty-fill", 3, "Message"], [1, "file-cards-grid", "file-grid", "file-grid-wrapper"], [1, "file-table-container", "file-table-wrapper"], ["Icon", "fa-solid fa-folder-open", "Title", "This folder is empty", "Message", "Drop files here to upload or create a new folder", "ActionText", "Upload Files", "ActionIcon", "fa-solid fa-cloud-arrow-up"], [1, "status-bar"], [1, "drag-drop-overlay"], [1, "upload-progress-bar-wrap"], [1, "mj-modal-backdrop"], [1, "mj-modal-backdrop", "modal-overlay"], [1, "command-bar-left"], [1, "nav-btn-group", "nav-buttons"], ["type", "button", "title", "Go up to parent folder", 1, "mj-tool-btn", "nav-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-arrow-up"], ["type", "button", "title", "Refresh current folder", 1, "mj-tool-btn", "nav-btn", 3, "click"], [1, "fa-solid", "fa-arrows-rotate"], ["aria-label", "Folder Path", 1, "breadcrumb-trail", "breadcrumb-path"], [1, "fa-solid", "fa-folder-open", "trail-root-icon"], [1, "command-bar-right"], [1, "search-pill", "search-bar"], [1, "fa-solid", "fa-magnifying-glass", "search-icon"], ["type", "text", "placeholder", "Search files...", 1, "search-input", 3, "ngModelChange", "ngModel", "disabled"], ["type", "button", "title", "Clear search", 1, "search-clear-btn"], [1, "type-filter-select", "file-type-filter", 3, "ngModelChange", "ngModel", "disabled"], ["value", "all"], ["value", "files"], ["value", "folders"], ["type", "button", "title", "Search across all storage accounts", 1, "mj-btn-search-toggle", "multi-search-btn", 3, "click"], [1, "fa-solid", "fa-layer-group"], [1, "view-mode-toggle", "view-toggle"], ["type", "button", "title", "List View", 1, "view-mode-btn", "view-btn", 3, "click"], [1, "fa-solid", "fa-list"], ["type", "button", "title", "Grid View", 1, "view-mode-btn", "view-btn", 3, "click"], [1, "fa-solid", "fa-table-cells-large"], ["type", "button", "title", "Create New Folder", 1, "mj-action-btn", "mj-action-btn--secondary", "toolbar-btn", 3, "click"], [1, "fa-solid", "fa-folder-plus"], ["type", "button", "title", "Upload Files", 1, "mj-action-btn", "mj-action-btn--primary", "toolbar-btn", 3, "click"], [1, "fa-solid", "fa-cloud-arrow-up"], ["type", "button", 1, "crumb-btn", 3, "click", "title"], [1, "crumb-separator"], ["type", "button", "title", "Clear search", 1, "search-clear-btn", 3, "click"], [1, "fa-solid", "fa-xmark"], [1, "selection-bar-left"], [1, "selection-checkbox-wrap", 3, "click"], ["type", "checkbox", 3, "change", "checked"], [1, "selection-count"], [1, "selection-bar-right"], ["type", "button", "title", "Delete selected item(s)", 1, "sel-action-btn", "sel-action-btn--danger", 3, "click"], [1, "fa-solid", "fa-trash"], ["type", "button", "title", "Deselect all", 1, "sel-action-btn", "sel-action-btn--ghost", 3, "click"], ["type", "button", "title", "Preview file", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-eye"], ["type", "button", "title", "Download file", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-download"], ["type", "button", "title", "Copy to another storage account", 1, "sel-action-btn", 3, "click"], ["type", "button", "title", "Rename item", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-pen"], ["type", "button", "title", "Copy to path", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-copy"], ["type", "button", "title", "Move item", 1, "sel-action-btn", 3, "click"], [1, "fa-solid", "fa-arrows-up-down-left-right"], [1, "multi-search-header", "search-panel-header"], [1, "multi-search-title"], ["type", "button", "title", "Close search", 1, "modal-close-btn", "close-panel-btn", 3, "click"], [1, "multi-search-body", "search-panel-body"], [1, "multi-search-bar", "search-input-row"], ["type", "text", "placeholder", "Type query to search across connected accounts...", 1, "multi-search-text", "multi-search-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", "search-execute-btn", 3, "click", "disabled"], [1, "fa-solid", "fa-spinner", "fa-spin"], [1, "fa-solid", "fa-magnifying-glass"], [1, "multi-search-accounts", "provider-selection"], [1, "account-select-label", "provider-selection-label"], [1, "account-pill-list", "provider-checkboxes"], [1, "account-select-pill", "provider-checkbox", 3, "disabled", "checked"], [1, "multi-search-results", "search-results-container"], [1, "account-select-pill", "provider-checkbox"], ["type", "checkbox", 3, "change", "checked", "disabled"], [1, "pill-account-name", "provider-name"], [1, "pill-provider-tag", "provider-type"], [1, "pill-no-search", "no-search-badge"], [1, "results-meta-bar", "search-results-summary"], [1, "meta-count", "result-count"], [1, "meta-stats", "provider-stats"], ["type", "button", 1, "clear-results-btn", 3, "click"], [1, "results-groups", "provider-results-list"], [1, "result-group", "provider-result-group", 3, "failed"], [1, "result-group", "provider-result-group"], [1, "result-group-head", "provider-result-header"], [1, "fa-solid", "fa-cloud-bolt", "group-icon"], [1, "group-account-name", "provider-name"], [1, "group-count-badge", "result-count"], [1, "group-error-badge", "error-badge"], [1, "result-items-grid", "provider-result-files"], [1, "group-empty-text", "no-results-message"], [1, "search-item-row", "search-result-item"], [1, "search-item-icon", "file-icon"], [1, "search-item-info", "file-info"], [1, "search-item-name", "file-name"], [1, "search-item-path", "file-path"], [1, "search-item-size", "file-size"], [1, "search-item-date", "file-date"], ["text", "Loading files..."], ["Variant", "error", "Icon", "fa-solid fa-triangle-exclamation", "Title", "Unable to load files", "ActionText", "Retry", "ActionIcon", "fa-solid fa-arrows-rotate", 1, "file-grid-empty-fill", 3, "Action", "Message"], [1, "file-card", "grid-item", 3, "selected", "title"], [1, "file-card", "grid-item", 3, "click", "dblclick", "title"], [1, "file-card-header"], [1, "file-card-checkbox", 3, "click"], [1, "file-card-quick-actions", 3, "click"], [1, "file-card-icon-container", "grid-icon"], [1, "file-card-body"], [1, "file-card-name", "grid-name"], [1, "file-card-meta", "grid-meta"], [1, "meta-size"], [1, "meta-date"], ["type", "button", "title", "Preview", 1, "quick-action-btn", 3, "click"], ["type", "button", "title", "Download", 1, "quick-action-btn", 3, "click"], [1, "file-table"], [1, "th-check"], [1, "th-name", "col-name", 3, "click"], [1, "fa-solid", "sort-arrow", "sort-icon"], [1, "th-type", "col-type"], [1, "th-size", "col-size", 3, "click"], [1, "th-date", "col-modified", 3, "click"], [1, "th-actions"], [1, "file-table-row", "file-row", 3, "selected"], [1, "file-table-row", "file-row", 3, "click", "dblclick"], [1, "td-check", 3, "click"], [1, "td-name", "col-name"], [1, "row-name-wrap"], [1, "row-icon-badge"], [1, "item-icon"], [1, "row-file-name", "item-name", 3, "title"], [1, "td-type", "col-type"], [1, "type-badge", "item-type"], [1, "td-size", "col-size"], [1, "size-text", "item-size"], [1, "td-date", "col-modified"], [1, "date-text", "item-date"], [1, "td-actions", 3, "click"], [1, "row-action-buttons"], ["type", "button", "title", "Preview", 1, "row-act-btn", 3, "click"], ["type", "button", "title", "Download", 1, "row-act-btn", 3, "click"], ["Icon", "fa-solid fa-folder-open", "Title", "This folder is empty", "Message", "Drop files here to upload or create a new folder", "ActionText", "Upload Files", "ActionIcon", "fa-solid fa-cloud-arrow-up", 3, "Action"], [1, "status-count"], ["class", "status-account-badge", 4, "ngIf"], [1, "status-account-badge"], [1, "fa-solid", "fa-cloud"], [1, "drag-drop-content"], [1, "fa-solid", "fa-cloud-arrow-up", "drag-icon"], [1, "upload-progress-card"], [1, "upload-info"], [1, "fa-solid", "fa-spinner", "fa-spin", "upload-spinner"], [1, "upload-text"], [1, "progress-track"], [1, "progress-fill"], [1, "mj-modal-backdrop", 3, "click"], [1, "mj-modal-window", "preview-modal-window", 3, "click"], [1, "mj-modal-header"], [1, "modal-title-wrap"], [1, "modal-title"], [1, "modal-subtitle"], [1, "modal-header-actions"], ["type", "button", "title", "Download file", 1, "mj-action-btn", "mj-action-btn--secondary", 3, "click"], ["type", "button", "title", "Close preview", 1, "modal-close-btn", 3, "click"], [1, "preview-modal-body"], [1, "preview-loading"], [1, "preview-fallback"], ["text", "Loading preview..."], [1, "preview-media-wrap", "image-preview-wrap"], [1, "preview-media-wrap", "pdf-preview-wrap"], [1, "preview-media-wrap", "video-preview-wrap"], [1, "preview-media-wrap", "audio-preview-wrap"], [1, "preview-img", 3, "src", "alt"], ["title", "PDF Preview", 1, "preview-iframe", 3, "src"], ["controls", "", "autoplay", "", 1, "preview-video", 3, "src"], [1, "fa-solid", "fa-music", "preview-audio-icon"], ["controls", "", 1, "preview-audio", 3, "src"], [1, "preview-fallback-icon"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", 3, "click"], [1, "fa-solid", "fa-triangle-exclamation", "preview-fallback-icon"], [1, "mj-modal-backdrop", "modal-overlay", 3, "click"], [1, "mj-modal-window", "modal-compact", "modal-dialog", 3, "click"], [1, "mj-modal-header", "modal-header"], ["type", "button", 1, "modal-close-btn", "modal-close", 3, "click"], [1, "mj-modal-body", "modal-body"], [1, "form-label"], ["type", "text", "placeholder", "Enter folder name", "autofocus", "", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-footer", "modal-footer"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--secondary", 3, "click", "disabled"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--primary", 3, "click", "disabled"], [1, "modal-body-text"], [1, "modal-warning-banner", "warning"], ["type", "button", 1, "mj-action-btn", "mj-action-btn--danger", "danger", 3, "click", "disabled"], [1, "fa-solid", "fa-triangle-exclamation"], ["type", "text", "placeholder", "Enter new name", "autofocus", "", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-window", "modal-compact", 3, "click"], ["type", "button", 1, "modal-close-btn", 3, "click"], [1, "mj-modal-body"], ["type", "text", "placeholder", "path/to/destination", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-footer"], ["type", "text", "placeholder", "destination/folder/", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "mj-modal-window", 3, "click"], [1, "account-picker-list"], [1, "account-picker-item", 3, "for"], [1, "form-label", 2, "margin-top", "14px"], ["type", "text", "placeholder", "Filename", 1, "form-input", 3, "ngModelChange", "keydown.enter", "ngModel", "disabled"], [1, "copy-progress-indicator"], ["type", "checkbox", 3, "change", "id", "checked", "disabled"], [1, "picker-name"], [1, "picker-badge"]], template: function FileGridComponent_Template(rf, ctx) { if (rf & 1) {
|
|
2686
3322
|
i0.ɵɵelementStart(0, "div", 0);
|
|
2687
3323
|
i0.ɵɵlistener("dragenter", function FileGridComponent_Template_div_dragenter_0_listener($event) { return ctx.onDragEnter($event); })("dragover", function FileGridComponent_Template_div_dragover_0_listener($event) { return ctx.onDragOver($event); })("dragleave", function FileGridComponent_Template_div_dragleave_0_listener($event) { return ctx.onDragLeave($event); })("drop", function FileGridComponent_Template_div_drop_0_listener($event) { return ctx.onDrop($event); });
|
|
2688
3324
|
i0.ɵɵconditionalCreate(1, FileGridComponent_Conditional_1_Template, 40, 12, "div", 1);
|
|
@@ -2745,13 +3381,21 @@ export class FileGridComponent {
|
|
|
2745
3381
|
}
|
|
2746
3382
|
(() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FileGridComponent, [{
|
|
2747
3383
|
type: Component,
|
|
2748
|
-
args: [{ standalone: false, selector: 'mj-file-grid', template: "<div class=\"file-grid-container\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n\n <!-- Top Command Bar: Breadcrumbs & Primary Actions -->\n @if (!isLoading && !errorMessage) {\n <div class=\"command-bar breadcrumb-bar\">\n <!-- Left: Breadcrumb Navigation Trail -->\n <div class=\"command-bar-left\">\n <div class=\"nav-btn-group nav-buttons\">\n <button\n type=\"button\"\n class=\"mj-tool-btn nav-btn\"\n [disabled]=\"!canNavigateUp()\"\n (click)=\"navigateUp()\"\n title=\"Go up to parent folder\">\n <i class=\"fa-solid fa-arrow-up\"></i>\n </button>\n <button\n type=\"button\"\n class=\"mj-tool-btn nav-btn\"\n (click)=\"refresh()\"\n title=\"Refresh current folder\">\n <i class=\"fa-solid fa-arrows-rotate\"></i>\n </button>\n </div>\n\n <nav class=\"breadcrumb-trail breadcrumb-path\" aria-label=\"Folder Path\">\n <i class=\"fa-solid fa-folder-open trail-root-icon\"></i>\n @for (crumb of getPathBreadcrumbs(); track crumb.path; let last = $last) {\n <button\n type=\"button\"\n class=\"crumb-btn\"\n [class.current]=\"last\"\n (click)=\"navigateToBreadcrumb(crumb.path)\"\n [title]=\"crumb.name\">\n {{ crumb.name }}\n </button>\n @if (!last) {\n <span class=\"crumb-separator\">/</span>\n }\n }\n </nav>\n </div>\n\n <!-- Right: Search, Filter, View Mode & Action Buttons -->\n <div class=\"command-bar-right\">\n <!-- Search & Filter Bar -->\n <div class=\"search-pill search-bar\">\n <i class=\"fa-solid fa-magnifying-glass search-icon\"></i>\n <input\n type=\"text\"\n class=\"search-input\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange($event)\"\n placeholder=\"Search files...\"\n [disabled]=\"isLoading || isMultiProviderSearchMode\" />\n @if (searchQuery && !isMultiProviderSearchMode) {\n <button\n type=\"button\"\n class=\"search-clear-btn\"\n (click)=\"clearSearch()\"\n title=\"Clear search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n <select\n class=\"type-filter-select file-type-filter\"\n [(ngModel)]=\"fileTypeFilter\"\n (ngModelChange)=\"onFileTypeFilterChange($event)\"\n [disabled]=\"isLoading || isMultiProviderSearchMode\">\n <option value=\"all\">All Items</option>\n <option value=\"files\">Files only</option>\n <option value=\"folders\">Folders only</option>\n </select>\n </div>\n\n <!-- Search Across Accounts Toggle -->\n <button\n type=\"button\"\n class=\"mj-btn-search-toggle multi-search-btn\"\n [class.active]=\"isMultiProviderSearchMode\"\n (click)=\"toggleMultiAccountSearchMode()\"\n title=\"Search across all storage accounts\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>All Accounts</span>\n </button>\n\n <!-- View Switcher -->\n <div class=\"view-mode-toggle view-toggle\">\n <button\n type=\"button\"\n class=\"view-mode-btn view-btn\"\n [class.active]=\"viewMode === 'list'\"\n (click)=\"viewMode = 'list'\"\n title=\"List View\">\n <i class=\"fa-solid fa-list\"></i>\n </button>\n <button\n type=\"button\"\n class=\"view-mode-btn view-btn\"\n [class.active]=\"viewMode === 'grid'\"\n (click)=\"viewMode = 'grid'\"\n title=\"Grid View\">\n <i class=\"fa-solid fa-table-cells-large\"></i>\n </button>\n </div>\n\n <!-- Primary Action CTAs -->\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary toolbar-btn\"\n (click)=\"onNewFolderClick()\"\n title=\"Create New Folder\">\n <i class=\"fa-solid fa-folder-plus\"></i>\n <span>New Folder</span>\n </button>\n\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary toolbar-btn\"\n (click)=\"onUploadClick()\"\n title=\"Upload Files\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Upload</span>\n </button>\n </div>\n </div>\n }\n\n <!-- Selection Action Bar (Appears when 1+ items selected) -->\n @if (selectedItems.length > 0 && !isMultiProviderSearchMode) {\n <div class=\"selection-bar\">\n <div class=\"selection-bar-left\">\n <label class=\"selection-checkbox-wrap\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"areAllSelected()\"\n (change)=\"areAllSelected() ? deselectAll() : selectAll()\" />\n <span class=\"selection-count\">{{ selectedItems.length }} item{{ selectedItems.length === 1 ? '' : 's' }} selected</span>\n </label>\n </div>\n\n <div class=\"selection-bar-right\">\n @if (selectedItems.length === 1 && getSelectedItem()?.type === 'file') {\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"openPreview(getSelectedItem()!)\"\n title=\"Preview file\">\n <i class=\"fa-solid fa-eye\"></i>\n <span>Preview</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onDownloadClick()\"\n title=\"Download file\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onCopyToAccountClick()\"\n title=\"Copy to another storage account\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Copy to...</span>\n </button>\n }\n @if (selectedItems.length === 1) {\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onRenameClick()\"\n title=\"Rename item\">\n <i class=\"fa-solid fa-pen\"></i>\n <span>Rename</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onCopyClick()\"\n title=\"Copy to path\">\n <i class=\"fa-solid fa-copy\"></i>\n <span>Copy</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onMoveClick()\"\n title=\"Move item\">\n <i class=\"fa-solid fa-arrows-up-down-left-right\"></i>\n <span>Move</span>\n </button>\n }\n <button\n type=\"button\"\n class=\"sel-action-btn sel-action-btn--danger\"\n (click)=\"onDeleteClick()\"\n title=\"Delete selected item(s)\">\n <i class=\"fa-solid fa-trash\"></i>\n <span>Delete</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn sel-action-btn--ghost\"\n (click)=\"deselectAll()\"\n title=\"Deselect all\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n }\n\n <!-- Multi-Provider Search Panel -->\n @if (isMultiProviderSearchMode) {\n <div class=\"multi-search-card multi-provider-search-panel\">\n <div class=\"multi-search-header search-panel-header\">\n <div class=\"multi-search-title\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <h3>Search Across Storage Accounts</h3>\n </div>\n <button\n type=\"button\"\n class=\"modal-close-btn close-panel-btn\"\n (click)=\"toggleMultiAccountSearchMode()\"\n title=\"Close search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n\n <div class=\"multi-search-body search-panel-body\">\n <div class=\"multi-search-bar search-input-row\">\n <input\n type=\"text\"\n class=\"multi-search-text multi-search-input\"\n [(ngModel)]=\"multiProviderSearchQuery\"\n placeholder=\"Type query to search across connected accounts...\"\n (keydown.enter)=\"executeMultiAccountSearch()\"\n [disabled]=\"isSearching\" />\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary search-execute-btn\"\n (click)=\"executeMultiAccountSearch()\"\n [disabled]=\"isSearching || !multiProviderSearchQuery.trim() || selectedSearchProviders.size === 0\">\n @if (isSearching) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n }\n <span>{{ isSearching ? 'Searching...' : 'Search Accounts' }}</span>\n </button>\n </div>\n\n <div class=\"multi-search-accounts provider-selection\">\n <span class=\"account-select-label provider-selection-label\">Target Accounts:</span>\n <div class=\"account-pill-list provider-checkboxes\">\n @for (a of availableAccounts; track a.account.ID) {\n <label\n class=\"account-select-pill provider-checkbox\"\n [class.disabled]=\"!accountSupportsSearch(a)\"\n [class.checked]=\"isAccountSelectedForSearch(a.account.ID)\">\n <input\n type=\"checkbox\"\n [checked]=\"isAccountSelectedForSearch(a.account.ID)\"\n (change)=\"toggleSearchAccount(a.account.ID)\"\n [disabled]=\"!accountSupportsSearch(a)\" />\n <span class=\"pill-account-name provider-name\">{{ a.account.Name }}</span>\n <span class=\"pill-provider-tag provider-type\">{{ a.provider.Name }}</span>\n @if (!accountSupportsSearch(a)) {\n <span class=\"pill-no-search no-search-badge\">(no search)</span>\n }\n </label>\n }\n </div>\n </div>\n </div>\n\n <!-- Search Results -->\n @if (multiProviderSearchResults) {\n <div class=\"multi-search-results search-results-container\">\n <div class=\"results-meta-bar search-results-summary\">\n <span class=\"meta-count result-count\">\n <strong>{{ multiProviderSearchResults.totalResultsReturned }}</strong> result{{ multiProviderSearchResults.totalResultsReturned === 1 ? '' : 's' }} found\n </span>\n <span class=\"meta-stats provider-stats\">\n ({{ multiProviderSearchResults.successfulAccounts }} account{{ multiProviderSearchResults.successfulAccounts === 1 ? '' : 's' }} searched)\n </span>\n <button\n type=\"button\"\n class=\"clear-results-btn\"\n (click)=\"clearMultiProviderSearch()\">\n <i class=\"fa-solid fa-xmark\"></i> Clear Results\n </button>\n </div>\n\n <div class=\"results-groups provider-results-list\">\n @for (accountResult of multiProviderSearchResults.accountResults; track accountResult.accountID) {\n <div class=\"result-group provider-result-group\" [class.failed]=\"!accountResult.success\">\n <div class=\"result-group-head provider-result-header\">\n <i class=\"fa-solid fa-cloud-bolt group-icon\"></i>\n <span class=\"group-account-name provider-name\">{{ accountResult.accountName }}</span>\n @if (accountResult.success) {\n <span class=\"group-count-badge result-count\">{{ accountResult.results.length }}</span>\n } @else {\n <span class=\"group-error-badge error-badge\">{{ accountResult.errorMessage }}</span>\n }\n </div>\n\n @if (accountResult.success && accountResult.results.length > 0) {\n <div class=\"result-items-grid provider-result-files\">\n @for (file of accountResult.results; track file.path) {\n <div class=\"search-item-row search-result-item\">\n <i [class]=\"getSearchResultIcon(file)\" class=\"search-item-icon file-icon\"></i>\n <div class=\"search-item-info file-info\">\n <span class=\"search-item-name file-name\">{{ file.name }}</span>\n <span class=\"search-item-path file-path\">{{ file.path }}</span>\n </div>\n <span class=\"search-item-size file-size\">{{ formatSearchResultSize(file.size) }}</span>\n <span class=\"search-item-date file-date\">{{ formatSearchResultDate(file.lastModified) }}</span>\n </div>\n }\n </div>\n }\n @if (accountResult.success && accountResult.results.length === 0) {\n <div class=\"group-empty-text no-results-message\">No matching files found in this account</div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Loading State -->\n @if (isLoading && !isMultiProviderSearchMode) {\n <div class=\"grid-loading-container loading-state\">\n <mj-loading text=\"Loading files...\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (errorMessage && !isLoading) {\n <mj-empty-state\n class=\"file-grid-empty-fill\"\n Variant=\"error\"\n Icon=\"fa-solid fa-triangle-exclamation\"\n Title=\"Unable to load files\"\n [Message]=\"errorMessage\"\n ActionText=\"Retry\"\n ActionIcon=\"fa-solid fa-arrows-rotate\"\n (Action)=\"refresh()\" />\n }\n\n <!-- Grid View -->\n @if (!isLoading && !errorMessage && filteredItems.length > 0 && viewMode === 'grid') {\n <div class=\"file-cards-grid file-grid file-grid-wrapper\">\n @for (item of filteredItems; track item.key) {\n <div\n class=\"file-card grid-item\"\n [class.selected]=\"selectedItems.includes(item.key)\"\n (click)=\"onTileClick(item, $event)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n [title]=\"item.name\">\n\n <!-- Card Top Row: Checkbox & Quick Actions -->\n <div class=\"file-card-header\">\n <label class=\"file-card-checkbox\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedItems.includes(item.key)\"\n (change)=\"toggleItemSelection(item, $event)\" />\n </label>\n <div class=\"file-card-quick-actions\" (click)=\"$event.stopPropagation()\">\n @if (item.type === 'file') {\n <button\n type=\"button\"\n class=\"quick-action-btn\"\n (click)=\"openPreview(item)\"\n title=\"Preview\">\n <i class=\"fa-solid fa-eye\"></i>\n </button>\n <button\n type=\"button\"\n class=\"quick-action-btn\"\n (click)=\"downloadFile(item)\"\n title=\"Download\">\n <i class=\"fa-solid fa-download\"></i>\n </button>\n }\n </div>\n </div>\n\n <!-- Card Thumbnail/Icon -->\n <div class=\"file-card-icon-container grid-icon\" [class]=\"getItemColorClass(item)\">\n <i [class]=\"getItemIcon(item)\"></i>\n </div>\n\n <!-- Card Body: Name & Meta -->\n <div class=\"file-card-body\">\n <span class=\"file-card-name grid-name\">{{ item.name }}</span>\n <div class=\"file-card-meta grid-meta\">\n <span class=\"meta-size\">{{ item.type === 'folder' ? 'Folder' : formatFileSize(item.size) }}</span>\n <span class=\"meta-date\">{{ formatDate(item.lastModified) }}</span>\n </div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- List View Table -->\n @if (!isLoading && !errorMessage && filteredItems.length > 0 && viewMode === 'list') {\n <div class=\"file-table-container file-table-wrapper\">\n <table class=\"file-table\">\n <thead>\n <tr>\n <th class=\"th-check\">\n <input\n type=\"checkbox\"\n [checked]=\"areAllSelected()\"\n (change)=\"areAllSelected() ? deselectAll() : selectAll()\" />\n </th>\n <th class=\"th-name col-name\" (click)=\"onSortChange([{ field: 'name', dir: sort[0].field === 'name' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Name</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'name' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'name' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'name'\"></i>\n </th>\n <th class=\"th-type col-type\">Type</th>\n <th class=\"th-size col-size\" (click)=\"onSortChange([{ field: 'size', dir: sort[0].field === 'size' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Size</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'size' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'size' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'size'\"></i>\n </th>\n <th class=\"th-date col-modified\" (click)=\"onSortChange([{ field: 'lastModified', dir: sort[0].field === 'lastModified' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Modified</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'lastModified' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'lastModified' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'lastModified'\"></i>\n </th>\n <th class=\"th-actions\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (item of filteredItems; track item.key) {\n <tr\n class=\"file-table-row file-row\"\n [class.selected]=\"selectedItems.includes(item.key)\"\n (click)=\"onTileClick(item, $event)\"\n (dblclick)=\"onItemDoubleClick(item)\">\n <td class=\"td-check\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedItems.includes(item.key)\"\n (change)=\"toggleItemSelection(item, $event)\" />\n </td>\n <td class=\"td-name col-name\">\n <div class=\"row-name-wrap\">\n <div class=\"row-icon-badge\" [class]=\"getItemColorClass(item)\">\n <i [class]=\"getItemIcon(item)\" class=\"item-icon\"></i>\n </div>\n <span class=\"row-file-name item-name\" [title]=\"item.name\">{{ item.name }}</span>\n </div>\n </td>\n <td class=\"td-type col-type\">\n <span class=\"type-badge item-type\">{{ getFileType(item) }}</span>\n </td>\n <td class=\"td-size col-size\">\n <span class=\"size-text item-size\">{{ item.type === 'folder' ? '\u2014' : formatFileSize(item.size) }}</span>\n </td>\n <td class=\"td-date col-modified\">\n <span class=\"date-text item-date\">{{ formatDate(item.lastModified) }}</span>\n </td>\n <td class=\"td-actions\" (click)=\"$event.stopPropagation()\">\n <div class=\"row-action-buttons\">\n @if (item.type === 'file') {\n <button\n type=\"button\"\n class=\"row-act-btn\"\n (click)=\"openPreview(item)\"\n title=\"Preview\">\n <i class=\"fa-solid fa-eye\"></i>\n </button>\n <button\n type=\"button\"\n class=\"row-act-btn\"\n (click)=\"downloadFile(item)\"\n title=\"Download\">\n <i class=\"fa-solid fa-download\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Empty State -->\n @if (!isLoading && !errorMessage && filteredItems.length === 0 && !isMultiProviderSearchMode) {\n <mj-empty-state\n Icon=\"fa-solid fa-folder-open\"\n Title=\"This folder is empty\"\n Message=\"Drop files here to upload or create a new folder\"\n ActionText=\"Upload Files\"\n ActionIcon=\"fa-solid fa-cloud-arrow-up\"\n (Action)=\"onUploadClick()\" />\n }\n\n <!-- Bottom Status Bar -->\n @if (!isLoading && !errorMessage) {\n <div class=\"status-bar\">\n <span class=\"status-count\">\n {{ filteredItems.length }} item{{ filteredItems.length === 1 ? '' : 's' }}\n @if (searchQuery) {\n (filtered)\n }\n </span>\n <div class=\"status-account-badge\" *ngIf=\"account\">\n <i class=\"fa-solid fa-cloud\"></i>\n <span>{{ account.account.Name }} ({{ account.provider.Name }})</span>\n </div>\n </div>\n }\n\n <!-- Drag & Drop Upload Overlay -->\n @if (isDragging) {\n <div class=\"drag-drop-overlay\">\n <div class=\"drag-drop-content\">\n <i class=\"fa-solid fa-cloud-arrow-up drag-icon\"></i>\n <h3>Drop files here to upload</h3>\n <p>Files will be stored in {{ folderPath || 'Root' }}</p>\n </div>\n </div>\n }\n\n <!-- Upload Progress Bar -->\n @if (isUploading) {\n <div class=\"upload-progress-bar-wrap\">\n <div class=\"upload-progress-card\">\n <div class=\"upload-info\">\n <i class=\"fa-solid fa-spinner fa-spin upload-spinner\"></i>\n <span class=\"upload-text\">Uploading {{ uploadingFileName }}...</span>\n </div>\n <div class=\"progress-track\">\n <div class=\"progress-fill\" [style.width.%]=\"uploadProgress\"></div>\n </div>\n </div>\n </div>\n }\n</div>\n\n<!-- ==========================================================================\n MODAL DIALOGS (Design Tokens, Smooth Animations, Backdrop Blurs)\n ========================================================================== -->\n\n<!-- File Preview Modal -->\n@if (showPreviewModal && previewItem) {\n <div class=\"mj-modal-backdrop\" (click)=\"closePreview()\">\n <div class=\"mj-modal-window preview-modal-window\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <div class=\"modal-title-wrap\">\n <div class=\"row-icon-badge\" [class]=\"getItemColorClass(previewItem)\">\n <i [class]=\"getItemIcon(previewItem)\"></i>\n </div>\n <div>\n <h3 class=\"modal-title\">{{ previewItem.name }}</h3>\n <span class=\"modal-subtitle\">{{ formatFileSize(previewItem.size) }} • {{ formatDate(previewItem.lastModified) }}</span>\n </div>\n </div>\n <div class=\"modal-header-actions\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"downloadFile(previewItem)\"\n title=\"Download file\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download</span>\n </button>\n <button\n type=\"button\"\n class=\"modal-close-btn\"\n (click)=\"closePreview()\"\n title=\"Close preview\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n\n <div class=\"preview-modal-body\">\n @if (isLoadingPreview) {\n <div class=\"preview-loading\">\n <mj-loading text=\"Loading preview...\"></mj-loading>\n </div>\n } @else if (previewUrl) {\n @switch (previewMediaType) {\n @case ('image') {\n <div class=\"preview-media-wrap image-preview-wrap\">\n <img [src]=\"previewUrl\" [alt]=\"previewItem.name\" class=\"preview-img\" />\n </div>\n }\n @case ('pdf') {\n <div class=\"preview-media-wrap pdf-preview-wrap\">\n <iframe [src]=\"previewUrl\" class=\"preview-iframe\" title=\"PDF Preview\"></iframe>\n </div>\n }\n @case ('video') {\n <div class=\"preview-media-wrap video-preview-wrap\">\n <video [src]=\"previewUrl\" controls autoplay class=\"preview-video\"></video>\n </div>\n }\n @case ('audio') {\n <div class=\"preview-media-wrap audio-preview-wrap\">\n <i class=\"fa-solid fa-music preview-audio-icon\"></i>\n <audio [src]=\"previewUrl\" controls class=\"preview-audio\"></audio>\n </div>\n }\n @default {\n <div class=\"preview-fallback\">\n <i [class]=\"getItemIcon(previewItem)\" class=\"preview-fallback-icon\"></i>\n <h4>{{ previewItem.name }}</h4>\n <p>Preview is not supported for this file type.</p>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"downloadFile(previewItem)\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download File</span>\n </button>\n </div>\n }\n }\n } @else {\n <div class=\"preview-fallback\">\n <i class=\"fa-solid fa-triangle-exclamation preview-fallback-icon\"></i>\n <h4>Preview unavailable</h4>\n <p>Could not generate a preview link for this file.</p>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"downloadFile(previewItem)\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download File</span>\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- New Folder Modal -->\n@if (showNewFolderDialog) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelNewFolder()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Create New Folder</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelNewFolder()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <label class=\"form-label\">Folder name:</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"newFolderName\"\n [disabled]=\"isCreatingFolder\"\n placeholder=\"Enter folder name\"\n (keydown.enter)=\"onCreateFolder()\"\n autofocus />\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelNewFolder()\"\n [disabled]=\"isCreatingFolder\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onCreateFolder()\"\n [disabled]=\"!newFolderName.trim() || isCreatingFolder\">\n @if (isCreatingFolder) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCreatingFolder ? 'Creating...' : 'Create' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Delete Confirmation Modal -->\n@if (showDeleteDialog && itemToDelete) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelDelete()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Delete {{ itemToDelete.type === 'folder' ? 'Folder' : 'File' }}?</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelDelete()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <p class=\"modal-body-text\">\n Are you sure you want to delete <strong>{{ itemToDelete.name }}</strong>?\n </p>\n @if (itemToDelete.type === 'folder') {\n <div class=\"modal-warning-banner warning\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>This will delete the folder and all its contents.</span>\n </div>\n }\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelDelete()\"\n [disabled]=\"isDeleting\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--danger danger\"\n (click)=\"onConfirmDelete()\"\n [disabled]=\"isDeleting\">\n @if (isDeleting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isDeleting ? 'Deleting...' : 'Delete' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Rename Modal -->\n@if (showRenameDialog && itemToRename) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelRename()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Rename {{ itemToRename.type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelRename()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <label class=\"form-label\">New name:</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"newItemName\"\n [disabled]=\"isRenaming\"\n placeholder=\"Enter new name\"\n (keydown.enter)=\"onConfirmRename()\"\n autofocus />\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelRename()\"\n [disabled]=\"isRenaming\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmRename()\"\n [disabled]=\"isRenaming || !newItemName.trim()\">\n @if (isRenaming) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isRenaming ? 'Renaming...' : 'Rename' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Copy Modal -->\n@if (showCopyDialog && itemToCopy) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelCopy()\">\n <div class=\"mj-modal-window modal-compact\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Copy {{ itemToCopy.type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelCopy()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">Copying <strong>{{ itemToCopy.name }}</strong></p>\n <label class=\"form-label\">Destination Path</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"copyDestinationPath\"\n [disabled]=\"isCopying\"\n placeholder=\"path/to/destination\"\n (keydown.enter)=\"onConfirmCopy()\" />\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelCopy()\"\n [disabled]=\"isCopying\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmCopy()\"\n [disabled]=\"isCopying || !copyDestinationPath.trim()\">\n @if (isCopying) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCopying ? 'Copying...' : 'Copy' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Move Modal -->\n@if (showMoveDialog && itemToMove) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelMove()\">\n <div class=\"mj-modal-window modal-compact\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Move {{ itemToMove.type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelMove()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">Moving <strong>{{ itemToMove.name }}</strong></p>\n <label class=\"form-label\">Destination Folder</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"moveDestinationPath\"\n [disabled]=\"isMoving\"\n placeholder=\"destination/folder/\"\n (keydown.enter)=\"onConfirmMove()\" />\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelMove()\"\n [disabled]=\"isMoving\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmMove()\"\n [disabled]=\"isMoving || !moveDestinationPath.trim()\">\n @if (isMoving) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isMoving ? 'Moving...' : 'Move' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Copy to Account Modal -->\n@if (showCopyToProviderDialog && itemToCopyToProvider) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelCopyToAccount()\">\n <div class=\"mj-modal-window\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Copy to Another Account</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelCopyToAccount()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">\n Copying <strong>{{ itemToCopyToProvider.name }}</strong> from <strong>{{ account?.account?.Name }}</strong>\n </p>\n\n <label class=\"form-label\">Select Destination Storage Accounts</label>\n <div class=\"account-picker-list\">\n @for (a of availableAccounts; track a.account.ID) {\n <label class=\"account-picker-item\" [for]=\"'dest-acc-' + a.account.ID\">\n <input\n type=\"checkbox\"\n [id]=\"'dest-acc-' + a.account.ID\"\n [checked]=\"isDestinationAccountSelected(a.account.ID)\"\n [disabled]=\"isCopyingToAccount\"\n (change)=\"toggleDestinationAccount(a.account.ID)\" />\n <span class=\"picker-name\">{{ a.account.Name }}</span>\n <span class=\"picker-badge\">{{ a.provider.Name }}</span>\n </label>\n }\n </div>\n\n <label class=\"form-label\" style=\"margin-top: 14px;\">Destination Filename</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"copyToAccountDestinationPath\"\n [disabled]=\"isCopyingToAccount\"\n placeholder=\"Filename\"\n (keydown.enter)=\"onConfirmCopyToAccount()\" />\n\n @if (copyToAccountProgress) {\n <div class=\"copy-progress-indicator\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Copying to {{ copyToAccountProgress.currentAccount }} ({{ copyToAccountProgress.current }}/{{ copyToAccountProgress.total }})...</span>\n </div>\n }\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelCopyToAccount()\"\n [disabled]=\"isCopyingToAccount\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmCopyToAccount()\"\n [disabled]=\"isCopyingToAccount || selectedDestinationAccounts.size === 0 || !copyToAccountDestinationPath.trim()\">\n @if (isCopyingToAccount) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCopyingToAccount ? 'Copying...' : 'Copy to ' + selectedDestinationAccounts.size + ' Account' + (selectedDestinationAccounts.size !== 1 ? 's' : '') }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n", styles: ["/* ==========================================================================\n File Grid & Table Component Styles\n Modern macOS Finder / Cloud Storage with Design Tokens\n ========================================================================== */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.file-grid-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* ==========================================================================\n Command Bar (Top Navigation, Search & Primary Actions)\n ========================================================================== */\n\n.command-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 16px;\n background-color: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n\n.command-bar-left {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n flex: 1;\n}\n\n.nav-btn-group {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.mj-tool-btn {\n width: 32px;\n height: 32px;\n min-width: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-tool-btn:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-tool-btn[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n background-color: transparent;\n}\n\n/* Breadcrumb Trail */\n.breadcrumb-trail {\n display: flex;\n align-items: center;\n gap: 4px;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n min-width: 0;\n padding: 2px 4px;\n}\n\n.breadcrumb-trail::-webkit-scrollbar {\n display: none;\n}\n\n.trail-root-icon {\n color: var(--mj-status-warning, #f59e0b);\n font-size: 0.95rem;\n margin-right: 2px;\n flex-shrink: 0;\n}\n\n.crumb-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 500;\n padding: 4px 8px;\n border-radius: var(--mj-radius-sm, 6px);\n cursor: pointer;\n white-space: nowrap;\n transition: all 0.12s ease;\n}\n\n.crumb-btn:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n}\n\n.crumb-btn.current {\n color: var(--mj-brand-primary);\n font-weight: 600;\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.crumb-separator {\n color: var(--mj-text-muted);\n font-size: 0.75rem;\n}\n\n/* Command Bar Right */\n.command-bar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n/* Search Pill */\n.search-pill {\n display: flex;\n align-items: center;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 4%, var(--mj-bg-surface)));\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n padding: 2px 10px;\n gap: 6px;\n transition: border-color 0.15s ease;\n}\n\n.search-pill:focus-within {\n border-color: var(--mj-brand-primary);\n}\n\n.search-icon {\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n}\n\n.search-input {\n border: none;\n background: transparent;\n outline: none;\n font-size: 0.82rem;\n color: var(--mj-text-primary);\n width: 150px;\n padding: 4px 0;\n}\n\n.search-input::placeholder {\n color: var(--mj-text-muted);\n}\n\n.search-clear-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n padding: 0 4px;\n font-size: 0.75rem;\n}\n\n.search-clear-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.type-filter-select {\n border: none;\n background: transparent;\n font-size: 0.76rem;\n color: var(--mj-text-secondary);\n outline: none;\n cursor: pointer;\n padding: 2px 4px;\n border-left: 1px solid var(--mj-border-default);\n}\n\n/* Toggle Multi-Search Button */\n.mj-btn-search-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-secondary);\n font-size: 0.8rem;\n font-weight: 500;\n padding: 6px 12px;\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-btn-search-toggle:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n}\n\n.mj-btn-search-toggle.active {\n background-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-brand-primary);\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 4%, var(--mj-bg-surface)));\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n padding: 2px;\n gap: 2px;\n}\n\n.view-mode-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n padding: 5px 8px;\n font-size: 0.8rem;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.view-mode-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.view-mode-btn.active {\n background-color: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);\n}\n\n/* Action Buttons */\n.mj-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n font-size: 0.82rem;\n font-weight: 500;\n border-radius: var(--mj-radius-sm, 6px);\n cursor: pointer;\n transition: all 0.15s ease;\n border: 1px solid transparent;\n white-space: nowrap;\n}\n\n.mj-action-btn--primary {\n background-color: var(--mj-brand-primary, #3b82f6);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-brand-primary, #3b82f6);\n}\n\n.mj-action-btn--primary:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 85%, var(--mj-text-primary, #000000));\n}\n\n.mj-action-btn--secondary {\n background-color: var(--mj-bg-surface);\n border-color: var(--mj-border-default);\n color: var(--mj-text-primary);\n}\n\n.mj-action-btn--secondary:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n}\n\n.mj-action-btn--danger {\n background-color: var(--mj-status-error, #ef4444);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-status-error, #ef4444);\n}\n\n.mj-action-btn--danger:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-status-error, #ef4444) 85%, var(--mj-text-primary, #000000));\n}\n\n.mj-action-btn[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* ==========================================================================\n Selection Action Bar\n ========================================================================== */\n\n.selection-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background-color: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n border-bottom: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n gap: 12px;\n flex-shrink: 0;\n animation: slideDown 0.18s ease-out;\n}\n\n@keyframes slideDown {\n from { transform: translateY(-100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n.selection-bar-left {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.selection-checkbox-wrap {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n user-select: none;\n}\n\n.selection-count {\n font-size: 0.82rem;\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.selection-bar-right {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.sel-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n font-size: 0.76rem;\n font-weight: 500;\n border-radius: var(--mj-radius-sm, 6px);\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.sel-action-btn:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.sel-action-btn--danger:hover {\n background-color: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface));\n border-color: var(--mj-status-error);\n color: var(--mj-status-error);\n}\n\n.sel-action-btn--ghost {\n background: transparent;\n border-color: transparent;\n}\n\n/* ==========================================================================\n Multi-Account Search Card\n ========================================================================== */\n\n.multi-search-card {\n margin: 12px 16px;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 10px);\n box-shadow: var(--mj-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));\n overflow: hidden;\n animation: fadeIn 0.2s ease;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.multi-search-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background-color: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.multi-search-title {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n}\n\n.multi-search-title h3 {\n margin: 0;\n font-size: 0.88rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.multi-search-body {\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.multi-search-bar {\n display: flex;\n gap: 8px;\n}\n\n.multi-search-text {\n flex: 1;\n padding: 8px 12px;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.85rem;\n outline: none;\n}\n\n.multi-search-text:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.multi-search-accounts {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.account-select-label {\n font-size: 0.74rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n}\n\n.account-pill-list {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.account-select-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: var(--mj-radius-full, 9999px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface-sunken);\n font-size: 0.76rem;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.account-select-pill.checked {\n border-color: var(--mj-brand-primary);\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.account-select-pill.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.pill-account-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.pill-provider-tag {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n.pill-no-search {\n font-size: 0.68rem;\n color: var(--mj-status-warning);\n}\n\n/* Results */\n.multi-search-results {\n border-top: 1px solid var(--mj-border-default);\n padding: 12px 16px;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.results-meta-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n font-size: 0.8rem;\n}\n\n.meta-count {\n color: var(--mj-text-primary);\n}\n\n.meta-stats {\n color: var(--mj-text-secondary);\n}\n\n.clear-results-btn {\n margin-left: auto;\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 0.76rem;\n cursor: pointer;\n}\n\n.clear-results-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.results-groups {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.result-group {\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n overflow: hidden;\n}\n\n.result-group-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background-color: var(--mj-bg-surface-sunken);\n font-size: 0.8rem;\n}\n\n.group-account-name {\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.group-count-badge {\n margin-left: auto;\n background-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse, #ffffff);\n font-size: 0.7rem;\n font-weight: 600;\n padding: 1px 6px;\n border-radius: 9999px;\n}\n\n.group-error-badge {\n margin-left: auto;\n color: var(--mj-status-error);\n font-size: 0.72rem;\n}\n\n.result-items-grid {\n display: flex;\n flex-direction: column;\n}\n\n.search-item-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n font-size: 0.8rem;\n}\n\n.search-item-row:last-child {\n border-bottom: none;\n}\n\n.search-item-icon {\n color: var(--mj-brand-primary);\n}\n\n.search-item-info {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n}\n\n.search-item-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.search-item-path {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.search-item-size, .search-item-date {\n font-size: 0.74rem;\n color: var(--mj-text-secondary);\n}\n\n.group-empty-text {\n padding: 12px;\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n text-align: center;\n}\n\n/* ==========================================================================\n Cards Grid View\n ========================================================================== */\n\n.file-cards-grid {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));\n gap: 14px;\n align-content: start;\n}\n\n.file-card {\n display: flex;\n flex-direction: column;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 10px);\n padding: 10px;\n cursor: pointer;\n transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n position: relative;\n user-select: none;\n}\n\n.file-card:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));\n transform: translateY(-2px);\n}\n\n.file-card.selected {\n border-color: var(--mj-brand-primary);\n background-color: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface));\n box-shadow: 0 0 0 1px var(--mj-brand-primary);\n}\n\n.file-card-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 22px;\n margin-bottom: 6px;\n}\n\n.file-card-checkbox {\n cursor: pointer;\n}\n\n.file-card-quick-actions {\n display: flex;\n gap: 4px;\n opacity: 0;\n transition: opacity 0.15s ease;\n}\n\n.file-card:hover .file-card-quick-actions {\n opacity: 1;\n}\n\n.quick-action-btn {\n width: 22px;\n height: 22px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n color: var(--mj-text-secondary);\n font-size: 0.68rem;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.quick-action-btn:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n/* Card Icon / Thumbnail */\n.file-card-icon-container {\n height: 74px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-md, 8px);\n margin-bottom: 8px;\n font-size: 2rem;\n transition: transform 0.15s ease;\n}\n\n.file-card:hover .file-card-icon-container {\n transform: scale(1.04);\n}\n\n/* Card Body */\n.file-card-body {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.file-card-name {\n font-size: 0.8rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n line-height: 1.25;\n min-height: 2em;\n}\n\n.file-card-meta {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n/* ==========================================================================\n List View Table\n ========================================================================== */\n\n.file-table-container {\n flex: 1;\n overflow-y: auto;\n padding: 8px 16px;\n}\n\n.file-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.82rem;\n}\n\n.file-table th {\n text-align: left;\n padding: 8px 10px;\n border-bottom: 1px solid var(--mj-border-default);\n color: var(--mj-text-muted);\n font-size: 0.72rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n user-select: none;\n cursor: pointer;\n}\n\n.file-table th.th-check {\n width: 32px;\n cursor: default;\n}\n\n.file-table th.th-actions {\n width: 70px;\n text-align: right;\n cursor: default;\n}\n\n.sort-arrow {\n margin-left: 4px;\n font-size: 0.65rem;\n}\n\n.file-table-row {\n border-bottom: 1px solid var(--mj-border-default);\n cursor: pointer;\n transition: background-color 0.12s ease;\n}\n\n.file-table-row:hover {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 4%, var(--mj-bg-surface));\n}\n\n.file-table-row.selected {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.file-table-row td {\n padding: 8px 10px;\n vertical-align: middle;\n}\n\n.row-name-wrap {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.row-icon-badge {\n width: 28px;\n height: 28px;\n min-width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-sm, 6px);\n font-size: 0.9rem;\n}\n\n.row-file-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.type-badge {\n display: inline-block;\n padding: 2px 6px;\n border-radius: 4px;\n background-color: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n font-size: 0.7rem;\n}\n\n.size-text, .date-text {\n color: var(--mj-text-secondary);\n font-size: 0.76rem;\n white-space: nowrap;\n}\n\n.row-action-buttons {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 4px;\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n.file-table-row:hover .row-action-buttons {\n opacity: 1;\n}\n\n.row-act-btn {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n color: var(--mj-text-secondary);\n font-size: 0.7rem;\n cursor: pointer;\n}\n\n.row-act-btn:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n/* ==========================================================================\n Media Color Badges\n ========================================================================== */\n\n.mj-file-thumb--folder {\n background-color: color-mix(in srgb, var(--mj-status-warning, #f59e0b) 14%, transparent);\n color: var(--mj-status-warning, #f59e0b);\n}\n\n.mj-file-thumb--pdf {\n background-color: color-mix(in srgb, var(--mj-status-error, #ef4444) 14%, transparent);\n color: var(--mj-status-error, #ef4444);\n}\n\n.mj-file-thumb--image {\n background-color: color-mix(in srgb, var(--mj-media-image, #8b5cf6) 14%, transparent);\n color: var(--mj-media-image, #8b5cf6);\n}\n\n.mj-file-thumb--video {\n background-color: color-mix(in srgb, var(--mj-brand-primary, #3b82f6) 14%, transparent);\n color: var(--mj-brand-primary, #3b82f6);\n}\n\n.mj-file-thumb--audio {\n background-color: color-mix(in srgb, var(--mj-status-warning, #f59e0b) 14%, transparent);\n color: var(--mj-status-warning, #f59e0b);\n}\n\n.mj-file-thumb--code {\n background-color: color-mix(in srgb, var(--mj-status-success, #10b981) 14%, transparent);\n color: var(--mj-status-success, #10b981);\n}\n\n.mj-file-thumb--text {\n background-color: color-mix(in srgb, var(--mj-text-muted, #6b7280) 14%, transparent);\n color: var(--mj-text-muted, #6b7280);\n}\n\n.mj-file-thumb--document {\n background-color: color-mix(in srgb, var(--mj-brand-primary-hover, #2563eb) 14%, transparent);\n color: var(--mj-brand-primary-hover, #2563eb);\n}\n\n/* ==========================================================================\n Status Bar (Bottom Information)\n ========================================================================== */\n\n.status-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 16px;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 3%, var(--mj-bg-surface)));\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n flex-shrink: 0;\n}\n\n.status-account-badge {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n/* ==========================================================================\n Drag & Drop Overlay & Progress\n ========================================================================== */\n\n.drag-drop-overlay {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, rgba(255, 255, 255, 0.9));\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drag-drop-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 32px 48px;\n background-color: var(--mj-bg-surface);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-lg, 0 10px 25px rgba(0, 0, 0, 0.15));\n}\n\n.drag-icon {\n font-size: 3rem;\n color: var(--mj-brand-primary);\n margin-bottom: 12px;\n}\n\n.drag-drop-content h3 {\n margin: 0 0 6px 0;\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.drag-drop-content p {\n margin: 0;\n font-size: 0.8rem;\n color: var(--mj-text-secondary);\n}\n\n.upload-progress-bar-wrap {\n position: absolute;\n bottom: 36px;\n right: 20px;\n z-index: 90;\n}\n\n.upload-progress-card {\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n padding: 10px 14px;\n box-shadow: var(--mj-shadow-lg, 0 8px 20px rgba(0, 0, 0, 0.15));\n width: 260px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.upload-info {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.upload-spinner {\n color: var(--mj-brand-primary);\n}\n\n.upload-text {\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.progress-track {\n height: 4px;\n background-color: var(--mj-bg-surface-sunken);\n border-radius: 9999px;\n overflow: hidden;\n}\n\n.progress-fill {\n height: 100%;\n background-color: var(--mj-brand-primary);\n transition: width 0.15s ease;\n}\n\n.grid-loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 1;\n}\n\n/* ==========================================================================\n Modals (Design Tokens & Glass Backdrop)\n ========================================================================== */\n\n.mj-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1050;\n padding: 20px;\n animation: modalFadeIn 0.18s ease-out;\n}\n\n@keyframes modalFadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.mj-modal-window {\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-xl, 0 20px 35px rgba(0, 0, 0, 0.25));\n width: 100%;\n max-width: 500px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n}\n\n.mj-modal-window.modal-compact {\n max-width: 420px;\n}\n\n.mj-modal-window.preview-modal-window {\n max-width: 850px;\n max-height: 85vh;\n height: 80vh;\n}\n\n.mj-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 18px;\n border-bottom: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n}\n\n.modal-title-wrap {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.modal-title {\n margin: 0;\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.modal-subtitle {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n}\n\n.modal-header-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.modal-close-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n}\n\n.modal-close-btn:hover {\n color: var(--mj-text-primary);\n background-color: var(--mj-bg-surface-sunken);\n}\n\n.mj-modal-body {\n padding: 18px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.form-label {\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n\n.form-input {\n padding: 8px 12px;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.85rem;\n outline: none;\n}\n\n.form-input:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.modal-body-text {\n margin: 0;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n}\n\n.modal-warning-banner {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background-color: color-mix(in srgb, var(--mj-status-warning) 12%, var(--mj-bg-surface));\n border: 1px solid var(--mj-status-warning);\n border-radius: var(--mj-radius-sm, 6px);\n font-size: 0.76rem;\n color: var(--mj-text-primary);\n}\n\n.mj-modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 18px;\n border-top: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface-sunken);\n}\n\n/* Preview Modal Body */\n.preview-modal-body {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--mj-bg-surface-sunken);\n padding: 16px;\n overflow: hidden;\n}\n\n.preview-media-wrap {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.preview-img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.preview-iframe {\n width: 100%;\n height: 100%;\n border: none;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.preview-video {\n max-width: 100%;\n max-height: 100%;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.audio-preview-wrap {\n flex-direction: column;\n gap: 16px;\n}\n\n.preview-audio-icon {\n font-size: 3rem;\n color: var(--mj-brand-primary);\n}\n\n.preview-audio {\n width: 320px;\n}\n\n.preview-fallback {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n text-align: center;\n color: var(--mj-text-secondary);\n}\n\n.preview-fallback-icon {\n font-size: 3rem;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n}\n\n.preview-fallback h4 {\n margin: 0;\n font-size: 0.95rem;\n color: var(--mj-text-primary);\n}\n\n.preview-fallback p {\n margin: 0;\n font-size: 0.8rem;\n}\n\n.account-picker-list {\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 180px;\n overflow-y: auto;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n padding: 6px;\n}\n\n.account-picker-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 4px;\n cursor: pointer;\n}\n\n.account-picker-item:hover {\n background-color: var(--mj-bg-surface-sunken);\n}\n\n.picker-name {\n font-size: 0.82rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.picker-badge {\n margin-left: auto;\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.copy-progress-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n color: var(--mj-brand-primary);\n margin-top: 8px;\n}\n"] }]
|
|
2749
|
-
}], () => [], {
|
|
3384
|
+
args: [{ standalone: false, selector: 'mj-file-grid', template: "<div class=\"file-grid-container\"\n (dragenter)=\"onDragEnter($event)\"\n (dragover)=\"onDragOver($event)\"\n (dragleave)=\"onDragLeave($event)\"\n (drop)=\"onDrop($event)\">\n\n <!-- Top Command Bar: Breadcrumbs & Primary Actions -->\n @if (!isLoading && !errorMessage) {\n <div class=\"command-bar breadcrumb-bar\">\n <!-- Left: Breadcrumb Navigation Trail -->\n <div class=\"command-bar-left\">\n <div class=\"nav-btn-group nav-buttons\">\n <button\n type=\"button\"\n class=\"mj-tool-btn nav-btn\"\n [disabled]=\"!canNavigateUp()\"\n (click)=\"navigateUp()\"\n title=\"Go up to parent folder\">\n <i class=\"fa-solid fa-arrow-up\"></i>\n </button>\n <button\n type=\"button\"\n class=\"mj-tool-btn nav-btn\"\n (click)=\"refresh()\"\n title=\"Refresh current folder\">\n <i class=\"fa-solid fa-arrows-rotate\"></i>\n </button>\n </div>\n\n <nav class=\"breadcrumb-trail breadcrumb-path\" aria-label=\"Folder Path\">\n <i class=\"fa-solid fa-folder-open trail-root-icon\"></i>\n @for (crumb of getPathBreadcrumbs(); track crumb.path; let last = $last) {\n <button\n type=\"button\"\n class=\"crumb-btn\"\n [class.current]=\"last\"\n (click)=\"navigateToBreadcrumb(crumb.path)\"\n [title]=\"crumb.name\">\n {{ crumb.name }}\n </button>\n @if (!last) {\n <span class=\"crumb-separator\">/</span>\n }\n }\n </nav>\n </div>\n\n <!-- Right: Search, Filter, View Mode & Action Buttons -->\n <div class=\"command-bar-right\">\n <!-- Search & Filter Bar -->\n <div class=\"search-pill search-bar\">\n <i class=\"fa-solid fa-magnifying-glass search-icon\"></i>\n <input\n type=\"text\"\n class=\"search-input\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange($event)\"\n placeholder=\"Search files...\"\n [disabled]=\"isLoading || isMultiProviderSearchMode\" />\n @if (searchQuery && !isMultiProviderSearchMode) {\n <button\n type=\"button\"\n class=\"search-clear-btn\"\n (click)=\"clearSearch()\"\n title=\"Clear search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n }\n <select\n class=\"type-filter-select file-type-filter\"\n [(ngModel)]=\"fileTypeFilter\"\n (ngModelChange)=\"onFileTypeFilterChange($event)\"\n [disabled]=\"isLoading || isMultiProviderSearchMode\">\n <option value=\"all\">All Items</option>\n <option value=\"files\">Files only</option>\n <option value=\"folders\">Folders only</option>\n </select>\n </div>\n\n <!-- Search Across Accounts Toggle -->\n <button\n type=\"button\"\n class=\"mj-btn-search-toggle multi-search-btn\"\n [class.active]=\"isMultiProviderSearchMode\"\n (click)=\"toggleMultiAccountSearchMode()\"\n title=\"Search across all storage accounts\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <span>All Accounts</span>\n </button>\n\n <!-- View Switcher -->\n <div class=\"view-mode-toggle view-toggle\">\n <button\n type=\"button\"\n class=\"view-mode-btn view-btn\"\n [class.active]=\"viewMode === 'list'\"\n (click)=\"viewMode = 'list'\"\n title=\"List View\">\n <i class=\"fa-solid fa-list\"></i>\n </button>\n <button\n type=\"button\"\n class=\"view-mode-btn view-btn\"\n [class.active]=\"viewMode === 'grid'\"\n (click)=\"viewMode = 'grid'\"\n title=\"Grid View\">\n <i class=\"fa-solid fa-table-cells-large\"></i>\n </button>\n </div>\n\n <!-- Primary Action CTAs -->\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary toolbar-btn\"\n (click)=\"onNewFolderClick()\"\n title=\"Create New Folder\">\n <i class=\"fa-solid fa-folder-plus\"></i>\n <span>New Folder</span>\n </button>\n\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary toolbar-btn\"\n (click)=\"onUploadClick()\"\n title=\"Upload Files\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Upload</span>\n </button>\n </div>\n </div>\n }\n\n <!-- Selection Action Bar (Appears when 1+ items selected) -->\n @if (selectedItems.length > 0 && !isMultiProviderSearchMode) {\n <div class=\"selection-bar\">\n <div class=\"selection-bar-left\">\n <label class=\"selection-checkbox-wrap\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"areAllSelected()\"\n (change)=\"areAllSelected() ? deselectAll() : selectAll()\" />\n <span class=\"selection-count\">{{ selectedItems.length }} item{{ selectedItems.length === 1 ? '' : 's' }} selected</span>\n </label>\n </div>\n\n <div class=\"selection-bar-right\">\n @if (selectedItems.length === 1 && getSelectedItem()?.Type === 'file') {\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"openPreview(getSelectedItem()!)\"\n title=\"Preview file\">\n <i class=\"fa-solid fa-eye\"></i>\n <span>Preview</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onDownloadClick()\"\n title=\"Download file\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onCopyToAccountClick()\"\n title=\"Copy to another storage account\">\n <i class=\"fa-solid fa-cloud-arrow-up\"></i>\n <span>Copy to...</span>\n </button>\n }\n @if (selectedItems.length === 1) {\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onRenameClick()\"\n title=\"Rename item\">\n <i class=\"fa-solid fa-pen\"></i>\n <span>Rename</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onCopyClick()\"\n title=\"Copy to path\">\n <i class=\"fa-solid fa-copy\"></i>\n <span>Copy</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn\"\n (click)=\"onMoveClick()\"\n title=\"Move item\">\n <i class=\"fa-solid fa-arrows-up-down-left-right\"></i>\n <span>Move</span>\n </button>\n }\n <button\n type=\"button\"\n class=\"sel-action-btn sel-action-btn--danger\"\n (click)=\"onDeleteClick()\"\n title=\"Delete selected item(s)\">\n <i class=\"fa-solid fa-trash\"></i>\n <span>Delete</span>\n </button>\n <button\n type=\"button\"\n class=\"sel-action-btn sel-action-btn--ghost\"\n (click)=\"deselectAll()\"\n title=\"Deselect all\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n }\n\n <!-- Multi-Provider Search Panel -->\n @if (isMultiProviderSearchMode) {\n <div class=\"multi-search-card multi-provider-search-panel\">\n <div class=\"multi-search-header search-panel-header\">\n <div class=\"multi-search-title\">\n <i class=\"fa-solid fa-layer-group\"></i>\n <h3>Search Across Storage Accounts</h3>\n </div>\n <button\n type=\"button\"\n class=\"modal-close-btn close-panel-btn\"\n (click)=\"toggleMultiAccountSearchMode()\"\n title=\"Close search\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n\n <div class=\"multi-search-body search-panel-body\">\n <div class=\"multi-search-bar search-input-row\">\n <input\n type=\"text\"\n class=\"multi-search-text multi-search-input\"\n [(ngModel)]=\"multiProviderSearchQuery\"\n placeholder=\"Type query to search across connected accounts...\"\n (keydown.enter)=\"executeMultiAccountSearch()\"\n [disabled]=\"isSearching\" />\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary search-execute-btn\"\n (click)=\"executeMultiAccountSearch()\"\n [disabled]=\"isSearching || !multiProviderSearchQuery.trim() || selectedSearchProviders.size === 0\">\n @if (isSearching) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n } @else {\n <i class=\"fa-solid fa-magnifying-glass\"></i>\n }\n <span>{{ isSearching ? 'Searching...' : 'Search Accounts' }}</span>\n </button>\n </div>\n\n <div class=\"multi-search-accounts provider-selection\">\n <span class=\"account-select-label provider-selection-label\">Target Accounts:</span>\n <div class=\"account-pill-list provider-checkboxes\">\n @for (a of availableAccounts; track a.account.ID) {\n <label\n class=\"account-select-pill provider-checkbox\"\n [class.disabled]=\"!accountSupportsSearch(a)\"\n [class.checked]=\"isAccountSelectedForSearch(a.account.ID)\">\n <input\n type=\"checkbox\"\n [checked]=\"isAccountSelectedForSearch(a.account.ID)\"\n (change)=\"toggleSearchAccount(a.account.ID)\"\n [disabled]=\"!accountSupportsSearch(a)\" />\n <span class=\"pill-account-name provider-name\">{{ a.account.Name }}</span>\n <span class=\"pill-provider-tag provider-type\">{{ a.provider.Name }}</span>\n @if (!accountSupportsSearch(a)) {\n <span class=\"pill-no-search no-search-badge\">(no search)</span>\n }\n </label>\n }\n </div>\n </div>\n </div>\n\n <!-- Search Results -->\n @if (multiProviderSearchResults) {\n <div class=\"multi-search-results search-results-container\">\n <div class=\"results-meta-bar search-results-summary\">\n <span class=\"meta-count result-count\">\n <strong>{{ multiProviderSearchResults.TotalResultsReturned }}</strong> result{{ multiProviderSearchResults.TotalResultsReturned === 1 ? '' : 's' }} found\n </span>\n <span class=\"meta-stats provider-stats\">\n ({{ multiProviderSearchResults.SuccessfulAccounts }} account{{ multiProviderSearchResults.SuccessfulAccounts === 1 ? '' : 's' }} searched)\n </span>\n <button\n type=\"button\"\n class=\"clear-results-btn\"\n (click)=\"clearMultiProviderSearch()\">\n <i class=\"fa-solid fa-xmark\"></i> Clear Results\n </button>\n </div>\n\n <div class=\"results-groups provider-results-list\">\n @for (accountResult of multiProviderSearchResults.AccountResults; track accountResult.accountID) {\n <div class=\"result-group provider-result-group\" [class.failed]=\"!accountResult.success\">\n <div class=\"result-group-head provider-result-header\">\n <i class=\"fa-solid fa-cloud-bolt group-icon\"></i>\n <span class=\"group-account-name provider-name\">{{ accountResult.accountName }}</span>\n @if (accountResult.success) {\n <span class=\"group-count-badge result-count\">{{ accountResult.results.length }}</span>\n } @else {\n <span class=\"group-error-badge error-badge\">{{ accountResult.errorMessage }}</span>\n }\n </div>\n\n @if (accountResult.success && accountResult.results.length > 0) {\n <div class=\"result-items-grid provider-result-files\">\n @for (file of accountResult.results; track file.path) {\n <div class=\"search-item-row search-result-item\">\n <i [class]=\"getSearchResultIcon(file)\" class=\"search-item-icon file-icon\"></i>\n <div class=\"search-item-info file-info\">\n <span class=\"search-item-name file-name\">{{ file.name }}</span>\n <span class=\"search-item-path file-path\">{{ file.path }}</span>\n </div>\n <span class=\"search-item-size file-size\">{{ formatSearchResultSize(file.size) }}</span>\n <span class=\"search-item-date file-date\">{{ formatSearchResultDate(file.lastModified) }}</span>\n </div>\n }\n </div>\n }\n @if (accountResult.success && accountResult.results.length === 0) {\n <div class=\"group-empty-text no-results-message\">No matching files found in this account</div>\n }\n </div>\n }\n </div>\n </div>\n }\n </div>\n }\n\n <!-- Loading State -->\n @if (isLoading && !isMultiProviderSearchMode) {\n <div class=\"grid-loading-container loading-state\">\n <mj-loading text=\"Loading files...\"></mj-loading>\n </div>\n }\n\n <!-- Error State -->\n @if (errorMessage && !isLoading) {\n <mj-empty-state\n class=\"file-grid-empty-fill\"\n Variant=\"error\"\n Icon=\"fa-solid fa-triangle-exclamation\"\n Title=\"Unable to load files\"\n [Message]=\"errorMessage\"\n ActionText=\"Retry\"\n ActionIcon=\"fa-solid fa-arrows-rotate\"\n (Action)=\"refresh()\" />\n }\n\n <!-- Grid View -->\n @if (!isLoading && !errorMessage && filteredItems.length > 0 && viewMode === 'grid') {\n <div class=\"file-cards-grid file-grid file-grid-wrapper\">\n @for (item of filteredItems; track item.Key) {\n <div\n class=\"file-card grid-item\"\n [class.selected]=\"selectedItems.includes(item.Key)\"\n (click)=\"onTileClick(item, $event)\"\n (dblclick)=\"onItemDoubleClick(item)\"\n [title]=\"item.name\">\n\n <!-- Card Top Row: Checkbox & Quick Actions -->\n <div class=\"file-card-header\">\n <label class=\"file-card-checkbox\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedItems.includes(item.Key)\"\n (change)=\"toggleItemSelection(item, $event)\" />\n </label>\n <div class=\"file-card-quick-actions\" (click)=\"$event.stopPropagation()\">\n @if (item.Type === 'file') {\n <button\n type=\"button\"\n class=\"quick-action-btn\"\n (click)=\"openPreview(item)\"\n title=\"Preview\">\n <i class=\"fa-solid fa-eye\"></i>\n </button>\n <button\n type=\"button\"\n class=\"quick-action-btn\"\n (click)=\"downloadFile(item)\"\n title=\"Download\">\n <i class=\"fa-solid fa-download\"></i>\n </button>\n }\n </div>\n </div>\n\n <!-- Card Thumbnail/Icon -->\n <div class=\"file-card-icon-container grid-icon\" [class]=\"getItemColorClass(item)\">\n <i [class]=\"getItemIcon(item)\"></i>\n </div>\n\n <!-- Card Body: Name & Meta -->\n <div class=\"file-card-body\">\n <span class=\"file-card-name grid-name\">{{ item.name }}</span>\n <div class=\"file-card-meta grid-meta\">\n <span class=\"meta-size\">{{ item.Type === 'folder' ? 'Folder' : formatFileSize(item.Size) }}</span>\n <span class=\"meta-date\">{{ formatDate(item.LastModified) }}</span>\n </div>\n </div>\n </div>\n }\n </div>\n }\n\n <!-- List View Table -->\n @if (!isLoading && !errorMessage && filteredItems.length > 0 && viewMode === 'list') {\n <div class=\"file-table-container file-table-wrapper\">\n <table class=\"file-table\">\n <thead>\n <tr>\n <th class=\"th-check\">\n <input\n type=\"checkbox\"\n [checked]=\"areAllSelected()\"\n (change)=\"areAllSelected() ? deselectAll() : selectAll()\" />\n </th>\n <th class=\"th-name col-name\" (click)=\"onSortChange([{ field: 'name', dir: sort[0].field === 'name' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Name</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'name' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'name' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'name'\"></i>\n </th>\n <th class=\"th-type col-type\">Type</th>\n <th class=\"th-size col-size\" (click)=\"onSortChange([{ field: 'size', dir: sort[0].field === 'size' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Size</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'size' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'size' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'size'\"></i>\n </th>\n <th class=\"th-date col-modified\" (click)=\"onSortChange([{ field: 'lastModified', dir: sort[0].field === 'lastModified' && sort[0].dir === 'asc' ? 'desc' : 'asc' }])\">\n <span>Modified</span>\n <i class=\"fa-solid sort-arrow sort-icon\"\n [class.fa-caret-up]=\"sort[0].field === 'lastModified' && sort[0].dir === 'asc'\"\n [class.fa-caret-down]=\"sort[0].field === 'lastModified' && sort[0].dir === 'desc'\"\n [class.fa-sort]=\"sort[0].field !== 'lastModified'\"></i>\n </th>\n <th class=\"th-actions\">Actions</th>\n </tr>\n </thead>\n <tbody>\n @for (item of filteredItems; track item.Key) {\n <tr\n class=\"file-table-row file-row\"\n [class.selected]=\"selectedItems.includes(item.Key)\"\n (click)=\"onTileClick(item, $event)\"\n (dblclick)=\"onItemDoubleClick(item)\">\n <td class=\"td-check\" (click)=\"$event.stopPropagation()\">\n <input\n type=\"checkbox\"\n [checked]=\"selectedItems.includes(item.Key)\"\n (change)=\"toggleItemSelection(item, $event)\" />\n </td>\n <td class=\"td-name col-name\">\n <div class=\"row-name-wrap\">\n <div class=\"row-icon-badge\" [class]=\"getItemColorClass(item)\">\n <i [class]=\"getItemIcon(item)\" class=\"item-icon\"></i>\n </div>\n <span class=\"row-file-name item-name\" [title]=\"item.name\">{{ item.name }}</span>\n </div>\n </td>\n <td class=\"td-type col-type\">\n <span class=\"type-badge item-type\">{{ getFileType(item) }}</span>\n </td>\n <td class=\"td-size col-size\">\n <span class=\"size-text item-size\">{{ item.Type === 'folder' ? '\u2014' : formatFileSize(item.Size) }}</span>\n </td>\n <td class=\"td-date col-modified\">\n <span class=\"date-text item-date\">{{ formatDate(item.LastModified) }}</span>\n </td>\n <td class=\"td-actions\" (click)=\"$event.stopPropagation()\">\n <div class=\"row-action-buttons\">\n @if (item.Type === 'file') {\n <button\n type=\"button\"\n class=\"row-act-btn\"\n (click)=\"openPreview(item)\"\n title=\"Preview\">\n <i class=\"fa-solid fa-eye\"></i>\n </button>\n <button\n type=\"button\"\n class=\"row-act-btn\"\n (click)=\"downloadFile(item)\"\n title=\"Download\">\n <i class=\"fa-solid fa-download\"></i>\n </button>\n }\n </div>\n </td>\n </tr>\n }\n </tbody>\n </table>\n </div>\n }\n\n <!-- Empty State -->\n @if (!isLoading && !errorMessage && filteredItems.length === 0 && !isMultiProviderSearchMode) {\n <mj-empty-state\n Icon=\"fa-solid fa-folder-open\"\n Title=\"This folder is empty\"\n Message=\"Drop files here to upload or create a new folder\"\n ActionText=\"Upload Files\"\n ActionIcon=\"fa-solid fa-cloud-arrow-up\"\n (Action)=\"onUploadClick()\" />\n }\n\n <!-- Bottom Status Bar -->\n @if (!isLoading && !errorMessage) {\n <div class=\"status-bar\">\n <span class=\"status-count\">\n {{ filteredItems.length }} item{{ filteredItems.length === 1 ? '' : 's' }}\n @if (searchQuery) {\n (filtered)\n }\n </span>\n <div class=\"status-account-badge\" *ngIf=\"account\">\n <i class=\"fa-solid fa-cloud\"></i>\n <span>{{ account.account.Name }} ({{ account.provider.Name }})</span>\n </div>\n </div>\n }\n\n <!-- Drag & Drop Upload Overlay -->\n @if (isDragging) {\n <div class=\"drag-drop-overlay\">\n <div class=\"drag-drop-content\">\n <i class=\"fa-solid fa-cloud-arrow-up drag-icon\"></i>\n <h3>Drop files here to upload</h3>\n <p>Files will be stored in {{ folderPath || 'Root' }}</p>\n </div>\n </div>\n }\n\n <!-- Upload Progress Bar -->\n @if (isUploading) {\n <div class=\"upload-progress-bar-wrap\">\n <div class=\"upload-progress-card\">\n <div class=\"upload-info\">\n <i class=\"fa-solid fa-spinner fa-spin upload-spinner\"></i>\n <span class=\"upload-text\">Uploading {{ uploadingFileName }}...</span>\n </div>\n <div class=\"progress-track\">\n <div class=\"progress-fill\" [style.width.%]=\"uploadProgress\"></div>\n </div>\n </div>\n </div>\n }\n</div>\n\n<!-- ==========================================================================\n MODAL DIALOGS (Design Tokens, Smooth Animations, Backdrop Blurs)\n ========================================================================== -->\n\n<!-- File Preview Modal -->\n@if (showPreviewModal && previewItem) {\n <div class=\"mj-modal-backdrop\" (click)=\"closePreview()\">\n <div class=\"mj-modal-window preview-modal-window\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <div class=\"modal-title-wrap\">\n <div class=\"row-icon-badge\" [class]=\"getItemColorClass(previewItem)\">\n <i [class]=\"getItemIcon(previewItem)\"></i>\n </div>\n <div>\n <h3 class=\"modal-title\">{{ previewItem.name }}</h3>\n <span class=\"modal-subtitle\">{{ formatFileSize(previewItem.Size) }} • {{ formatDate(previewItem.LastModified) }}</span>\n </div>\n </div>\n <div class=\"modal-header-actions\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"downloadFile(previewItem)\"\n title=\"Download file\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download</span>\n </button>\n <button\n type=\"button\"\n class=\"modal-close-btn\"\n (click)=\"closePreview()\"\n title=\"Close preview\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n </div>\n\n <div class=\"preview-modal-body\">\n @if (isLoadingPreview) {\n <div class=\"preview-loading\">\n <mj-loading text=\"Loading preview...\"></mj-loading>\n </div>\n } @else if (previewUrl) {\n @switch (previewMediaType) {\n @case ('image') {\n <div class=\"preview-media-wrap image-preview-wrap\">\n <img [src]=\"previewUrl\" [alt]=\"previewItem.name\" class=\"preview-img\" />\n </div>\n }\n @case ('pdf') {\n <div class=\"preview-media-wrap pdf-preview-wrap\">\n <iframe [src]=\"previewUrl\" class=\"preview-iframe\" title=\"PDF Preview\"></iframe>\n </div>\n }\n @case ('video') {\n <div class=\"preview-media-wrap video-preview-wrap\">\n <video [src]=\"previewUrl\" controls autoplay class=\"preview-video\"></video>\n </div>\n }\n @case ('audio') {\n <div class=\"preview-media-wrap audio-preview-wrap\">\n <i class=\"fa-solid fa-music preview-audio-icon\"></i>\n <audio [src]=\"previewUrl\" controls class=\"preview-audio\"></audio>\n </div>\n }\n @default {\n <div class=\"preview-fallback\">\n <i [class]=\"getItemIcon(previewItem)\" class=\"preview-fallback-icon\"></i>\n <h4>{{ previewItem.name }}</h4>\n <p>Preview is not supported for this file type.</p>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"downloadFile(previewItem)\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download File</span>\n </button>\n </div>\n }\n }\n } @else {\n <div class=\"preview-fallback\">\n <i class=\"fa-solid fa-triangle-exclamation preview-fallback-icon\"></i>\n <h4>Preview unavailable</h4>\n <p>Could not generate a preview link for this file.</p>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"downloadFile(previewItem)\">\n <i class=\"fa-solid fa-download\"></i>\n <span>Download File</span>\n </button>\n </div>\n }\n </div>\n </div>\n </div>\n}\n\n<!-- New Folder Modal -->\n@if (showNewFolderDialog) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelNewFolder()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Create New Folder</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelNewFolder()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <label class=\"form-label\">Folder name:</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"newFolderName\"\n [disabled]=\"isCreatingFolder\"\n placeholder=\"Enter folder name\"\n (keydown.enter)=\"onCreateFolder()\"\n autofocus />\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelNewFolder()\"\n [disabled]=\"isCreatingFolder\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onCreateFolder()\"\n [disabled]=\"!newFolderName.trim() || isCreatingFolder\">\n @if (isCreatingFolder) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCreatingFolder ? 'Creating...' : 'Create' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Delete Confirmation Modal -->\n@if (showDeleteDialog && itemToDelete) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelDelete()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Delete {{ itemToDelete.Type === 'folder' ? 'Folder' : 'File' }}?</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelDelete()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <p class=\"modal-body-text\">\n Are you sure you want to delete <strong>{{ itemToDelete.name }}</strong>?\n </p>\n @if (itemToDelete.Type === 'folder') {\n <div class=\"modal-warning-banner warning\">\n <i class=\"fa-solid fa-triangle-exclamation\"></i>\n <span>This will delete the folder and all its contents.</span>\n </div>\n }\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelDelete()\"\n [disabled]=\"isDeleting\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--danger danger\"\n (click)=\"onConfirmDelete()\"\n [disabled]=\"isDeleting\">\n @if (isDeleting) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isDeleting ? 'Deleting...' : 'Delete' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Rename Modal -->\n@if (showRenameDialog && itemToRename) {\n <div class=\"mj-modal-backdrop modal-overlay\" (click)=\"onCancelRename()\">\n <div class=\"mj-modal-window modal-compact modal-dialog\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header modal-header\">\n <h3 class=\"modal-title\">Rename {{ itemToRename.Type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn modal-close\" (click)=\"onCancelRename()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body modal-body\">\n <label class=\"form-label\">New name:</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"newItemName\"\n [disabled]=\"isRenaming\"\n placeholder=\"Enter new name\"\n (keydown.enter)=\"onConfirmRename()\"\n autofocus />\n </div>\n <div class=\"mj-modal-footer modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelRename()\"\n [disabled]=\"isRenaming\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmRename()\"\n [disabled]=\"isRenaming || !newItemName.trim()\">\n @if (isRenaming) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isRenaming ? 'Renaming...' : 'Rename' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Copy Modal -->\n@if (showCopyDialog && itemToCopy) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelCopy()\">\n <div class=\"mj-modal-window modal-compact\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Copy {{ itemToCopy.Type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelCopy()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">Copying <strong>{{ itemToCopy.name }}</strong></p>\n <label class=\"form-label\">Destination Path</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"copyDestinationPath\"\n [disabled]=\"isCopying\"\n placeholder=\"path/to/destination\"\n (keydown.enter)=\"onConfirmCopy()\" />\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelCopy()\"\n [disabled]=\"isCopying\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmCopy()\"\n [disabled]=\"isCopying || !copyDestinationPath.trim()\">\n @if (isCopying) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCopying ? 'Copying...' : 'Copy' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Move Modal -->\n@if (showMoveDialog && itemToMove) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelMove()\">\n <div class=\"mj-modal-window modal-compact\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Move {{ itemToMove.Type === 'folder' ? 'Folder' : 'File' }}</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelMove()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">Moving <strong>{{ itemToMove.name }}</strong></p>\n <label class=\"form-label\">Destination Folder</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"moveDestinationPath\"\n [disabled]=\"isMoving\"\n placeholder=\"destination/folder/\"\n (keydown.enter)=\"onConfirmMove()\" />\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelMove()\"\n [disabled]=\"isMoving\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmMove()\"\n [disabled]=\"isMoving || !moveDestinationPath.trim()\">\n @if (isMoving) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isMoving ? 'Moving...' : 'Move' }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n\n<!-- Copy to Account Modal -->\n@if (showCopyToProviderDialog && itemToCopyToProvider) {\n <div class=\"mj-modal-backdrop\" (click)=\"onCancelCopyToAccount()\">\n <div class=\"mj-modal-window\" (click)=\"$event.stopPropagation()\">\n <div class=\"mj-modal-header\">\n <h3 class=\"modal-title\">Copy to Another Account</h3>\n <button type=\"button\" class=\"modal-close-btn\" (click)=\"onCancelCopyToAccount()\">\n <i class=\"fa-solid fa-xmark\"></i>\n </button>\n </div>\n <div class=\"mj-modal-body\">\n <p class=\"modal-body-text\">\n Copying <strong>{{ itemToCopyToProvider.name }}</strong> from <strong>{{ account?.account?.Name }}</strong>\n </p>\n\n <label class=\"form-label\">Select Destination Storage Accounts</label>\n <div class=\"account-picker-list\">\n @for (a of availableAccounts; track a.account.ID) {\n <label class=\"account-picker-item\" [for]=\"'dest-acc-' + a.account.ID\">\n <input\n type=\"checkbox\"\n [id]=\"'dest-acc-' + a.account.ID\"\n [checked]=\"isDestinationAccountSelected(a.account.ID)\"\n [disabled]=\"isCopyingToAccount\"\n (change)=\"toggleDestinationAccount(a.account.ID)\" />\n <span class=\"picker-name\">{{ a.account.Name }}</span>\n <span class=\"picker-badge\">{{ a.provider.Name }}</span>\n </label>\n }\n </div>\n\n <label class=\"form-label\" style=\"margin-top: 14px;\">Destination Filename</label>\n <input\n type=\"text\"\n class=\"form-input\"\n [(ngModel)]=\"copyToAccountDestinationPath\"\n [disabled]=\"isCopyingToAccount\"\n placeholder=\"Filename\"\n (keydown.enter)=\"onConfirmCopyToAccount()\" />\n\n @if (copyToAccountProgress) {\n <div class=\"copy-progress-indicator\">\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n <span>Copying to {{ copyToAccountProgress.currentAccount }} ({{ copyToAccountProgress.current }}/{{ copyToAccountProgress.total }})...</span>\n </div>\n }\n </div>\n <div class=\"mj-modal-footer\">\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--secondary\"\n (click)=\"onCancelCopyToAccount()\"\n [disabled]=\"isCopyingToAccount\">\n Cancel\n </button>\n <button\n type=\"button\"\n class=\"mj-action-btn mj-action-btn--primary\"\n (click)=\"onConfirmCopyToAccount()\"\n [disabled]=\"isCopyingToAccount || selectedDestinationAccounts.size === 0 || !copyToAccountDestinationPath.trim()\">\n @if (isCopyingToAccount) {\n <i class=\"fa-solid fa-spinner fa-spin\"></i>\n }\n <span>{{ isCopyingToAccount ? 'Copying...' : 'Copy to ' + selectedDestinationAccounts.size + ' Account' + (selectedDestinationAccounts.size !== 1 ? 's' : '') }}</span>\n </button>\n </div>\n </div>\n </div>\n}\n", styles: ["/* ==========================================================================\n File Grid & Table Component Styles\n Modern macOS Finder / Cloud Storage with Design Tokens\n ========================================================================== */\n\n:host {\n display: block;\n width: 100%;\n height: 100%;\n overflow: hidden;\n}\n\n.file-grid-container {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n position: relative;\n overflow: hidden;\n}\n\n/* ==========================================================================\n Command Bar (Top Navigation, Search & Primary Actions)\n ========================================================================== */\n\n.command-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 12px;\n padding: 10px 16px;\n background-color: var(--mj-bg-surface);\n border-bottom: 1px solid var(--mj-border-default);\n flex-shrink: 0;\n flex-wrap: wrap;\n}\n\n.command-bar-left {\n display: flex;\n align-items: center;\n gap: 12px;\n min-width: 0;\n flex: 1;\n}\n\n.nav-btn-group {\n display: flex;\n align-items: center;\n gap: 4px;\n flex-shrink: 0;\n}\n\n.mj-tool-btn {\n width: 32px;\n height: 32px;\n min-width: 32px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-secondary);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-tool-btn:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n border-color: var(--mj-brand-primary);\n}\n\n.mj-tool-btn[disabled] {\n opacity: 0.4;\n cursor: not-allowed;\n background-color: transparent;\n}\n\n/* Breadcrumb Trail */\n.breadcrumb-trail {\n display: flex;\n align-items: center;\n gap: 4px;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none;\n min-width: 0;\n padding: 2px 4px;\n}\n\n.breadcrumb-trail::-webkit-scrollbar {\n display: none;\n}\n\n.trail-root-icon {\n color: var(--mj-status-warning, #f59e0b);\n font-size: 0.95rem;\n margin-right: 2px;\n flex-shrink: 0;\n}\n\n.crumb-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 0.82rem;\n font-weight: 500;\n padding: 4px 8px;\n border-radius: var(--mj-radius-sm, 6px);\n cursor: pointer;\n white-space: nowrap;\n transition: all 0.12s ease;\n}\n\n.crumb-btn:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n}\n\n.crumb-btn.current {\n color: var(--mj-brand-primary);\n font-weight: 600;\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.crumb-separator {\n color: var(--mj-text-muted);\n font-size: 0.75rem;\n}\n\n/* Command Bar Right */\n.command-bar-right {\n display: flex;\n align-items: center;\n gap: 8px;\n flex-shrink: 0;\n}\n\n/* Search Pill */\n.search-pill {\n display: flex;\n align-items: center;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 4%, var(--mj-bg-surface)));\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-full, 9999px);\n padding: 2px 10px;\n gap: 6px;\n transition: border-color 0.15s ease;\n}\n\n.search-pill:focus-within {\n border-color: var(--mj-brand-primary);\n}\n\n.search-icon {\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n}\n\n.search-input {\n border: none;\n background: transparent;\n outline: none;\n font-size: 0.82rem;\n color: var(--mj-text-primary);\n width: 150px;\n padding: 4px 0;\n}\n\n.search-input::placeholder {\n color: var(--mj-text-muted);\n}\n\n.search-clear-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-muted);\n cursor: pointer;\n padding: 0 4px;\n font-size: 0.75rem;\n}\n\n.search-clear-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.type-filter-select {\n border: none;\n background: transparent;\n font-size: 0.76rem;\n color: var(--mj-text-secondary);\n outline: none;\n cursor: pointer;\n padding: 2px 4px;\n border-left: 1px solid var(--mj-border-default);\n}\n\n/* Toggle Multi-Search Button */\n.mj-btn-search-toggle {\n display: flex;\n align-items: center;\n gap: 6px;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-secondary);\n font-size: 0.8rem;\n font-weight: 500;\n padding: 6px 12px;\n border-radius: var(--mj-radius-full, 9999px);\n cursor: pointer;\n transition: all 0.15s ease;\n}\n\n.mj-btn-search-toggle:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n color: var(--mj-text-primary);\n}\n\n.mj-btn-search-toggle.active {\n background-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-brand-primary);\n}\n\n/* View Mode Toggle */\n.view-mode-toggle {\n display: flex;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 4%, var(--mj-bg-surface)));\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n padding: 2px;\n gap: 2px;\n}\n\n.view-mode-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n padding: 5px 8px;\n font-size: 0.8rem;\n border-radius: 4px;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.view-mode-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.view-mode-btn.active {\n background-color: var(--mj-bg-surface);\n color: var(--mj-brand-primary);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.08);\n}\n\n/* Action Buttons */\n.mj-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 6px 12px;\n font-size: 0.82rem;\n font-weight: 500;\n border-radius: var(--mj-radius-sm, 6px);\n cursor: pointer;\n transition: all 0.15s ease;\n border: 1px solid transparent;\n white-space: nowrap;\n}\n\n.mj-action-btn--primary {\n background-color: var(--mj-brand-primary, #3b82f6);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-brand-primary, #3b82f6);\n}\n\n.mj-action-btn--primary:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 85%, var(--mj-text-primary, #000000));\n}\n\n.mj-action-btn--secondary {\n background-color: var(--mj-bg-surface);\n border-color: var(--mj-border-default);\n color: var(--mj-text-primary);\n}\n\n.mj-action-btn--secondary:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n}\n\n.mj-action-btn--danger {\n background-color: var(--mj-status-error, #ef4444);\n color: var(--mj-text-inverse, #ffffff);\n border-color: var(--mj-status-error, #ef4444);\n}\n\n.mj-action-btn--danger:hover:not([disabled]) {\n background-color: color-mix(in srgb, var(--mj-status-error, #ef4444) 85%, var(--mj-text-primary, #000000));\n}\n\n.mj-action-btn[disabled] {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n/* ==========================================================================\n Selection Action Bar\n ========================================================================== */\n\n.selection-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 8px 16px;\n background-color: color-mix(in srgb, var(--mj-brand-primary) 8%, var(--mj-bg-surface));\n border-bottom: 1px solid color-mix(in srgb, var(--mj-brand-primary) 25%, transparent);\n gap: 12px;\n flex-shrink: 0;\n animation: slideDown 0.18s ease-out;\n}\n\n@keyframes slideDown {\n from { transform: translateY(-100%); opacity: 0; }\n to { transform: translateY(0); opacity: 1; }\n}\n\n.selection-bar-left {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.selection-checkbox-wrap {\n display: flex;\n align-items: center;\n gap: 8px;\n cursor: pointer;\n user-select: none;\n}\n\n.selection-count {\n font-size: 0.82rem;\n font-weight: 600;\n color: var(--mj-brand-primary);\n}\n\n.selection-bar-right {\n display: flex;\n align-items: center;\n gap: 6px;\n flex-wrap: wrap;\n}\n\n.sel-action-btn {\n display: inline-flex;\n align-items: center;\n gap: 5px;\n padding: 4px 10px;\n font-size: 0.76rem;\n font-weight: 500;\n border-radius: var(--mj-radius-sm, 6px);\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n color: var(--mj-text-primary);\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.sel-action-btn:hover {\n background-color: color-mix(in srgb, var(--mj-text-primary) 6%, var(--mj-bg-surface));\n border-color: var(--mj-brand-primary);\n color: var(--mj-brand-primary);\n}\n\n.sel-action-btn--danger:hover {\n background-color: color-mix(in srgb, var(--mj-status-error) 10%, var(--mj-bg-surface));\n border-color: var(--mj-status-error);\n color: var(--mj-status-error);\n}\n\n.sel-action-btn--ghost {\n background: transparent;\n border-color: transparent;\n}\n\n/* ==========================================================================\n Multi-Account Search Card\n ========================================================================== */\n\n.multi-search-card {\n margin: 12px 16px;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 10px);\n box-shadow: var(--mj-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));\n overflow: hidden;\n animation: fadeIn 0.2s ease;\n}\n\n@keyframes fadeIn {\n from { opacity: 0; transform: translateY(-6px); }\n to { opacity: 1; transform: translateY(0); }\n}\n\n.multi-search-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 10px 16px;\n background-color: var(--mj-bg-surface-sunken);\n border-bottom: 1px solid var(--mj-border-default);\n}\n\n.multi-search-title {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--mj-brand-primary);\n}\n\n.multi-search-title h3 {\n margin: 0;\n font-size: 0.88rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.multi-search-body {\n padding: 14px 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.multi-search-bar {\n display: flex;\n gap: 8px;\n}\n\n.multi-search-text {\n flex: 1;\n padding: 8px 12px;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.85rem;\n outline: none;\n}\n\n.multi-search-text:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.multi-search-accounts {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.account-select-label {\n font-size: 0.74rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n text-transform: uppercase;\n}\n\n.account-pill-list {\n display: flex;\n flex-wrap: wrap;\n gap: 6px;\n}\n\n.account-select-pill {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n padding: 4px 10px;\n border-radius: var(--mj-radius-full, 9999px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface-sunken);\n font-size: 0.76rem;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.account-select-pill.checked {\n border-color: var(--mj-brand-primary);\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.account-select-pill.disabled {\n opacity: 0.4;\n cursor: not-allowed;\n}\n\n.pill-account-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.pill-provider-tag {\n font-size: 0.68rem;\n color: var(--mj-text-muted);\n}\n\n.pill-no-search {\n font-size: 0.68rem;\n color: var(--mj-status-warning);\n}\n\n/* Results */\n.multi-search-results {\n border-top: 1px solid var(--mj-border-default);\n padding: 12px 16px;\n max-height: 400px;\n overflow-y: auto;\n}\n\n.results-meta-bar {\n display: flex;\n align-items: center;\n gap: 10px;\n margin-bottom: 12px;\n font-size: 0.8rem;\n}\n\n.meta-count {\n color: var(--mj-text-primary);\n}\n\n.meta-stats {\n color: var(--mj-text-secondary);\n}\n\n.clear-results-btn {\n margin-left: auto;\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 0.76rem;\n cursor: pointer;\n}\n\n.clear-results-btn:hover {\n color: var(--mj-text-primary);\n}\n\n.results-groups {\n display: flex;\n flex-direction: column;\n gap: 12px;\n}\n\n.result-group {\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n overflow: hidden;\n}\n\n.result-group-head {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background-color: var(--mj-bg-surface-sunken);\n font-size: 0.8rem;\n}\n\n.group-account-name {\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.group-count-badge {\n margin-left: auto;\n background-color: var(--mj-brand-primary);\n color: var(--mj-text-inverse, #ffffff);\n font-size: 0.7rem;\n font-weight: 600;\n padding: 1px 6px;\n border-radius: 9999px;\n}\n\n.group-error-badge {\n margin-left: auto;\n color: var(--mj-status-error);\n font-size: 0.72rem;\n}\n\n.result-items-grid {\n display: flex;\n flex-direction: column;\n}\n\n.search-item-row {\n display: flex;\n align-items: center;\n gap: 10px;\n padding: 8px 12px;\n border-bottom: 1px solid var(--mj-border-default);\n font-size: 0.8rem;\n}\n\n.search-item-row:last-child {\n border-bottom: none;\n}\n\n.search-item-icon {\n color: var(--mj-brand-primary);\n}\n\n.search-item-info {\n display: flex;\n flex-direction: column;\n flex: 1;\n min-width: 0;\n}\n\n.search-item-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.search-item-path {\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.search-item-size, .search-item-date {\n font-size: 0.74rem;\n color: var(--mj-text-secondary);\n}\n\n.group-empty-text {\n padding: 12px;\n font-size: 0.78rem;\n color: var(--mj-text-muted);\n text-align: center;\n}\n\n/* ==========================================================================\n Cards Grid View\n ========================================================================== */\n\n.file-cards-grid {\n flex: 1;\n overflow-y: auto;\n padding: 16px;\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));\n gap: 14px;\n align-content: start;\n}\n\n.file-card {\n display: flex;\n flex-direction: column;\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 10px);\n padding: 10px;\n cursor: pointer;\n transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);\n position: relative;\n user-select: none;\n}\n\n.file-card:hover {\n border-color: var(--mj-brand-primary);\n box-shadow: var(--mj-shadow-md, 0 4px 12px rgba(0, 0, 0, 0.08));\n transform: translateY(-2px);\n}\n\n.file-card.selected {\n border-color: var(--mj-brand-primary);\n background-color: color-mix(in srgb, var(--mj-brand-primary) 6%, var(--mj-bg-surface));\n box-shadow: 0 0 0 1px var(--mj-brand-primary);\n}\n\n.file-card-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n height: 22px;\n margin-bottom: 6px;\n}\n\n.file-card-checkbox {\n cursor: pointer;\n}\n\n.file-card-quick-actions {\n display: flex;\n gap: 4px;\n opacity: 0;\n transition: opacity 0.15s ease;\n}\n\n.file-card:hover .file-card-quick-actions {\n opacity: 1;\n}\n\n.quick-action-btn {\n width: 22px;\n height: 22px;\n padding: 0;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n color: var(--mj-text-secondary);\n font-size: 0.68rem;\n cursor: pointer;\n transition: all 0.12s ease;\n}\n\n.quick-action-btn:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n/* Card Icon / Thumbnail */\n.file-card-icon-container {\n height: 74px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-md, 8px);\n margin-bottom: 8px;\n font-size: 2rem;\n transition: transform 0.15s ease;\n}\n\n.file-card:hover .file-card-icon-container {\n transform: scale(1.04);\n}\n\n/* Card Body */\n.file-card-body {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.file-card-name {\n font-size: 0.8rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 2;\n -webkit-box-orient: vertical;\n line-height: 1.25;\n min-height: 2em;\n}\n\n.file-card-meta {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n/* ==========================================================================\n List View Table\n ========================================================================== */\n\n.file-table-container {\n flex: 1;\n overflow-y: auto;\n padding: 8px 16px;\n}\n\n.file-table {\n width: 100%;\n border-collapse: collapse;\n font-size: 0.82rem;\n}\n\n.file-table th {\n text-align: left;\n padding: 8px 10px;\n border-bottom: 1px solid var(--mj-border-default);\n color: var(--mj-text-muted);\n font-size: 0.72rem;\n font-weight: 600;\n text-transform: uppercase;\n letter-spacing: 0.04em;\n user-select: none;\n cursor: pointer;\n}\n\n.file-table th.th-check {\n width: 32px;\n cursor: default;\n}\n\n.file-table th.th-actions {\n width: 70px;\n text-align: right;\n cursor: default;\n}\n\n.sort-arrow {\n margin-left: 4px;\n font-size: 0.65rem;\n}\n\n.file-table-row {\n border-bottom: 1px solid var(--mj-border-default);\n cursor: pointer;\n transition: background-color 0.12s ease;\n}\n\n.file-table-row:hover {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 4%, var(--mj-bg-surface));\n}\n\n.file-table-row.selected {\n background-color: color-mix(in srgb, var(--mj-brand-primary) 10%, var(--mj-bg-surface));\n}\n\n.file-table-row td {\n padding: 8px 10px;\n vertical-align: middle;\n}\n\n.row-name-wrap {\n display: flex;\n align-items: center;\n gap: 10px;\n min-width: 0;\n}\n\n.row-icon-badge {\n width: 28px;\n height: 28px;\n min-width: 28px;\n display: flex;\n align-items: center;\n justify-content: center;\n border-radius: var(--mj-radius-sm, 6px);\n font-size: 0.9rem;\n}\n\n.row-file-name {\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.type-badge {\n display: inline-block;\n padding: 2px 6px;\n border-radius: 4px;\n background-color: var(--mj-bg-surface-sunken);\n color: var(--mj-text-secondary);\n font-size: 0.7rem;\n}\n\n.size-text, .date-text {\n color: var(--mj-text-secondary);\n font-size: 0.76rem;\n white-space: nowrap;\n}\n\n.row-action-buttons {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 4px;\n opacity: 0;\n transition: opacity 0.12s ease;\n}\n\n.file-table-row:hover .row-action-buttons {\n opacity: 1;\n}\n\n.row-act-btn {\n width: 24px;\n height: 24px;\n display: flex;\n align-items: center;\n justify-content: center;\n background: var(--mj-bg-surface-sunken);\n border: 1px solid var(--mj-border-default);\n border-radius: 4px;\n color: var(--mj-text-secondary);\n font-size: 0.7rem;\n cursor: pointer;\n}\n\n.row-act-btn:hover {\n color: var(--mj-brand-primary);\n border-color: var(--mj-brand-primary);\n}\n\n/* ==========================================================================\n Media Color Badges\n ========================================================================== */\n\n.mj-file-thumb--folder {\n background-color: color-mix(in srgb, var(--mj-status-warning, #f59e0b) 14%, transparent);\n color: var(--mj-status-warning, #f59e0b);\n}\n\n.mj-file-thumb--pdf {\n background-color: color-mix(in srgb, var(--mj-status-error, #ef4444) 14%, transparent);\n color: var(--mj-status-error, #ef4444);\n}\n\n.mj-file-thumb--image {\n background-color: color-mix(in srgb, var(--mj-media-image, #8b5cf6) 14%, transparent);\n color: var(--mj-media-image, #8b5cf6);\n}\n\n.mj-file-thumb--video {\n background-color: color-mix(in srgb, var(--mj-brand-primary, #3b82f6) 14%, transparent);\n color: var(--mj-brand-primary, #3b82f6);\n}\n\n.mj-file-thumb--audio {\n background-color: color-mix(in srgb, var(--mj-status-warning, #f59e0b) 14%, transparent);\n color: var(--mj-status-warning, #f59e0b);\n}\n\n.mj-file-thumb--code {\n background-color: color-mix(in srgb, var(--mj-status-success, #10b981) 14%, transparent);\n color: var(--mj-status-success, #10b981);\n}\n\n.mj-file-thumb--text {\n background-color: color-mix(in srgb, var(--mj-text-muted, #6b7280) 14%, transparent);\n color: var(--mj-text-muted, #6b7280);\n}\n\n.mj-file-thumb--document {\n background-color: color-mix(in srgb, var(--mj-brand-primary-hover, #2563eb) 14%, transparent);\n color: var(--mj-brand-primary-hover, #2563eb);\n}\n\n/* ==========================================================================\n Status Bar (Bottom Information)\n ========================================================================== */\n\n.status-bar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 6px 16px;\n background-color: var(--mj-bg-surface-sunken, color-mix(in srgb, var(--mj-text-primary) 3%, var(--mj-bg-surface)));\n border-top: 1px solid var(--mj-border-default);\n font-size: 0.72rem;\n color: var(--mj-text-secondary);\n flex-shrink: 0;\n}\n\n.status-account-badge {\n display: flex;\n align-items: center;\n gap: 6px;\n color: var(--mj-brand-primary);\n font-weight: 500;\n}\n\n/* ==========================================================================\n Drag & Drop Overlay & Progress\n ========================================================================== */\n\n.drag-drop-overlay {\n position: absolute;\n inset: 0;\n background: color-mix(in srgb, var(--mj-brand-primary) 12%, rgba(255, 255, 255, 0.9));\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 100;\n pointer-events: none;\n}\n\n.drag-drop-content {\n display: flex;\n flex-direction: column;\n align-items: center;\n padding: 32px 48px;\n background-color: var(--mj-bg-surface);\n border: 2px dashed var(--mj-brand-primary);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-lg, 0 10px 25px rgba(0, 0, 0, 0.15));\n}\n\n.drag-icon {\n font-size: 3rem;\n color: var(--mj-brand-primary);\n margin-bottom: 12px;\n}\n\n.drag-drop-content h3 {\n margin: 0 0 6px 0;\n font-size: 1.1rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.drag-drop-content p {\n margin: 0;\n font-size: 0.8rem;\n color: var(--mj-text-secondary);\n}\n\n.upload-progress-bar-wrap {\n position: absolute;\n bottom: 36px;\n right: 20px;\n z-index: 90;\n}\n\n.upload-progress-card {\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-md, 8px);\n padding: 10px 14px;\n box-shadow: var(--mj-shadow-lg, 0 8px 20px rgba(0, 0, 0, 0.15));\n width: 260px;\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.upload-info {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.upload-spinner {\n color: var(--mj-brand-primary);\n}\n\n.upload-text {\n font-size: 0.78rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.progress-track {\n height: 4px;\n background-color: var(--mj-bg-surface-sunken);\n border-radius: 9999px;\n overflow: hidden;\n}\n\n.progress-fill {\n height: 100%;\n background-color: var(--mj-brand-primary);\n transition: width 0.15s ease;\n}\n\n.grid-loading-container {\n display: flex;\n align-items: center;\n justify-content: center;\n flex: 1;\n}\n\n/* ==========================================================================\n Modals (Design Tokens & Glass Backdrop)\n ========================================================================== */\n\n.mj-modal-backdrop {\n position: fixed;\n inset: 0;\n background: rgba(0, 0, 0, 0.5);\n backdrop-filter: blur(4px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 1050;\n padding: 20px;\n animation: modalFadeIn 0.18s ease-out;\n}\n\n@keyframes modalFadeIn {\n from { opacity: 0; }\n to { opacity: 1; }\n}\n\n.mj-modal-window {\n background-color: var(--mj-bg-surface);\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-lg, 12px);\n box-shadow: var(--mj-shadow-xl, 0 20px 35px rgba(0, 0, 0, 0.25));\n width: 100%;\n max-width: 500px;\n overflow: hidden;\n display: flex;\n flex-direction: column;\n}\n\n.mj-modal-window.modal-compact {\n max-width: 420px;\n}\n\n.mj-modal-window.preview-modal-window {\n max-width: 850px;\n max-height: 85vh;\n height: 80vh;\n}\n\n.mj-modal-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n padding: 12px 18px;\n border-bottom: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n}\n\n.modal-title-wrap {\n display: flex;\n align-items: center;\n gap: 10px;\n}\n\n.modal-title {\n margin: 0;\n font-size: 0.95rem;\n font-weight: 600;\n color: var(--mj-text-primary);\n}\n\n.modal-subtitle {\n font-size: 0.72rem;\n color: var(--mj-text-muted);\n}\n\n.modal-header-actions {\n display: flex;\n align-items: center;\n gap: 8px;\n}\n\n.modal-close-btn {\n background: transparent;\n border: none;\n color: var(--mj-text-secondary);\n font-size: 1rem;\n cursor: pointer;\n padding: 4px;\n border-radius: 4px;\n}\n\n.modal-close-btn:hover {\n color: var(--mj-text-primary);\n background-color: var(--mj-bg-surface-sunken);\n}\n\n.mj-modal-body {\n padding: 18px;\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.form-label {\n font-size: 0.78rem;\n font-weight: 600;\n color: var(--mj-text-secondary);\n}\n\n.form-input {\n padding: 8px 12px;\n border-radius: var(--mj-radius-sm, 6px);\n border: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface);\n color: var(--mj-text-primary);\n font-size: 0.85rem;\n outline: none;\n}\n\n.form-input:focus {\n border-color: var(--mj-brand-primary);\n}\n\n.modal-body-text {\n margin: 0;\n font-size: 0.85rem;\n color: var(--mj-text-primary);\n}\n\n.modal-warning-banner {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 8px 12px;\n background-color: color-mix(in srgb, var(--mj-status-warning) 12%, var(--mj-bg-surface));\n border: 1px solid var(--mj-status-warning);\n border-radius: var(--mj-radius-sm, 6px);\n font-size: 0.76rem;\n color: var(--mj-text-primary);\n}\n\n.mj-modal-footer {\n display: flex;\n align-items: center;\n justify-content: flex-end;\n gap: 8px;\n padding: 12px 18px;\n border-top: 1px solid var(--mj-border-default);\n background-color: var(--mj-bg-surface-sunken);\n}\n\n/* Preview Modal Body */\n.preview-modal-body {\n flex: 1;\n display: flex;\n align-items: center;\n justify-content: center;\n background-color: var(--mj-bg-surface-sunken);\n padding: 16px;\n overflow: hidden;\n}\n\n.preview-media-wrap {\n width: 100%;\n height: 100%;\n display: flex;\n align-items: center;\n justify-content: center;\n}\n\n.preview-img {\n max-width: 100%;\n max-height: 100%;\n object-fit: contain;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.preview-iframe {\n width: 100%;\n height: 100%;\n border: none;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.preview-video {\n max-width: 100%;\n max-height: 100%;\n border-radius: var(--mj-radius-md, 8px);\n}\n\n.audio-preview-wrap {\n flex-direction: column;\n gap: 16px;\n}\n\n.preview-audio-icon {\n font-size: 3rem;\n color: var(--mj-brand-primary);\n}\n\n.preview-audio {\n width: 320px;\n}\n\n.preview-fallback {\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n text-align: center;\n color: var(--mj-text-secondary);\n}\n\n.preview-fallback-icon {\n font-size: 3rem;\n color: var(--mj-text-muted);\n margin-bottom: 8px;\n}\n\n.preview-fallback h4 {\n margin: 0;\n font-size: 0.95rem;\n color: var(--mj-text-primary);\n}\n\n.preview-fallback p {\n margin: 0;\n font-size: 0.8rem;\n}\n\n.account-picker-list {\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-height: 180px;\n overflow-y: auto;\n border: 1px solid var(--mj-border-default);\n border-radius: var(--mj-radius-sm, 6px);\n padding: 6px;\n}\n\n.account-picker-item {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 6px 8px;\n border-radius: 4px;\n cursor: pointer;\n}\n\n.account-picker-item:hover {\n background-color: var(--mj-bg-surface-sunken);\n}\n\n.picker-name {\n font-size: 0.82rem;\n font-weight: 500;\n color: var(--mj-text-primary);\n}\n\n.picker-badge {\n margin-left: auto;\n font-size: 0.7rem;\n color: var(--mj-text-muted);\n}\n\n.copy-progress-indicator {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.78rem;\n color: var(--mj-brand-primary);\n margin-top: 8px;\n}\n"] }]
|
|
3385
|
+
}], () => [], { Account: [{
|
|
3386
|
+
type: Input
|
|
3387
|
+
}], account: [{
|
|
3388
|
+
type: Input
|
|
3389
|
+
}], FolderPath: [{
|
|
2750
3390
|
type: Input
|
|
2751
3391
|
}], folderPath: [{
|
|
2752
3392
|
type: Input
|
|
3393
|
+
}], FolderNavigate: [{
|
|
3394
|
+
type: Output
|
|
2753
3395
|
}], folderNavigate: [{
|
|
2754
3396
|
type: Output
|
|
3397
|
+
}], FolderStructureChanged: [{
|
|
3398
|
+
type: Output
|
|
2755
3399
|
}], folderStructureChanged: [{
|
|
2756
3400
|
type: Output
|
|
2757
3401
|
}] }); })();
|