dsh-workspace-explorer 1.0.1 → 1.1.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.
@@ -0,0 +1,69 @@
1
+ const dockItems = ':is([data-dockkit-host="dock"], [data-dockkit-empty], [data-dockkit-divider])';
2
+ const toggleButtons = 'button:is([data-sidebar-right-expand], [data-sidebar-right-toggle])';
3
+
4
+ export function installSidebarMotion(sidebar) {
5
+ let allowed;
6
+ let collectFrame;
7
+ const instantFrames = new Map();
8
+
9
+ function stopMotion() {
10
+ cancelAnimationFrame(collectFrame);
11
+ if (!allowed) return;
12
+ for (const item of allowed.items) delete item.dataset.dweSidebarToggleMotion;
13
+ allowed = undefined;
14
+ }
15
+
16
+ function allowButtonMotion(event) {
17
+ const button = event.target instanceof Element && event.target.closest(toggleButtons);
18
+ if (!button || button.disabled) return;
19
+ const sessionId = sidebar.mounted.getSnapshot();
20
+ const panel = sessionId && document.querySelector(
21
+ '[data-sidebar-right-panel][data-sidebar-right-session="' + CSS.escape(sessionId) + '"]',
22
+ );
23
+ const frame = panel?.closest('[data-rightbar-col]')?.parentElement;
24
+ if (!frame) return;
25
+
26
+ // A tab selected during the slide must not inherit permission to animate.
27
+ stopMotion();
28
+ const items = [...panel.querySelectorAll(dockItems + ':not([hidden])')];
29
+ const motion = { frame, items };
30
+ allowed = motion;
31
+ for (const item of items) item.dataset.dweSidebarToggleMotion = '';
32
+ collectFrame = requestAnimationFrame(() => {
33
+ const transitions = [...frame.getAnimations(), ...items.flatMap(item => item.getAnimations())]
34
+ .filter(animation => ['transform', 'grid-template-columns'].includes(animation.transitionProperty));
35
+ Promise.allSettled(transitions.map(animation => animation.finished)).then(() => {
36
+ if (allowed === motion) stopMotion();
37
+ });
38
+ });
39
+ }
40
+
41
+ // The shell shares one grid transition between the left and right columns.
42
+ // Snap right-track changes outside the toggle controls so the left controls still animate.
43
+ const track = new MutationObserver(records => {
44
+ for (const frame of new Set(records.map(record => record.target))) {
45
+ if (allowed?.frame === frame || !frame.querySelector(':scope > [data-rightbar-col]')) continue;
46
+ cancelAnimationFrame(instantFrames.get(frame));
47
+ frame.dataset.dweSidebarTrackInstant = '';
48
+ instantFrames.set(frame, requestAnimationFrame(() => {
49
+ instantFrames.set(frame, requestAnimationFrame(() => {
50
+ delete frame.dataset.dweSidebarTrackInstant;
51
+ instantFrames.delete(frame);
52
+ }));
53
+ }));
54
+ }
55
+ });
56
+ track.observe(document.documentElement, { subtree: true, attributeFilter: ['data-rightbar-collapsed'] });
57
+ document.addEventListener('click', allowButtonMotion, true);
58
+ const disposeSwitch = sidebar.mounted.subscribe(stopMotion);
59
+ return () => {
60
+ stopMotion();
61
+ disposeSwitch();
62
+ track.disconnect();
63
+ for (const [frame, request] of instantFrames) {
64
+ cancelAnimationFrame(request);
65
+ delete frame.dataset.dweSidebarTrackInstant;
66
+ }
67
+ document.removeEventListener('click', allowButtonMotion, true);
68
+ };
69
+ }
@@ -0,0 +1,2 @@
1
+ export const officeExtensions = new Set(['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx']);
2
+ export const pdfAssetsPath = '/api/dsh.workspaceExplorer/pdf';
@@ -1,184 +1,184 @@
1
- using System;
2
- using System.Runtime.InteropServices;
3
-
4
- public static class ExplorerShell {
5
- [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
6
- private static extern IShellItem SHCreateItemFromParsingName(string path, IntPtr binding, [MarshalAs(UnmanagedType.LPStruct)] Guid iid);
7
-
8
- [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
9
- private static extern bool MoveFileEx(string source, string destination, uint flags);
10
-
11
- [DllImport("user32.dll")]
12
- public static extern uint GetClipboardSequenceNumber();
13
-
14
- [DllImport("user32.dll")]
15
- private static extern bool OpenClipboard(IntPtr window);
16
-
17
- [DllImport("user32.dll")]
18
- private static extern bool CloseClipboard();
19
-
20
- [DllImport("user32.dll")]
21
- private static extern bool EmptyClipboard();
22
-
23
- [DllImport("user32.dll", CharSet = CharSet.Unicode)]
24
- private static extern uint RegisterClipboardFormat(string format);
25
-
26
- [DllImport("user32.dll")]
27
- private static extern IntPtr GetClipboardData(uint format);
28
-
29
- [DllImport("kernel32.dll")]
30
- private static extern IntPtr GlobalLock(IntPtr memory);
31
-
32
- [DllImport("kernel32.dll")]
33
- private static extern bool GlobalUnlock(IntPtr memory);
34
-
35
- [DllImport("kernel32.dll")]
36
- private static extern UIntPtr GlobalSize(IntPtr memory);
37
-
38
- public static bool ClipboardIsCut() {
39
- if (!OpenClipboard(IntPtr.Zero)) throw new InvalidOperationException("Clipboard unavailable");
40
- try {
41
- IntPtr memory = GetClipboardData(RegisterClipboardFormat("Preferred DropEffect"));
42
- if (memory == IntPtr.Zero || GlobalSize(memory).ToUInt64() < 4) return false;
43
- IntPtr data = GlobalLock(memory);
44
- if (data == IntPtr.Zero) return false;
45
- try { return (Marshal.ReadInt32(data) & 2) != 0; }
46
- finally { GlobalUnlock(memory); }
47
- } finally { CloseClipboard(); }
48
- }
49
-
50
- public static int Rename(string source, string destination) {
51
- return MoveFileEx(source, destination, 0) ? 0 : Marshal.GetLastWin32Error();
52
- }
53
-
54
- public static void ClearClipboardIfUnchanged(uint sequence) {
55
- if (!OpenClipboard(IntPtr.Zero)) return;
56
- try {
57
- if (sequence == GetClipboardSequenceNumber()) EmptyClipboard();
58
- } finally { CloseClipboard(); }
59
- }
60
-
61
- public static bool Transfer(string source, string destination, bool cut) {
62
- IFileOperation operation = (IFileOperation)new FileOperation();
63
- try {
64
- const uint silent = 0x4, keepBoth = 0x8, noConfirmation = 0x10;
65
- const uint noErrorUI = 0x400, onlySelected = 0x2000, earlyFailure = 0x100000;
66
- const uint keepExtension = 0x200000, noCopyHooks = 0x800000, undoRecord = 0x20000000;
67
- operation.SetOperationFlags(silent | keepBoth | noConfirmation | noErrorUI | onlySelected
68
- | earlyFailure | keepExtension | noCopyHooks | undoRecord);
69
- Guid iid = typeof(IShellItem).GUID;
70
- IShellItem item = SHCreateItemFromParsingName(source, IntPtr.Zero, iid);
71
- try {
72
- IShellItem folder = SHCreateItemFromParsingName(destination, IntPtr.Zero, iid);
73
- try {
74
- if (cut) operation.MoveItem(item, folder, null, IntPtr.Zero);
75
- else operation.CopyItem(item, folder, null, IntPtr.Zero);
76
- operation.PerformOperations();
77
- return !operation.GetAnyOperationsAborted();
78
- } finally { Marshal.ReleaseComObject(folder); }
79
- } finally { Marshal.ReleaseComObject(item); }
80
- } finally { Marshal.ReleaseComObject(operation); }
81
- }
82
-
83
- public static bool Recycle(string source) {
84
- IFileOperation operation = (IFileOperation)new FileOperation();
85
- try {
86
- const uint silent = 0x4, noConfirmation = 0x10, allowUndo = 0x40, noErrorUI = 0x400;
87
- const uint onlySelected = 0x2000, warnPermanent = 0x4000, recycle = 0x80000;
88
- const uint earlyFailure = 0x100000, noCopyHooks = 0x800000, undoRecord = 0x20000000;
89
- operation.SetOperationFlags(silent | noConfirmation | allowUndo | noErrorUI | onlySelected | warnPermanent
90
- | recycle | earlyFailure | noCopyHooks | undoRecord);
91
- IShellItem item = SHCreateItemFromParsingName(source, IntPtr.Zero, typeof(IShellItem).GUID);
92
- try {
93
- RecycleOnly sink = new RecycleOnly();
94
- operation.DeleteItem(item, sink);
95
- operation.PerformOperations();
96
- return sink.Recycled && !operation.GetAnyOperationsAborted();
97
- } finally { Marshal.ReleaseComObject(item); }
98
- } finally { Marshal.ReleaseComObject(operation); }
99
- }
100
-
101
- [ComImport, Guid("3AD05575-8857-4850-9277-11B85BDB8E09")]
102
- private class FileOperation { }
103
-
104
- [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
105
- public interface IShellItem { }
106
-
107
- // Keep the declaration in the native IFileOperation vtable order.
108
- [ComImport, Guid("947AAB5F-0A5C-4C13-B4D6-4BF7836FC9F8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
109
- private interface IFileOperation {
110
- void Advise(IntPtr sink, out uint cookie);
111
- void Unadvise(uint cookie);
112
- void SetOperationFlags(uint flags);
113
- void SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string message);
114
- void SetProgressDialog(IntPtr dialog);
115
- void SetProperties(IntPtr properties);
116
- void SetOwnerWindow(IntPtr window);
117
- void ApplyPropertiesToItem(IShellItem item);
118
- void ApplyPropertiesToItems(IntPtr items);
119
- void RenameItem(IShellItem item, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
120
- void RenameItems(IntPtr items, [MarshalAs(UnmanagedType.LPWStr)] string name);
121
- void MoveItem(IShellItem item, IShellItem destination, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
122
- void MoveItems(IntPtr items, IShellItem destination);
123
- void CopyItem(IShellItem item, IShellItem destination, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
124
- void CopyItems(IntPtr items, IShellItem destination);
125
- void DeleteItem(IShellItem item, IFileOperationProgressSink sink);
126
- void DeleteItems(IntPtr items);
127
- void NewItem(IShellItem destination, uint attributes, [MarshalAs(UnmanagedType.LPWStr)] string name,
128
- [MarshalAs(UnmanagedType.LPWStr)] string template, IntPtr sink);
129
- void PerformOperations();
130
- [return: MarshalAs(UnmanagedType.Bool)]
131
- bool GetAnyOperationsAborted();
132
- }
133
-
134
- [ComImport, Guid("04B0F1A7-9490-44BC-96E1-4296A31252E2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
135
- public interface IFileOperationProgressSink {
136
- [PreserveSig] int StartOperations();
137
- [PreserveSig] int FinishOperations(int result);
138
- [PreserveSig] int PreRenameItem(uint flags, IShellItem item, IntPtr name);
139
- [PreserveSig] int PostRenameItem(uint flags, IShellItem item, IntPtr name, int result, IShellItem created);
140
- [PreserveSig] int PreMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name);
141
- [PreserveSig] int PostMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created);
142
- [PreserveSig] int PreCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name);
143
- [PreserveSig] int PostCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created);
144
- [PreserveSig] int PreDeleteItem(uint flags, IShellItem item);
145
- [PreserveSig] int PostDeleteItem(uint flags, IShellItem item, int result, IShellItem created);
146
- [PreserveSig] int PreNewItem(uint flags, IShellItem folder, IntPtr name);
147
- [PreserveSig] int PostNewItem(uint flags, IShellItem folder, IntPtr name, IntPtr template, uint attrs, int result, IShellItem created);
148
- [PreserveSig] int UpdateProgress(uint total, uint completed);
149
- [PreserveSig] int ResetTimer();
150
- [PreserveSig] int PauseTimer();
151
- [PreserveSig] int ResumeTimer();
152
- }
153
-
154
- [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
155
- public sealed class RecycleOnly : IFileOperationProgressSink {
156
- public bool Recycled { get; private set; }
157
-
158
- public int PreDeleteItem(uint flags, IShellItem item) {
159
- const uint recycleIfPossible = 0x80;
160
- const int cancelled = unchecked((int)0x800704C7);
161
- return (flags & recycleIfPossible) != 0 ? 0 : cancelled;
162
- }
163
-
164
- public int PostDeleteItem(uint flags, IShellItem item, int result, IShellItem created) {
165
- Recycled = result >= 0 && created != null;
166
- return 0;
167
- }
168
-
169
- public int StartOperations() { return 0; }
170
- public int FinishOperations(int result) { return 0; }
171
- public int PreRenameItem(uint flags, IShellItem item, IntPtr name) { return 0; }
172
- public int PostRenameItem(uint flags, IShellItem item, IntPtr name, int result, IShellItem created) { return 0; }
173
- public int PreMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name) { return 0; }
174
- public int PostMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created) { return 0; }
175
- public int PreCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name) { return 0; }
176
- public int PostCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created) { return 0; }
177
- public int PreNewItem(uint flags, IShellItem folder, IntPtr name) { return 0; }
178
- public int PostNewItem(uint flags, IShellItem folder, IntPtr name, IntPtr template, uint attrs, int result, IShellItem created) { return 0; }
179
- public int UpdateProgress(uint total, uint completed) { return 0; }
180
- public int ResetTimer() { return 0; }
181
- public int PauseTimer() { return 0; }
182
- public int ResumeTimer() { return 0; }
183
- }
1
+ using System;
2
+ using System.Runtime.InteropServices;
3
+
4
+ public static class ExplorerShell {
5
+ [DllImport("shell32.dll", CharSet = CharSet.Unicode, PreserveSig = false)]
6
+ private static extern IShellItem SHCreateItemFromParsingName(string path, IntPtr binding, [MarshalAs(UnmanagedType.LPStruct)] Guid iid);
7
+
8
+ [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
9
+ private static extern bool MoveFileEx(string source, string destination, uint flags);
10
+
11
+ [DllImport("user32.dll")]
12
+ public static extern uint GetClipboardSequenceNumber();
13
+
14
+ [DllImport("user32.dll")]
15
+ private static extern bool OpenClipboard(IntPtr window);
16
+
17
+ [DllImport("user32.dll")]
18
+ private static extern bool CloseClipboard();
19
+
20
+ [DllImport("user32.dll")]
21
+ private static extern bool EmptyClipboard();
22
+
23
+ [DllImport("user32.dll", CharSet = CharSet.Unicode)]
24
+ private static extern uint RegisterClipboardFormat(string format);
25
+
26
+ [DllImport("user32.dll")]
27
+ private static extern IntPtr GetClipboardData(uint format);
28
+
29
+ [DllImport("kernel32.dll")]
30
+ private static extern IntPtr GlobalLock(IntPtr memory);
31
+
32
+ [DllImport("kernel32.dll")]
33
+ private static extern bool GlobalUnlock(IntPtr memory);
34
+
35
+ [DllImport("kernel32.dll")]
36
+ private static extern UIntPtr GlobalSize(IntPtr memory);
37
+
38
+ public static bool ClipboardIsCut() {
39
+ if (!OpenClipboard(IntPtr.Zero)) throw new InvalidOperationException("Clipboard unavailable");
40
+ try {
41
+ IntPtr memory = GetClipboardData(RegisterClipboardFormat("Preferred DropEffect"));
42
+ if (memory == IntPtr.Zero || GlobalSize(memory).ToUInt64() < 4) return false;
43
+ IntPtr data = GlobalLock(memory);
44
+ if (data == IntPtr.Zero) return false;
45
+ try { return (Marshal.ReadInt32(data) & 2) != 0; }
46
+ finally { GlobalUnlock(memory); }
47
+ } finally { CloseClipboard(); }
48
+ }
49
+
50
+ public static int Rename(string source, string destination) {
51
+ return MoveFileEx(source, destination, 0) ? 0 : Marshal.GetLastWin32Error();
52
+ }
53
+
54
+ public static void ClearClipboardIfUnchanged(uint sequence) {
55
+ if (!OpenClipboard(IntPtr.Zero)) return;
56
+ try {
57
+ if (sequence == GetClipboardSequenceNumber()) EmptyClipboard();
58
+ } finally { CloseClipboard(); }
59
+ }
60
+
61
+ public static bool Transfer(string source, string destination, bool cut) {
62
+ IFileOperation operation = (IFileOperation)new FileOperation();
63
+ try {
64
+ const uint silent = 0x4, keepBoth = 0x8, noConfirmation = 0x10;
65
+ const uint noErrorUI = 0x400, onlySelected = 0x2000, earlyFailure = 0x100000;
66
+ const uint keepExtension = 0x200000, noCopyHooks = 0x800000, undoRecord = 0x20000000;
67
+ operation.SetOperationFlags(silent | keepBoth | noConfirmation | noErrorUI | onlySelected
68
+ | earlyFailure | keepExtension | noCopyHooks | undoRecord);
69
+ Guid iid = typeof(IShellItem).GUID;
70
+ IShellItem item = SHCreateItemFromParsingName(source, IntPtr.Zero, iid);
71
+ try {
72
+ IShellItem folder = SHCreateItemFromParsingName(destination, IntPtr.Zero, iid);
73
+ try {
74
+ if (cut) operation.MoveItem(item, folder, null, IntPtr.Zero);
75
+ else operation.CopyItem(item, folder, null, IntPtr.Zero);
76
+ operation.PerformOperations();
77
+ return !operation.GetAnyOperationsAborted();
78
+ } finally { Marshal.ReleaseComObject(folder); }
79
+ } finally { Marshal.ReleaseComObject(item); }
80
+ } finally { Marshal.ReleaseComObject(operation); }
81
+ }
82
+
83
+ public static bool Recycle(string source) {
84
+ IFileOperation operation = (IFileOperation)new FileOperation();
85
+ try {
86
+ const uint silent = 0x4, noConfirmation = 0x10, allowUndo = 0x40, noErrorUI = 0x400;
87
+ const uint onlySelected = 0x2000, warnPermanent = 0x4000, recycle = 0x80000;
88
+ const uint earlyFailure = 0x100000, noCopyHooks = 0x800000, undoRecord = 0x20000000;
89
+ operation.SetOperationFlags(silent | noConfirmation | allowUndo | noErrorUI | onlySelected | warnPermanent
90
+ | recycle | earlyFailure | noCopyHooks | undoRecord);
91
+ IShellItem item = SHCreateItemFromParsingName(source, IntPtr.Zero, typeof(IShellItem).GUID);
92
+ try {
93
+ RecycleOnly sink = new RecycleOnly();
94
+ operation.DeleteItem(item, sink);
95
+ operation.PerformOperations();
96
+ return sink.Recycled && !operation.GetAnyOperationsAborted();
97
+ } finally { Marshal.ReleaseComObject(item); }
98
+ } finally { Marshal.ReleaseComObject(operation); }
99
+ }
100
+
101
+ [ComImport, Guid("3AD05575-8857-4850-9277-11B85BDB8E09")]
102
+ private class FileOperation { }
103
+
104
+ [ComImport, Guid("43826D1E-E718-42EE-BC55-A1E261C37BFE"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
105
+ public interface IShellItem { }
106
+
107
+ // Keep the declaration in the native IFileOperation vtable order.
108
+ [ComImport, Guid("947AAB5F-0A5C-4C13-B4D6-4BF7836FC9F8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
109
+ private interface IFileOperation {
110
+ void Advise(IntPtr sink, out uint cookie);
111
+ void Unadvise(uint cookie);
112
+ void SetOperationFlags(uint flags);
113
+ void SetProgressMessage([MarshalAs(UnmanagedType.LPWStr)] string message);
114
+ void SetProgressDialog(IntPtr dialog);
115
+ void SetProperties(IntPtr properties);
116
+ void SetOwnerWindow(IntPtr window);
117
+ void ApplyPropertiesToItem(IShellItem item);
118
+ void ApplyPropertiesToItems(IntPtr items);
119
+ void RenameItem(IShellItem item, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
120
+ void RenameItems(IntPtr items, [MarshalAs(UnmanagedType.LPWStr)] string name);
121
+ void MoveItem(IShellItem item, IShellItem destination, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
122
+ void MoveItems(IntPtr items, IShellItem destination);
123
+ void CopyItem(IShellItem item, IShellItem destination, [MarshalAs(UnmanagedType.LPWStr)] string name, IntPtr sink);
124
+ void CopyItems(IntPtr items, IShellItem destination);
125
+ void DeleteItem(IShellItem item, IFileOperationProgressSink sink);
126
+ void DeleteItems(IntPtr items);
127
+ void NewItem(IShellItem destination, uint attributes, [MarshalAs(UnmanagedType.LPWStr)] string name,
128
+ [MarshalAs(UnmanagedType.LPWStr)] string template, IntPtr sink);
129
+ void PerformOperations();
130
+ [return: MarshalAs(UnmanagedType.Bool)]
131
+ bool GetAnyOperationsAborted();
132
+ }
133
+
134
+ [ComImport, Guid("04B0F1A7-9490-44BC-96E1-4296A31252E2"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
135
+ public interface IFileOperationProgressSink {
136
+ [PreserveSig] int StartOperations();
137
+ [PreserveSig] int FinishOperations(int result);
138
+ [PreserveSig] int PreRenameItem(uint flags, IShellItem item, IntPtr name);
139
+ [PreserveSig] int PostRenameItem(uint flags, IShellItem item, IntPtr name, int result, IShellItem created);
140
+ [PreserveSig] int PreMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name);
141
+ [PreserveSig] int PostMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created);
142
+ [PreserveSig] int PreCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name);
143
+ [PreserveSig] int PostCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created);
144
+ [PreserveSig] int PreDeleteItem(uint flags, IShellItem item);
145
+ [PreserveSig] int PostDeleteItem(uint flags, IShellItem item, int result, IShellItem created);
146
+ [PreserveSig] int PreNewItem(uint flags, IShellItem folder, IntPtr name);
147
+ [PreserveSig] int PostNewItem(uint flags, IShellItem folder, IntPtr name, IntPtr template, uint attrs, int result, IShellItem created);
148
+ [PreserveSig] int UpdateProgress(uint total, uint completed);
149
+ [PreserveSig] int ResetTimer();
150
+ [PreserveSig] int PauseTimer();
151
+ [PreserveSig] int ResumeTimer();
152
+ }
153
+
154
+ [ComVisible(true), ClassInterface(ClassInterfaceType.None)]
155
+ public sealed class RecycleOnly : IFileOperationProgressSink {
156
+ public bool Recycled { get; private set; }
157
+
158
+ public int PreDeleteItem(uint flags, IShellItem item) {
159
+ const uint recycleIfPossible = 0x80;
160
+ const int cancelled = unchecked((int)0x800704C7);
161
+ return (flags & recycleIfPossible) != 0 ? 0 : cancelled;
162
+ }
163
+
164
+ public int PostDeleteItem(uint flags, IShellItem item, int result, IShellItem created) {
165
+ Recycled = result >= 0 && created != null;
166
+ return 0;
167
+ }
168
+
169
+ public int StartOperations() { return 0; }
170
+ public int FinishOperations(int result) { return 0; }
171
+ public int PreRenameItem(uint flags, IShellItem item, IntPtr name) { return 0; }
172
+ public int PostRenameItem(uint flags, IShellItem item, IntPtr name, int result, IShellItem created) { return 0; }
173
+ public int PreMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name) { return 0; }
174
+ public int PostMoveItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created) { return 0; }
175
+ public int PreCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name) { return 0; }
176
+ public int PostCopyItem(uint flags, IShellItem item, IShellItem folder, IntPtr name, int result, IShellItem created) { return 0; }
177
+ public int PreNewItem(uint flags, IShellItem folder, IntPtr name) { return 0; }
178
+ public int PostNewItem(uint flags, IShellItem folder, IntPtr name, IntPtr template, uint attrs, int result, IShellItem created) { return 0; }
179
+ public int UpdateProgress(uint total, uint completed) { return 0; }
180
+ public int ResetTimer() { return 0; }
181
+ public int PauseTimer() { return 0; }
182
+ public int ResumeTimer() { return 0; }
183
+ }
184
184
  }
package/src/host/files.js CHANGED
@@ -4,8 +4,10 @@ import { lstat, open, opendir, readlink, realpath, stat } from 'node:fs/promises
4
4
  import { win32 as path } from 'node:path';
5
5
  import { Readable } from 'node:stream';
6
6
  import { promisify } from 'node:util';
7
+ import { officeExtensions } from '../document-types.js';
7
8
  import { thumbnailType } from '../thumbnail-types.js';
8
9
  import { underPath } from '../ui/navigation.js';
10
+ import { registerPdfAssets } from './pdf-assets.js';
9
11
  import { windowsFileOperation } from './shell-files.js';
10
12
 
11
13
  const execute = promisify(execFile);
@@ -50,6 +52,21 @@ function operationPaths(input) {
50
52
  return [...new Set(paths.map(normalizeLocalPath))];
51
53
  }
52
54
 
55
+ function fileVersion(info) {
56
+ return JSON.stringify([info.dev, info.ino, info.size, info.mtimeMs, info.ctimeMs]);
57
+ }
58
+
59
+ function decodePreviewText(data, eof) {
60
+ let text;
61
+ try {
62
+ text = new TextDecoder('utf-8', { fatal: true }).decode(data, { stream: !eof });
63
+ } catch {
64
+ throw new BrowseError('此文件不是 UTF-8 文本');
65
+ }
66
+ if (text.includes('\0')) throw new BrowseError('此格式没有可用的文本预览');
67
+ return text;
68
+ }
69
+
53
70
  async function scanDrives() {
54
71
  const powershell = path.join(process.env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe');
55
72
  const command = "[System.IO.DriveInfo]::GetDrives() | Where-Object { $_.DriveType -in 'Fixed','Removable','CDRom','Ram' } | ForEach-Object { $_.Name }";
@@ -205,7 +222,7 @@ function createDiskAccess() {
205
222
  return { matches, truncated: false, skipped };
206
223
  }
207
224
 
208
- async function read(input, limit, signal, workspace) {
225
+ async function read(input, limit, signal, workspace, { version, sniffText } = {}) {
209
226
  const target = await resolve(input, signal, workspace);
210
227
  const info = await stat(target);
211
228
  if (!info.isFile()) throw new BrowseError('只能预览普通文件');
@@ -214,28 +231,57 @@ function createDiskAccess() {
214
231
  try {
215
232
  const opened = await handle.stat();
216
233
  if (!opened.isFile()) throw new BrowseError('只能预览普通文件');
234
+ if (version && fileVersion(opened) !== version) throw new BrowseError('文件已改变,请重新预览', 409);
217
235
  const buffer = Buffer.allocUnsafe(Math.min(opened.size, limit) + 1);
218
236
  let length = 0;
219
237
  while (length < buffer.length) {
220
238
  signal.throwIfAborted();
221
- const { bytesRead } = await handle.read(buffer, length, buffer.length - length, length);
239
+ const readSize = sniffText && length === 0 ? Math.min(4096, buffer.length) : buffer.length - length;
240
+ const { bytesRead } = await handle.read(buffer, length, readSize, length);
222
241
  if (!bytesRead) break;
242
+ if (sniffText && length === 0) decodePreviewText(buffer.subarray(0, bytesRead), false);
223
243
  length += bytesRead;
224
244
  }
225
245
  const eof = length <= limit && length <= opened.size;
226
246
  if (limit === maxBytes && (!eof || opened.size > limit)) throw new BrowseError('文件超过 32 MB 读取上限', 413);
247
+ if (version && (!eof || fileVersion(await handle.stat()) !== version)) {
248
+ throw new BrowseError('文件已改变,请重新预览', 409);
249
+ }
227
250
  return { data: buffer.subarray(0, Math.min(length, limit)), eof };
228
251
  } finally {
229
252
  await handle.close();
230
253
  }
231
254
  }
232
255
 
256
+ async function renderOffice(input, signal, workspace, converter) {
257
+ const absolute = normalizeLocalPath(input);
258
+ const extension = path.extname(absolute).slice(1).toLowerCase();
259
+ if (!officeExtensions.has(extension)) throw new BrowseError('不是可预览的 Office 格式');
260
+ const target = await resolve(absolute, signal, workspace);
261
+ const info = await stat(target);
262
+ if (!info.isFile()) throw new BrowseError('只能预览普通文件');
263
+ if (info.size > maxBytes) throw new BrowseError('文件超过 32 MB 读取上限', 413);
264
+ const version = fileVersion(info);
265
+ const result = await converter.convert({
266
+ extension, priority: 'foreground',
267
+ source: {
268
+ key: JSON.stringify(['dsh-workspace-explorer', workspace?.nativeRoot, target]),
269
+ version, bytes: info.size,
270
+ read: async (readSignal, budget) => {
271
+ const { data } = await read(absolute, Math.min(budget, maxBytes), readSignal, workspace, { version });
272
+ return { bytes: data, version };
273
+ },
274
+ },
275
+ }, signal);
276
+ return result.pdf;
277
+ }
278
+
233
279
  async function locate(input, signal, workspace) {
234
280
  const absolute = normalizeLocalPath(input);
235
281
  const target = await resolve(absolute, signal, workspace);
236
282
  const info = await stat(target);
237
283
  if (info.isDirectory()) return { path: absolute, nativePath: target, type: 'directory' };
238
- if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute) };
284
+ if (info.isFile()) return { path: absolute, nativePath: target, type: 'file', name: path.basename(absolute), size: info.size };
239
285
  throw new BrowseError('该路径不指向普通文件或文件夹');
240
286
  }
241
287
 
@@ -401,7 +447,7 @@ function createDiskAccess() {
401
447
  }
402
448
  }
403
449
 
404
- return { list, read, locate, thumbnail, stage, rename, paste, recycle, scope, search };
450
+ return { list, read, renderOffice, locate, thumbnail, stage, rename, paste, recycle, scope, search };
405
451
  }
406
452
 
407
453
  function byteRange(value, size) {
@@ -428,6 +474,7 @@ function json(value, status = 200) {
428
474
 
429
475
  export function registerDesktopFiles(ctx) {
430
476
  const disks = createDiskAccess();
477
+ registerPdfAssets(ctx);
431
478
  let writes = Promise.resolve();
432
479
  function write(action) {
433
480
  const result = writes.then(action);
@@ -513,20 +560,18 @@ export function registerDesktopFiles(ctx) {
513
560
  return json({ url: thumbnailEndpoint + '?ticket=' + token });
514
561
  }
515
562
  if (operation === 'text') {
516
- const { data, eof } = await disks.read(input, maxText, signal, workspace);
517
- let text;
518
- try {
519
- text = new TextDecoder('utf-8', { fatal: true }).decode(data, { stream: !eof });
520
- } catch {
521
- throw new BrowseError('此文件不是 UTF-8 文本');
522
- }
523
- if (text.includes('\0')) throw new BrowseError('此格式没有可用的文本预览');
524
- return json({ text, eof });
563
+ const { data, eof } = await disks.read(input, maxText, signal, workspace, { sniffText: true });
564
+ return json({ text: decodePreviewText(data, eof), eof });
525
565
  }
526
- if (operation === 'bytes') {
527
- const { data } = await disks.read(input, maxBytes, signal, workspace);
566
+ if (operation === 'bytes' || operation === 'office') {
567
+ const converter = operation === 'office' ? ctx.officeToPdf : undefined;
568
+ if (operation === 'office' && !converter) return json({ error: '当前 DSH 未提供 Office 转换服务' }, 503);
569
+ const data = converter
570
+ ? await disks.renderOffice(input, signal, workspace, converter)
571
+ : (await disks.read(input, maxBytes, signal, workspace)).data;
528
572
  return new Response(data, { headers: {
529
- 'content-type': 'application/octet-stream', 'cache-control': 'no-store', 'x-content-type-options': 'nosniff',
573
+ 'content-type': converter ? 'application/pdf' : 'application/octet-stream',
574
+ 'cache-control': 'no-store', 'x-content-type-options': 'nosniff',
530
575
  } });
531
576
  }
532
577
  throw new BrowseError('无效的文件操作');
@@ -0,0 +1,30 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { createRequire } from 'node:module';
3
+ import { dirname, join } from 'node:path';
4
+ import { pdfAssetsPath } from '../document-types.js';
5
+
6
+ const require = createRequire(import.meta.url);
7
+ const assetName = /^(?:build\/pdf(?:\.worker)?\.min\.mjs|cmaps\/[\w.-]+\.bcmap|standard_fonts\/[\w.-]+\.(?:pfb|ttf)|wasm\/[\w.-]+\.(?:wasm|js))$/;
8
+
9
+ export function registerPdfAssets(ctx) {
10
+ ctx.connection.fetch.register({
11
+ path: pdfAssetsPath,
12
+ methods: ['GET'],
13
+ requestBody: 'buffered',
14
+ async fetch(request) {
15
+ const file = new URL(request.url).searchParams.get('file');
16
+ if (!file || !assetName.test(file)) return new Response(null, { status: 404 });
17
+ try {
18
+ const root = dirname(require.resolve('pdfjs-dist/package.json'));
19
+ const data = await readFile(join(root, file));
20
+ const contentType = file.endsWith('.wasm') ? 'application/wasm'
21
+ : /\.m?js$/.test(file) ? 'text/javascript' : 'application/octet-stream';
22
+ return new Response(data, { headers: {
23
+ 'content-type': contentType, 'x-content-type-options': 'nosniff', 'cache-control': 'no-store',
24
+ } });
25
+ } catch {
26
+ return new Response(null, { status: 404 });
27
+ }
28
+ },
29
+ });
30
+ }