@anymux/ui-kit 0.1.0
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/ExplorerLayout-CSIJd7N4.js +105 -0
- package/dist/ExplorerLayout-CSIJd7N4.js.map +1 -0
- package/dist/FileBrowserContext-B6jixa2j.js +11 -0
- package/dist/FileBrowserContext-B6jixa2j.js.map +1 -0
- package/dist/calendar-DSlrbHoj.js +761 -0
- package/dist/calendar-DSlrbHoj.js.map +1 -0
- package/dist/calendar.d.ts +3 -0
- package/dist/calendar.js +3 -0
- package/dist/contacts-DQXTZzHc.js +539 -0
- package/dist/contacts-DQXTZzHc.js.map +1 -0
- package/dist/contacts.d.ts +3 -0
- package/dist/contacts.js +3 -0
- package/dist/file-browser-m5atC3kF.js +6755 -0
- package/dist/file-browser-m5atC3kF.js.map +1 -0
- package/dist/file-browser.d.ts +11 -0
- package/dist/file-browser.js +9 -0
- package/dist/git-B55e6LL-.js +561 -0
- package/dist/git-B55e6LL-.js.map +1 -0
- package/dist/git.d.ts +2 -0
- package/dist/git.js +3 -0
- package/dist/iconMap-V4B8P-Uh.js +206 -0
- package/dist/iconMap-V4B8P-Uh.js.map +1 -0
- package/dist/icons-CIsIOZXR.js +0 -0
- package/dist/icons.d.ts +2 -0
- package/dist/icons.js +4 -0
- package/dist/index-BNmNIWBL.d.ts +71 -0
- package/dist/index-BNmNIWBL.d.ts.map +1 -0
- package/dist/index-Bryv_GCG.d.ts +1481 -0
- package/dist/index-Bryv_GCG.d.ts.map +1 -0
- package/dist/index-CuQIjSXs.d.ts +134 -0
- package/dist/index-CuQIjSXs.d.ts.map +1 -0
- package/dist/index-DSu19mq0.d.ts +153 -0
- package/dist/index-DSu19mq0.d.ts.map +1 -0
- package/dist/index-DmsyeHFr.d.ts +149 -0
- package/dist/index-DmsyeHFr.d.ts.map +1 -0
- package/dist/index-DxnJ8FYM.d.ts +17 -0
- package/dist/index-DxnJ8FYM.d.ts.map +1 -0
- package/dist/index-DzfY1Tok.d.ts +32 -0
- package/dist/index-DzfY1Tok.d.ts.map +1 -0
- package/dist/index-Ml_SgiKa.d.ts +1847 -0
- package/dist/index-Ml_SgiKa.d.ts.map +1 -0
- package/dist/index-kHr9udZD.d.ts +1025 -0
- package/dist/index-kHr9udZD.d.ts.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +15 -0
- package/dist/layout-Ca_4r8ka.js +89 -0
- package/dist/layout-Ca_4r8ka.js.map +1 -0
- package/dist/layout.d.ts +2 -0
- package/dist/layout.js +5 -0
- package/dist/list-CxfT6hix.js +6831 -0
- package/dist/list-CxfT6hix.js.map +1 -0
- package/dist/list.d.ts +2 -0
- package/dist/list.js +5 -0
- package/dist/media-DZ292aKK.js +557 -0
- package/dist/media-DZ292aKK.js.map +1 -0
- package/dist/media.d.ts +3 -0
- package/dist/media.js +3 -0
- package/dist/tree-Dd9Z0Aso.js +3351 -0
- package/dist/tree-Dd9Z0Aso.js.map +1 -0
- package/dist/tree.d.ts +2 -0
- package/dist/tree.js +6 -0
- package/dist/types-common-CB3kRek8.d.ts +26 -0
- package/dist/types-common-CB3kRek8.d.ts.map +1 -0
- package/dist/utils-B4fdKKsy.js +3 -0
- package/package.json +109 -0
- package/src/calendar/AgendaView.tsx +37 -0
- package/src/calendar/CalendarBrowser.tsx +90 -0
- package/src/calendar/CalendarModel.ts +142 -0
- package/src/calendar/CalendarSidebar.tsx +81 -0
- package/src/calendar/DayView.tsx +76 -0
- package/src/calendar/EventCard.tsx +51 -0
- package/src/calendar/MockCalendarProvider.ts +98 -0
- package/src/calendar/MonthView.tsx +77 -0
- package/src/calendar/WeekView.tsx +129 -0
- package/src/calendar/index.ts +18 -0
- package/src/calendar/types.ts +25 -0
- package/src/contacts/ContactAvatar.tsx +35 -0
- package/src/contacts/ContactBrowser.tsx +56 -0
- package/src/contacts/ContactCard.tsx +37 -0
- package/src/contacts/ContactDetail.tsx +63 -0
- package/src/contacts/ContactGroupSidebar.tsx +40 -0
- package/src/contacts/ContactList.tsx +32 -0
- package/src/contacts/ContactListModel.ts +120 -0
- package/src/contacts/MockContactProvider.ts +77 -0
- package/src/contacts/index.ts +17 -0
- package/src/contacts/types.ts +26 -0
- package/src/demos/CalendarBrowserDemo.tsx +15 -0
- package/src/demos/ContactBrowserDemo.tsx +15 -0
- package/src/demos/MediaBrowserDemo.tsx +15 -0
- package/src/file-browser/adapters/DocumentViewerAdapter.ts +371 -0
- package/src/file-browser/adapters/FileSystemBridge.ts +168 -0
- package/src/file-browser/adapters/GitBrowserAdapter.ts +546 -0
- package/src/file-browser/adapters/README.md +504 -0
- package/src/file-browser/adapters/index.ts +27 -0
- package/src/file-browser/adapters/types.ts +70 -0
- package/src/file-browser/architecture.md +645 -0
- package/src/file-browser/components/CreateItemDialog.tsx +71 -0
- package/src/file-browser/components/DeleteConfirmDialog.tsx +58 -0
- package/src/file-browser/components/FileBrowser.tsx +473 -0
- package/src/file-browser/components/FileBrowserContent.tsx +209 -0
- package/src/file-browser/components/FileBrowserHeader.tsx +151 -0
- package/src/file-browser/components/FileBrowserToolbar.tsx +145 -0
- package/src/file-browser/components/LeftPanel/LeftPanel.tsx +103 -0
- package/src/file-browser/components/LeftPanel/LeftPanelTabs.tsx +70 -0
- package/src/file-browser/components/LeftPanel/TreeNavigationView.tsx +256 -0
- package/src/file-browser/components/PreviewPane.tsx +146 -0
- package/src/file-browser/components/RightPanel/FilePreview.tsx +219 -0
- package/src/file-browser/components/RightPanel/RightPanel.tsx +186 -0
- package/src/file-browser/components/RightPanel/RightPanelToolbar.tsx +113 -0
- package/src/file-browser/components/UploadProgress.tsx +123 -0
- package/src/file-browser/components/ViewerHost.tsx +208 -0
- package/src/file-browser/components/mobile/MobileNavigation.tsx +227 -0
- package/src/file-browser/components/navigation/NavigationButtons.tsx +171 -0
- package/src/file-browser/components/shared/ErrorBoundary.tsx +116 -0
- package/src/file-browser/components/shared/FileBrowserItem.tsx +195 -0
- package/src/file-browser/components/shared/FileIcon.tsx +169 -0
- package/src/file-browser/components/toolbar/ViewModeToggle.tsx +200 -0
- package/src/file-browser/components/views/ListView/ListView.tsx +484 -0
- package/src/file-browser/components/views/ThumbnailView/ThumbnailView.tsx +323 -0
- package/src/file-browser/components/views/TreeView/TreeNode.tsx +186 -0
- package/src/file-browser/components/views/TreeView/TreeNodeList.tsx +191 -0
- package/src/file-browser/components/views/TreeView/TreeView.tsx +200 -0
- package/src/file-browser/components/views/TreemapView/TreemapView.tsx +339 -0
- package/src/file-browser/context/FileBrowserContext.tsx +13 -0
- package/src/file-browser/examples/BasicUsage.tsx +20 -0
- package/src/file-browser/index.ts +98 -0
- package/src/file-browser/models/FileBrowserModel.ts +623 -0
- package/src/file-browser/models/LeftPanelManagerModel.ts +105 -0
- package/src/file-browser/models/NavigationManagerModel.ts +312 -0
- package/src/file-browser/models/ResponsiveLayoutManagerModel.ts +437 -0
- package/src/file-browser/models/RightPanelManagerModel.ts +190 -0
- package/src/file-browser/models/SelectionManagerModel.ts +252 -0
- package/src/file-browser/models/ToolbarManagerModel.ts +144 -0
- package/src/file-browser/models/UploadModel.ts +147 -0
- package/src/file-browser/models/ViewModeManagerModel.ts +185 -0
- package/src/file-browser/models/ViewerHostModel.ts +44 -0
- package/src/file-browser/models/ui/ListViewUIModel.ts +265 -0
- package/src/file-browser/models/ui/PreviewUIModel.ts +297 -0
- package/src/file-browser/models/ui/ThumbnailViewUIModel.ts +254 -0
- package/src/file-browser/models/ui/TreeViewUIModel.ts +128 -0
- package/src/file-browser/models/ui/TreemapViewUIModel.ts +350 -0
- package/src/file-browser/providers/FileSystemListProvider.ts +552 -0
- package/src/file-browser/providers/FileSystemProvider.ts +401 -0
- package/src/file-browser/providers/FileSystemTreeProvider.ts +231 -0
- package/src/file-browser/providers/GitProvider.ts +337 -0
- package/src/file-browser/providers/GitRepositoryProvider.ts +376 -0
- package/src/file-browser/providers/IFileBrowserProvider.ts +56 -0
- package/src/file-browser/providers/MemoryProvider.ts +303 -0
- package/src/file-browser/providers/index.ts +4 -0
- package/src/file-browser/registry/ViewerRegistry.ts +551 -0
- package/src/file-browser/registry/types.ts +144 -0
- package/src/file-browser/scripts/performanceBenchmark.ts +553 -0
- package/src/file-browser/services/ThumbnailCacheService.ts +128 -0
- package/src/file-browser/tasks.md +537 -0
- package/src/file-browser/types/FileBrowserTypes.ts +126 -0
- package/src/file-browser/types/ProviderTypes.ts +155 -0
- package/src/file-browser/types/UITypes.ts +235 -0
- package/src/file-browser/types/ViewModeTypes.ts +150 -0
- package/src/file-browser/utils/gestures.ts +327 -0
- package/src/file-browser/utils/performance.ts +563 -0
- package/src/file-browser/viewers/ImageViewer.tsx +163 -0
- package/src/file-browser/viewers/ImageViewerModel.ts +79 -0
- package/src/file-browser/viewers/TextViewer.tsx +95 -0
- package/src/file-browser/viewers/UnsupportedFileViewer.tsx +57 -0
- package/src/file-browser/viewers/index.ts +61 -0
- package/src/git/BranchList.tsx +128 -0
- package/src/git/CommitGraph.tsx +239 -0
- package/src/git/CommitList.tsx +258 -0
- package/src/git/DiffViewer.tsx +219 -0
- package/src/git/index.ts +4 -0
- package/src/icons/iconMap.ts +146 -0
- package/src/icons/index.ts +9 -0
- package/src/index.ts +13 -0
- package/src/layout/README.md +307 -0
- package/src/layout/components/ExplorerLayout/ExplorerLayout.tsx +178 -0
- package/src/layout/examples/SimpleExample.tsx +60 -0
- package/src/layout/index.ts +6 -0
- package/src/lib/utils.ts +1 -0
- package/src/list/README.md +303 -0
- package/src/list/architecture.md +807 -0
- package/src/list/components/CalculatedGridView.tsx +252 -0
- package/src/list/components/DragPreview.tsx +102 -0
- package/src/list/components/ListContextMenu.tsx +274 -0
- package/src/list/components/ListItem.tsx +761 -0
- package/src/list/components/ListItems.tsx +919 -0
- package/src/list/components/MasonryView.tsx +241 -0
- package/src/list/components/SearchFilter.tsx +44 -0
- package/src/list/components/TreemapView.tsx +709 -0
- package/src/list/components/ViewSizeControls.tsx +205 -0
- package/src/list/components/ViewTypeSelector.tsx +312 -0
- package/src/list/components/VirtualizedDetailsView.tsx +231 -0
- package/src/list/components/VirtualizedGrid.tsx +164 -0
- package/src/list/components/VirtualizedList.tsx +154 -0
- package/src/list/components/VirtualizedMasonryView.tsx +344 -0
- package/src/list/components/shared/EmptyState.tsx +103 -0
- package/src/list/components/shared/ErrorBoundary.tsx +123 -0
- package/src/list/components/shared/ErrorDisplay.tsx +100 -0
- package/src/list/components/shared/ListLoader.tsx +146 -0
- package/src/list/components/shared/LoadingIndicator.tsx +80 -0
- package/src/list/index.ts +92 -0
- package/src/list/models/ListItemsModel.ts +1301 -0
- package/src/list/models/TreemapModel.ts +204 -0
- package/src/list/providers/ListItemsProvider.ts +313 -0
- package/src/list/providers/TestListProvider.ts +604 -0
- package/src/list/tasks.md +937 -0
- package/src/list/types/ListTypes.ts +178 -0
- package/src/list/utils/BenchmarkLogger.ts +243 -0
- package/src/list/utils/DragDropManager.ts +320 -0
- package/src/list/utils/GridLayoutCalculator.ts +290 -0
- package/src/list/utils/ListAccessibility.ts +367 -0
- package/src/list/utils/ListKeyboard.ts +414 -0
- package/src/list/utils/MasonryLayoutCalculator.ts +302 -0
- package/src/list/utils/MasonryLayoutEngine.ts +401 -0
- package/src/list/utils/__tests__/MasonryLayoutEngine.test.ts +157 -0
- package/src/list/utils/__tests__/VirtualizedMasonryView.test.tsx +251 -0
- package/src/media/AlbumSidebar.tsx +48 -0
- package/src/media/MediaBrowser.tsx +92 -0
- package/src/media/MediaBrowserModel.ts +138 -0
- package/src/media/MediaGrid.tsx +50 -0
- package/src/media/MediaList.tsx +49 -0
- package/src/media/MediaPreview.tsx +63 -0
- package/src/media/MediaTimeline.tsx +38 -0
- package/src/media/MockMediaProvider.ts +70 -0
- package/src/media/index.ts +18 -0
- package/src/media/types.ts +21 -0
- package/src/styles/variables.css +60 -0
- package/src/tree/DEVELOPMENT_SUMMARY.md +170 -0
- package/src/tree/__tests__/TreeModel.test.ts +16 -0
- package/src/tree/architecture.md +530 -0
- package/src/tree/components/Tree.tsx +283 -0
- package/src/tree/components/TreeCheckbox.tsx +147 -0
- package/src/tree/components/TreeContextMenu.tsx +139 -0
- package/src/tree/components/TreeNodeList.tsx +329 -0
- package/src/tree/components/TreeTable.tsx +382 -0
- package/src/tree/index.ts +58 -0
- package/src/tree/models/TreeModel.ts +839 -0
- package/src/tree/providers/SimpleTreeProvider.ts +463 -0
- package/src/tree/providers/TestTreeProvider.ts +946 -0
- package/src/tree/providers/TreeProvider.ts +308 -0
- package/src/tree/tasks.md +2046 -0
- package/src/tree/types/TreeTypes.ts +279 -0
- package/src/tree/utils/SelectionTheme.ts +150 -0
- package/src/tree/utils/logger.ts +203 -0
- package/src/types-common.ts +21 -0
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
import { makeAutoObservable, observable, runInAction } from 'mobx';
|
|
2
|
+
import type {
|
|
3
|
+
ListItemsProvider,
|
|
4
|
+
ListItemsProviderListener
|
|
5
|
+
} from '../../list/providers/ListItemsProvider';
|
|
6
|
+
import type {
|
|
7
|
+
ListItemData,
|
|
8
|
+
ListLoadOptions,
|
|
9
|
+
ListLoadResult,
|
|
10
|
+
ListViewType,
|
|
11
|
+
ListContextMenuItem,
|
|
12
|
+
ListSelectionInfo,
|
|
13
|
+
VirtualizationConfig,
|
|
14
|
+
ListDragDropInfo
|
|
15
|
+
} from '../../list/types/ListTypes';
|
|
16
|
+
import { LIST_VIEW_TYPE, GRID_VIEW_TYPE, DETAILS_VIEW_TYPE } from '../../list';
|
|
17
|
+
import { MASONRY_HORIZONTAL_VIEW_TYPE, MASONRY_VERTICAL_VIEW_TYPE, TREEMAP_VIEW_TYPE } from '../../list/providers/ListItemsProvider';
|
|
18
|
+
import { FileSystemBridge, type FileSystemItem } from '../adapters/FileSystemBridge';
|
|
19
|
+
import { getIconForFile } from '../../icons/iconMap';
|
|
20
|
+
import { ThumbnailCacheService } from '../services/ThumbnailCacheService';
|
|
21
|
+
|
|
22
|
+
interface FileSystemListProviderOptions {
|
|
23
|
+
showAllItems: boolean;
|
|
24
|
+
/** Service identifier for IndexedDB thumbnail cache key scoping */
|
|
25
|
+
serviceId?: string;
|
|
26
|
+
onSelectionChange: (items: ListItemData[]) => void;
|
|
27
|
+
onNavigation: (path: string) => void;
|
|
28
|
+
onNavigateUp?: () => void;
|
|
29
|
+
onOpenFile?: (path: string, name: string, size?: number) => void;
|
|
30
|
+
onPreviewFile?: (path: string, name: string, size?: number) => void;
|
|
31
|
+
onRefresh?: () => Promise<void>;
|
|
32
|
+
onRenameRequest?: (itemId: string, currentName: string, path: string, source: 'list' | 'tree') => void;
|
|
33
|
+
onDeleteRequest?: (targets: Array<{ path: string; isDirectory: boolean; name: string }>) => void;
|
|
34
|
+
onNewItemRequest?: (parentPath: string, type: 'file' | 'folder') => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export class FileSystemListProvider implements ListItemsProvider {
|
|
38
|
+
readonly id = 'filesystem-list';
|
|
39
|
+
readonly name = 'File System List';
|
|
40
|
+
readonly supportedViewTypes: ListViewType[] = [
|
|
41
|
+
LIST_VIEW_TYPE,
|
|
42
|
+
GRID_VIEW_TYPE,
|
|
43
|
+
DETAILS_VIEW_TYPE,
|
|
44
|
+
MASONRY_HORIZONTAL_VIEW_TYPE,
|
|
45
|
+
MASONRY_VERTICAL_VIEW_TYPE,
|
|
46
|
+
TREEMAP_VIEW_TYPE
|
|
47
|
+
];
|
|
48
|
+
readonly isMultiSelectEnabled = true;
|
|
49
|
+
readonly isVirtualizationEnabled = false;
|
|
50
|
+
readonly isDragDropEnabled = true;
|
|
51
|
+
|
|
52
|
+
currentPath: string = '/';
|
|
53
|
+
private listeners: ListItemsProviderListener[] = [];
|
|
54
|
+
|
|
55
|
+
// Thumbnail blob URL cache: path → blob URL
|
|
56
|
+
thumbnailCache = observable.map<string, string>();
|
|
57
|
+
// Aspect ratio cache: path → width/height ratio
|
|
58
|
+
aspectRatioCache = observable.map<string, number>();
|
|
59
|
+
// Track in-flight loads to avoid duplicate requests
|
|
60
|
+
private thumbnailLoading = new Set<string>();
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
private bridge: FileSystemBridge,
|
|
64
|
+
private options: FileSystemListProviderOptions
|
|
65
|
+
) {
|
|
66
|
+
makeAutoObservable(this, {
|
|
67
|
+
bridge: false,
|
|
68
|
+
options: false,
|
|
69
|
+
listeners: false,
|
|
70
|
+
thumbnailLoading: false,
|
|
71
|
+
});
|
|
72
|
+
// Evict expired IndexedDB thumbnails on startup (non-blocking)
|
|
73
|
+
ThumbnailCacheService.evictOld();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get a cached thumbnail blob URL, or trigger async load if not cached.
|
|
78
|
+
* Returns undefined while loading (MobX will re-render when cache updates).
|
|
79
|
+
*/
|
|
80
|
+
getThumbnailBlobUrl(path: string): string | undefined {
|
|
81
|
+
const cached = this.thumbnailCache.get(path);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
|
|
84
|
+
// Start async load if not already loading
|
|
85
|
+
if (!this.thumbnailLoading.has(path)) {
|
|
86
|
+
this.thumbnailLoading.add(path);
|
|
87
|
+
this.loadThumbnail(path);
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private async loadThumbnail(path: string): Promise<void> {
|
|
93
|
+
const serviceId = this.options.serviceId ?? 'default';
|
|
94
|
+
try {
|
|
95
|
+
// Try IndexedDB cache first
|
|
96
|
+
const cached = await ThumbnailCacheService.get(serviceId, path);
|
|
97
|
+
if (cached) {
|
|
98
|
+
runInAction(() => {
|
|
99
|
+
this.thumbnailCache.set(path, cached.blobUrl);
|
|
100
|
+
this.aspectRatioCache.set(path, cached.aspectRatio);
|
|
101
|
+
});
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Fall back to network load
|
|
106
|
+
const content = await this.bridge.readFile(path);
|
|
107
|
+
const ext = path.split('.').pop()?.toLowerCase() || '';
|
|
108
|
+
const mimeMap: Record<string, string> = {
|
|
109
|
+
jpg: 'image/jpeg', jpeg: 'image/jpeg', png: 'image/png',
|
|
110
|
+
gif: 'image/gif', svg: 'image/svg+xml', webp: 'image/webp'
|
|
111
|
+
};
|
|
112
|
+
const mime = mimeMap[ext] || 'image/png';
|
|
113
|
+
const rawData = typeof content === 'string' ? new TextEncoder().encode(content) : content;
|
|
114
|
+
const blob = new Blob([rawData], { type: mime });
|
|
115
|
+
const url = URL.createObjectURL(blob);
|
|
116
|
+
runInAction(() => {
|
|
117
|
+
this.thumbnailCache.set(path, url);
|
|
118
|
+
});
|
|
119
|
+
// Measure actual image dimensions for masonry aspect ratios
|
|
120
|
+
const img = new Image();
|
|
121
|
+
img.src = url;
|
|
122
|
+
img.onload = () => {
|
|
123
|
+
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
|
|
124
|
+
const aspectRatio = img.naturalWidth / img.naturalHeight;
|
|
125
|
+
runInAction(() => {
|
|
126
|
+
this.aspectRatioCache.set(path, aspectRatio);
|
|
127
|
+
});
|
|
128
|
+
// Persist to IndexedDB for future page loads
|
|
129
|
+
const buffer = rawData instanceof ArrayBuffer ? rawData : (rawData as Uint8Array).buffer;
|
|
130
|
+
ThumbnailCacheService.put(serviceId, path, buffer, mime, aspectRatio);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
console.warn('Failed to load thumbnail:', path, error);
|
|
135
|
+
} finally {
|
|
136
|
+
this.thumbnailLoading.delete(path);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get cached aspect ratio for an image path.
|
|
142
|
+
* Returns undefined if not yet measured.
|
|
143
|
+
*/
|
|
144
|
+
getAspectRatio(path: string): number | undefined {
|
|
145
|
+
return this.aspectRatioCache.get(path);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Revoke all cached blob URLs (call on path change / unmount)
|
|
150
|
+
*/
|
|
151
|
+
clearThumbnailCache(): void {
|
|
152
|
+
for (const url of this.thumbnailCache.values()) {
|
|
153
|
+
URL.revokeObjectURL(url);
|
|
154
|
+
}
|
|
155
|
+
this.thumbnailCache.clear();
|
|
156
|
+
this.aspectRatioCache.clear();
|
|
157
|
+
this.thumbnailLoading.clear();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
setPath(path: string) {
|
|
161
|
+
this.currentPath = path;
|
|
162
|
+
this.clearThumbnailCache();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async loadItems(options?: ListLoadOptions): Promise<ListLoadResult> {
|
|
166
|
+
const items = await this.bridge.loadDirectoryContents(this.currentPath);
|
|
167
|
+
|
|
168
|
+
const allItems: ListItemData[] = items.map(item => ({
|
|
169
|
+
id: item.id,
|
|
170
|
+
name: item.name,
|
|
171
|
+
type: item.type,
|
|
172
|
+
path: item.path,
|
|
173
|
+
size: item.size,
|
|
174
|
+
modified: item.modified,
|
|
175
|
+
isDirectory: item.isDirectory,
|
|
176
|
+
metadata: item,
|
|
177
|
+
thumbnailUrl: this.getThumbnailUrl(item),
|
|
178
|
+
icon: this.getFileIconString(item)
|
|
179
|
+
}));
|
|
180
|
+
|
|
181
|
+
const totalCount = allItems.length;
|
|
182
|
+
const offset = options?.offset ?? 0;
|
|
183
|
+
const limit = options?.limit ?? totalCount;
|
|
184
|
+
const paginatedItems = allItems.slice(offset, offset + limit);
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
items: paginatedItems,
|
|
188
|
+
totalCount,
|
|
189
|
+
hasMore: offset + limit < totalCount
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async getItemCount(): Promise<number> {
|
|
194
|
+
const result = await this.loadItems();
|
|
195
|
+
return result.totalCount || 0;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async loadItemRange(start: number, end: number): Promise<ListLoadResult> {
|
|
199
|
+
const result = await this.loadItems();
|
|
200
|
+
const rangeItems = result.items.slice(start, end);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
items: rangeItems,
|
|
204
|
+
totalCount: result.totalCount
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async refresh(): Promise<ListLoadResult> {
|
|
209
|
+
const result = await this.loadItems();
|
|
210
|
+
this.listeners.forEach(listener => {
|
|
211
|
+
listener.onItemsCountChanged(result.totalCount || 0);
|
|
212
|
+
});
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
getItemHeight(viewType: ListViewType): number {
|
|
217
|
+
switch (viewType.id) {
|
|
218
|
+
case 'list': return 32;
|
|
219
|
+
case 'grid': return 200;
|
|
220
|
+
case 'details': return 32;
|
|
221
|
+
case 'masonry-horizontal': return 160;
|
|
222
|
+
default: return 40;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
getItemWidth(viewType: ListViewType): number {
|
|
227
|
+
switch (viewType.id) {
|
|
228
|
+
case 'grid': return 240;
|
|
229
|
+
case 'masonry-horizontal': return 200;
|
|
230
|
+
default: return 0;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
getVirtualizationConfig(): VirtualizationConfig {
|
|
235
|
+
return {
|
|
236
|
+
itemHeight: 40,
|
|
237
|
+
overscan: 5,
|
|
238
|
+
threshold: 50
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
onSelectionChange = (selectionInfo: ListSelectionInfo) => {
|
|
243
|
+
this.options.onSelectionChange(selectionInfo.selectedItems);
|
|
244
|
+
|
|
245
|
+
// Trigger preview on single file selection
|
|
246
|
+
if (
|
|
247
|
+
selectionInfo.selectedItems.length === 1 &&
|
|
248
|
+
selectionInfo.selectionType === 'single' &&
|
|
249
|
+
this.options.onPreviewFile
|
|
250
|
+
) {
|
|
251
|
+
const item = selectionInfo.selectedItems[0]!;
|
|
252
|
+
if (!item.isDirectory) {
|
|
253
|
+
const fsItem = item.metadata as FileSystemItem;
|
|
254
|
+
this.options.onPreviewFile(item.path, fsItem.name, fsItem.size);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
onViewTypeChange?(viewType: ListViewType): void {}
|
|
260
|
+
|
|
261
|
+
async loadTreemapData(onProgress?: (progress: import('../../list/providers/ListItemsProvider').TreemapScanProgress) => void) {
|
|
262
|
+
const items = await this.bridge.loadDirectoryRecursive(this.currentPath, 3, onProgress);
|
|
263
|
+
const convert = (fsItems: FileSystemItem[]): import('../../list/providers/ListItemsProvider').TreemapNodeData[] =>
|
|
264
|
+
fsItems.map(item => ({
|
|
265
|
+
name: item.name,
|
|
266
|
+
path: item.path,
|
|
267
|
+
size: item.size,
|
|
268
|
+
isDirectory: item.isDirectory,
|
|
269
|
+
children: item.children ? convert(item.children) : undefined,
|
|
270
|
+
}));
|
|
271
|
+
return convert(items);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
onItemDoubleClick = (item: ListItemData) => {
|
|
275
|
+
if (item.isDirectory) {
|
|
276
|
+
this.options.onNavigation(item.path);
|
|
277
|
+
} else {
|
|
278
|
+
this.openFileWithViewer(item);
|
|
279
|
+
}
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
addListener(listener: ListItemsProviderListener): void {
|
|
283
|
+
this.listeners.push(listener);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
removeListener(listener: ListItemsProviderListener): void {
|
|
287
|
+
const index = this.listeners.indexOf(listener);
|
|
288
|
+
if (index >= 0) {
|
|
289
|
+
this.listeners.splice(index, 1);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
getItemIcon(item: ListItemData): string {
|
|
294
|
+
const fsItem = item.metadata as FileSystemItem;
|
|
295
|
+
return this.getFileIconString(fsItem);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
getItemContextMenu(item: ListItemData): ListContextMenuItem[] {
|
|
299
|
+
const baseMenu: ListContextMenuItem[] = [
|
|
300
|
+
{
|
|
301
|
+
id: 'open',
|
|
302
|
+
label: item.isDirectory ? 'Open Folder' : 'Open',
|
|
303
|
+
icon: item.isDirectory ? 'folder-open' : 'eye',
|
|
304
|
+
type: 'item'
|
|
305
|
+
}
|
|
306
|
+
];
|
|
307
|
+
|
|
308
|
+
if (!item.isDirectory) {
|
|
309
|
+
baseMenu.push(
|
|
310
|
+
{ id: 'separator1', type: 'separator', label: '' },
|
|
311
|
+
{ id: 'download', label: 'Download', icon: 'download', type: 'item' }
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (item.isDirectory) {
|
|
316
|
+
baseMenu.push(
|
|
317
|
+
{ id: 'separator-new', type: 'separator', label: '' },
|
|
318
|
+
{ id: 'new-folder', label: 'New Folder', icon: 'folder-plus', type: 'item' },
|
|
319
|
+
{ id: 'new-file', label: 'New File', icon: 'file-plus', type: 'item' }
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
baseMenu.push(
|
|
324
|
+
{ id: 'separator2', type: 'separator', label: '' },
|
|
325
|
+
{ id: 'copy-path', label: 'Copy Path', icon: 'copy', type: 'item' },
|
|
326
|
+
{ id: 'rename', label: 'Rename', icon: 'edit', type: 'item', shortcut: 'F2' },
|
|
327
|
+
{ id: 'separator3', type: 'separator', label: '' },
|
|
328
|
+
{ id: 'delete', label: 'Delete', icon: 'trash', type: 'item', destructive: true, shortcut: 'Del' }
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
return baseMenu;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
getEmptySpaceContextMenu(): ListContextMenuItem[] {
|
|
335
|
+
return [
|
|
336
|
+
{ id: 'new-folder', label: 'New Folder', icon: 'folder-plus', type: 'item' },
|
|
337
|
+
{ id: 'new-file', label: 'New File', icon: 'file-plus', type: 'item' }
|
|
338
|
+
];
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
getMultiItemContextMenu(items: ListItemData[]): ListContextMenuItem[] {
|
|
342
|
+
const hasFiles = items.some(i => !i.isDirectory);
|
|
343
|
+
const menu: ListContextMenuItem[] = [];
|
|
344
|
+
|
|
345
|
+
if (hasFiles) {
|
|
346
|
+
menu.push(
|
|
347
|
+
{ id: 'download-multiple', label: `Download ${items.filter(i => !i.isDirectory).length} Files`, icon: 'download', type: 'item' }
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
menu.push(
|
|
352
|
+
{ id: 'copy-path', label: 'Copy Paths', icon: 'copy', type: 'item' },
|
|
353
|
+
{ id: 'separator-del', type: 'separator', label: '' },
|
|
354
|
+
{ id: 'delete-multiple', label: `Delete ${items.length} Items`, icon: 'trash', type: 'item', destructive: true, shortcut: 'Del' }
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return menu;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
onContextMenuAction = (menuItemId: string, items: ListItemData[]) => {
|
|
361
|
+
if (items.length === 0) {
|
|
362
|
+
switch (menuItemId) {
|
|
363
|
+
case 'new-folder':
|
|
364
|
+
this.requestNewItem(this.currentPath, 'folder');
|
|
365
|
+
break;
|
|
366
|
+
case 'new-file':
|
|
367
|
+
this.requestNewItem(this.currentPath, 'file');
|
|
368
|
+
break;
|
|
369
|
+
}
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const item = items[0];
|
|
374
|
+
if (!item) return;
|
|
375
|
+
|
|
376
|
+
switch (menuItemId) {
|
|
377
|
+
case 'open':
|
|
378
|
+
this.onItemDoubleClick(item);
|
|
379
|
+
break;
|
|
380
|
+
case 'download':
|
|
381
|
+
this.downloadFile(item.path);
|
|
382
|
+
break;
|
|
383
|
+
case 'download-multiple':
|
|
384
|
+
items.filter(i => !i.isDirectory).forEach(i => this.downloadFile(i.path));
|
|
385
|
+
break;
|
|
386
|
+
case 'copy-path':
|
|
387
|
+
this.copyPaths(items);
|
|
388
|
+
break;
|
|
389
|
+
case 'rename':
|
|
390
|
+
this.requestRename(item);
|
|
391
|
+
break;
|
|
392
|
+
case 'delete':
|
|
393
|
+
this.requestDelete([item]);
|
|
394
|
+
break;
|
|
395
|
+
case 'delete-multiple':
|
|
396
|
+
this.requestDelete(items);
|
|
397
|
+
break;
|
|
398
|
+
case 'new-folder':
|
|
399
|
+
this.requestNewItem(item.isDirectory ? item.path : this.currentPath, 'folder');
|
|
400
|
+
break;
|
|
401
|
+
case 'new-file':
|
|
402
|
+
this.requestNewItem(item.isDirectory ? item.path : this.currentPath, 'file');
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
canDragItem?(item: ListItemData): boolean {
|
|
408
|
+
return true;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
canDropItems?(draggedItems: ListItemData[], targetItem: ListItemData | null, position: 'before' | 'after' | 'inside'): boolean {
|
|
412
|
+
// Can only drop into directories
|
|
413
|
+
if (!targetItem?.isDirectory) return false;
|
|
414
|
+
if (position !== 'inside') return false;
|
|
415
|
+
// Cannot drop a folder into itself or its own children
|
|
416
|
+
for (const item of draggedItems) {
|
|
417
|
+
if (targetItem.path === item.path) return false;
|
|
418
|
+
if (targetItem.path.startsWith(item.path + '/')) return false;
|
|
419
|
+
}
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
onDragStart?(draggedItems: ListItemData[], event: DragEvent): void {}
|
|
424
|
+
|
|
425
|
+
onDrop?(dragDropInfo: ListDragDropInfo): void {
|
|
426
|
+
const { draggedItems, targetItem } = dragDropInfo;
|
|
427
|
+
if (!targetItem?.isDirectory) return;
|
|
428
|
+
|
|
429
|
+
// Move each dragged item into the target directory
|
|
430
|
+
const moves = draggedItems.map(async (item) => {
|
|
431
|
+
const name = item.path.split('/').pop() || '';
|
|
432
|
+
const newPath = `${targetItem.path}/${name}`.replace(/\/\//g, '/');
|
|
433
|
+
try {
|
|
434
|
+
await this.bridge.renameItem(item.path, newPath);
|
|
435
|
+
} catch (e) {
|
|
436
|
+
console.error(`Failed to move ${item.path} → ${newPath}:`, e);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
Promise.all(moves).then(() => this.options.onRefresh?.());
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
onDragEnd?(draggedItems: ListItemData[], success: boolean): void {}
|
|
444
|
+
|
|
445
|
+
resolveThumbnailUrl(item: ListItemData): string | undefined {
|
|
446
|
+
if (item.type === 'file' && this.isImageFile(item.name)) {
|
|
447
|
+
return this.getThumbnailBlobUrl(item.path);
|
|
448
|
+
}
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
private getThumbnailUrl(item: FileSystemItem): string | undefined {
|
|
453
|
+
// Return path as marker that this item has a thumbnail.
|
|
454
|
+
// Actual blob URL resolved via resolveThumbnailUrl/thumbnailCache.
|
|
455
|
+
if (item.type === 'file' && this.isImageFile(item.name)) {
|
|
456
|
+
return item.path;
|
|
457
|
+
}
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
private getFileIconString(item: FileSystemItem): string {
|
|
462
|
+
return getIconForFile(item.name, item.isDirectory);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
private isImageFile(filename: string): boolean {
|
|
466
|
+
const ext = filename.split('.').pop()?.toLowerCase();
|
|
467
|
+
return ['jpg', 'jpeg', 'png', 'gif', 'svg', 'webp'].includes(ext || '');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
private openFileWithViewer(item: ListItemData): void {
|
|
471
|
+
const fsItem = item.metadata as FileSystemItem;
|
|
472
|
+
if (this.options.onOpenFile) {
|
|
473
|
+
this.options.onOpenFile(item.path, fsItem.name, fsItem.size);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
private copyPaths(items: ListItemData[]) {
|
|
478
|
+
const paths = items.map(i => i.path).join('\n');
|
|
479
|
+
navigator.clipboard.writeText(paths).catch((err) => {
|
|
480
|
+
console.error('Failed to copy paths:', err);
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
private async downloadFile(path: string) {
|
|
485
|
+
try {
|
|
486
|
+
const content = await this.bridge.readFile(path);
|
|
487
|
+
const filename = path.split('/').pop() || 'download';
|
|
488
|
+
const blob = new Blob(
|
|
489
|
+
[typeof content === 'string' ? content : content],
|
|
490
|
+
{ type: 'application/octet-stream' }
|
|
491
|
+
);
|
|
492
|
+
const url = URL.createObjectURL(blob);
|
|
493
|
+
const a = document.createElement('a');
|
|
494
|
+
a.href = url;
|
|
495
|
+
a.download = filename;
|
|
496
|
+
document.body.appendChild(a);
|
|
497
|
+
a.click();
|
|
498
|
+
document.body.removeChild(a);
|
|
499
|
+
URL.revokeObjectURL(url);
|
|
500
|
+
} catch (error) {
|
|
501
|
+
console.error('Download failed:', error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
private requestRename(item: ListItemData) {
|
|
506
|
+
if (this.options.onRenameRequest) {
|
|
507
|
+
const currentName = item.path.split('/').pop() || '';
|
|
508
|
+
this.options.onRenameRequest(item.id, currentName, item.path, 'list');
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
private requestDelete(items: ListItemData[]) {
|
|
513
|
+
if (this.options.onDeleteRequest) {
|
|
514
|
+
const targets = items.map(item => ({
|
|
515
|
+
path: item.path,
|
|
516
|
+
isDirectory: Boolean(item.isDirectory),
|
|
517
|
+
name: item.path.split('/').pop() || ''
|
|
518
|
+
}));
|
|
519
|
+
this.options.onDeleteRequest(targets);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private requestNewItem(parentPath: string, type: 'file' | 'folder') {
|
|
524
|
+
if (this.options.onNewItemRequest) {
|
|
525
|
+
this.options.onNewItemRequest(parentPath, type);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async executeRename(path: string, newName: string): Promise<void> {
|
|
530
|
+
const parentPath = path.split('/').slice(0, -1).join('/') || '/';
|
|
531
|
+
const newPath = `${parentPath}/${newName}`.replace(/\/\//g, '/');
|
|
532
|
+
await this.bridge.renameItem(path, newPath);
|
|
533
|
+
await this.options.onRefresh?.();
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
async executeDelete(targets: Array<{ path: string; isDirectory: boolean }>): Promise<void> {
|
|
537
|
+
await Promise.all(targets.map(t =>
|
|
538
|
+
this.bridge.deleteItem(t.path, t.isDirectory)
|
|
539
|
+
));
|
|
540
|
+
await this.options.onRefresh?.();
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async executeCreate(parentPath: string, name: string, type: 'file' | 'folder'): Promise<void> {
|
|
544
|
+
const newPath = `${parentPath}/${name}`.replace(/\/\//g, '/');
|
|
545
|
+
if (type === 'folder') {
|
|
546
|
+
await this.bridge.createDirectory(newPath);
|
|
547
|
+
} else {
|
|
548
|
+
await this.bridge.writeFile(newPath, '');
|
|
549
|
+
}
|
|
550
|
+
await this.options.onRefresh?.();
|
|
551
|
+
}
|
|
552
|
+
}
|