@mqn00/file-manager-plugin-compress 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.
@@ -0,0 +1,405 @@
1
+ // src/style.ts
2
+ var STYLE_ID = "fcp-style";
3
+ var CSS = `
4
+ .fcp-dialog-body { padding: 4px 0; }
5
+ .fcp-sec-title { color: var(--app-text-dim, #888); font-size: 12px; margin: 10px 0 6px; }
6
+ .fcp-sel-list {
7
+ max-height: 140px; overflow-y: auto;
8
+ border: 1px solid var(--app-border, #ccc); border-radius: 4px;
9
+ padding: 6px 8px; background: var(--app-accent-bg, transparent);
10
+ }
11
+ .fcp-sel-item {
12
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
13
+ padding: 2px 0; font-size: 13px; color: var(--app-text, #333);
14
+ }
15
+ .fcp-sel-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
16
+ .fcp-sel-meta { flex-shrink: 0; color: var(--app-text-dim, #888); font-size: 12px; }
17
+ .fcp-out-row { display: flex; align-items: center; gap: 8px; }
18
+ .fcp-target { font-size: 12px; color: var(--app-text-dim, #888); margin-top: 6px; word-break: break-all; }
19
+ .fcp-status-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; max-height: 180px; overflow-y: auto; }
20
+ .fcp-status-row {
21
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
22
+ font-size: 13px; color: var(--app-text, #333);
23
+ padding: 2px 0;
24
+ }
25
+ .fcp-status-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
26
+ .fcp-dim { color: var(--app-text-dim, #888); font-size: 12px; }
27
+ .fcp-picker-path { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
28
+ .fcp-picker-list {
29
+ max-height: 320px; overflow-y: auto;
30
+ border: 1px solid var(--app-border, #ccc); border-radius: 4px; padding: 4px;
31
+ background: var(--app-accent-bg, transparent);
32
+ }
33
+ .fcp-picker-row {
34
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px;
35
+ cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--app-text, #333);
36
+ }
37
+ .fcp-picker-icon {
38
+ display: flex; align-items: center; flex-shrink: 0;
39
+ color: var(--app-accent, #409eff);
40
+ }
41
+ .fcp-picker-row:hover { background: var(--app-accent-bg-hover, var(--app-accent-bg, #f0f0f0)); }
42
+ .fcp-picker-empty { color: var(--app-text-dim, #888); font-size: 12px; padding: 8px; }
43
+ `;
44
+ function injectStyles() {
45
+ if (document.getElementById(STYLE_ID)) return;
46
+ const style = document.createElement("style");
47
+ style.id = STYLE_ID;
48
+ style.textContent = CSS;
49
+ document.head.appendChild(style);
50
+ }
51
+ function removeStyles() {
52
+ document.getElementById(STYLE_ID)?.remove();
53
+ }
54
+
55
+ // src/dialog.ts
56
+ var HOST_ID = "fcp-dialog-host";
57
+ var activeDialog = null;
58
+ function closeCompressDialog() {
59
+ if (!activeDialog) return;
60
+ try {
61
+ activeDialog.app.unmount();
62
+ } catch {
63
+ }
64
+ try {
65
+ activeDialog.host.remove();
66
+ } catch {
67
+ }
68
+ activeDialog = null;
69
+ }
70
+ function parentOf(pathStr) {
71
+ const parts = pathStr.split("/").filter(Boolean);
72
+ parts.pop();
73
+ return parts.join("/");
74
+ }
75
+ function displayPath(pathStr) {
76
+ return pathStr || "/";
77
+ }
78
+ function renderFolderIcon(h) {
79
+ return h(
80
+ "svg",
81
+ {
82
+ viewBox: "0 0 1024 1024",
83
+ width: "16",
84
+ height: "16",
85
+ "aria-hidden": "true",
86
+ style: { display: "block" }
87
+ },
88
+ [
89
+ h("path", {
90
+ d: "M880 298.4H521L403.7 186.2a8.16 8.16 0 0 0-5.5-2.2H144c-17.7 0-32 14.3-32 32v592c0 17.7 14.3 32 32 32h736c17.7 0 32-14.3 32-32V330.4c0-17.7-14.3-32-32-32z",
91
+ fill: "currentColor"
92
+ })
93
+ ]
94
+ );
95
+ }
96
+ function openCompressDialog(ctx, payload) {
97
+ const { createApp, defineComponent, h, ref, computed, watch } = ctx.Vue;
98
+ const { ElDialog, ElButton, ElInput, ElAlert, ElTag, ElMessage } = ctx.ElementPlus;
99
+ const formatSize = ctx.utils.formatSize;
100
+ const api = ctx.api.instance;
101
+ closeCompressDialog();
102
+ const host = document.createElement("div");
103
+ host.id = HOST_ID;
104
+ document.body.appendChild(host);
105
+ const EllipsisPath = defineComponent({
106
+ name: "EllipsisPath",
107
+ props: { path: { type: String, default: "" } },
108
+ setup(props) {
109
+ return () => h("span", { class: "fcp-sel-name", style: { flex: 1 } }, displayPath(props.path));
110
+ }
111
+ });
112
+ const CompressDialog = defineComponent({
113
+ name: "CompressDialog",
114
+ setup() {
115
+ const visible = ref(true);
116
+ const outputDir = ref(payload.currentPath || "/");
117
+ const checkResult = ref(null);
118
+ const checking = ref(false);
119
+ const runCheck = async () => {
120
+ checking.value = true;
121
+ checkResult.value = null;
122
+ try {
123
+ const resp = await api.post("/plugin/compress/check", {
124
+ paths: payload.selected,
125
+ outputDir: outputDir.value
126
+ });
127
+ checkResult.value = resp.data;
128
+ } catch (e) {
129
+ const msg = e?.response?.data?.message;
130
+ ElMessage.error(msg || "\u6743\u9650\u68C0\u67E5\u5931\u8D25");
131
+ checkResult.value = null;
132
+ } finally {
133
+ checking.value = false;
134
+ }
135
+ };
136
+ watch(() => outputDir.value, runCheck);
137
+ void runCheck();
138
+ const canStart = computed(
139
+ () => !!checkResult.value?.ok && !checking.value && payload.selected.length > 0
140
+ );
141
+ const refreshCurrentDir = async () => {
142
+ try {
143
+ const resp = await api.get("/files", {
144
+ params: { path: ctx.stores.file.currentPath }
145
+ });
146
+ ctx.stores.file.setFiles(resp.data.files);
147
+ } catch {
148
+ }
149
+ };
150
+ const startCompress = async () => {
151
+ if (!canStart.value) return;
152
+ try {
153
+ const resp = await api.post("/plugin/compress/zip", {
154
+ paths: payload.selected,
155
+ outputDir: outputDir.value
156
+ });
157
+ const { taskId } = resp.data;
158
+ const info = {
159
+ id: taskId,
160
+ type: "compress",
161
+ status: "running",
162
+ phase: "compress",
163
+ progress: 0,
164
+ speed: 0,
165
+ totalSize: 0,
166
+ startTime: Date.now(),
167
+ metadata: {
168
+ paths: payload.selected.slice(),
169
+ names: payload.infos.map((i) => i.name),
170
+ outputDir: outputDir.value,
171
+ targetPath: checkResult.value?.targetPath || ""
172
+ },
173
+ completedCount: 0,
174
+ totalCount: payload.selected.length,
175
+ totalItemCount: 0,
176
+ processedItemCount: 0
177
+ };
178
+ ctx.stores.task.attachTask(taskId, info, makeConditionalRefresh());
179
+ ElMessage.success("\u538B\u7F29\u4EFB\u52A1\u5DF2\u63A8\u9001\u5230\u540E\u53F0");
180
+ visible.value = false;
181
+ } catch (e) {
182
+ ElMessage.error(e?.response?.data?.message || "\u521B\u5EFA\u538B\u7F29\u4EFB\u52A1\u5931\u8D25");
183
+ }
184
+ };
185
+ const makeConditionalRefresh = () => {
186
+ const outputDirSnapshot = outputDir.value;
187
+ return () => {
188
+ const current = ctx.stores.file.currentPath || "/";
189
+ if (current === outputDirSnapshot) {
190
+ void refreshCurrentDir();
191
+ }
192
+ };
193
+ };
194
+ const pickerVisible = ref(false);
195
+ const pickerPath = ref(payload.currentPath || "/");
196
+ const pickerFolders = ref([]);
197
+ const pickerLoading = ref(false);
198
+ const loadPickerFolders = async (p) => {
199
+ pickerLoading.value = true;
200
+ try {
201
+ const resp = await api.get("/files", { params: { path: p } });
202
+ pickerFolders.value = (resp.data?.files || []).filter((f) => f.isDirectory && !f.broken).map((f) => f.path);
203
+ } catch {
204
+ pickerFolders.value = [];
205
+ } finally {
206
+ pickerLoading.value = false;
207
+ }
208
+ };
209
+ const openPicker = () => {
210
+ pickerPath.value = outputDir.value;
211
+ pickerVisible.value = true;
212
+ void loadPickerFolders(pickerPath.value);
213
+ };
214
+ const pickerUp = () => {
215
+ pickerPath.value = parentOf(pickerPath.value);
216
+ void loadPickerFolders(pickerPath.value);
217
+ };
218
+ const pickerEnter = (p) => {
219
+ pickerPath.value = p;
220
+ void loadPickerFolders(p);
221
+ };
222
+ const pickerConfirm = () => {
223
+ outputDir.value = pickerPath.value;
224
+ pickerVisible.value = false;
225
+ };
226
+ return () => {
227
+ const selectedRows = payload.infos.map(
228
+ (info) => h("div", { class: "fcp-sel-item", key: info.path }, [
229
+ h("span", { class: "fcp-sel-name" }, info.name),
230
+ h(
231
+ "span",
232
+ { class: "fcp-sel-meta" },
233
+ info.isDirectory ? "\u6587\u4EF6\u5939" : formatSize(info.size)
234
+ )
235
+ ])
236
+ );
237
+ const statusChildren = [];
238
+ if (checkResult.value) {
239
+ for (const item of checkResult.value.items) {
240
+ const ok = item.exists && item.readable;
241
+ statusChildren.push(
242
+ h("div", { class: "fcp-status-row", key: item.path }, [
243
+ h(
244
+ "span",
245
+ { class: "fcp-status-name" },
246
+ `${item.name}${item.kind === "dir" ? "\uFF08\u6587\u4EF6\u5939\uFF09" : ""}`
247
+ ),
248
+ h(
249
+ ElTag,
250
+ { type: ok ? "success" : "danger", size: "small", effect: "plain" },
251
+ () => !item.exists ? "\u4E0D\u5B58\u5728" : ok ? "\u53EF\u8BFB" : "\u4E0D\u53EF\u8BFB"
252
+ )
253
+ ])
254
+ );
255
+ }
256
+ const out = checkResult.value.output;
257
+ const outOk = out.exists && out.isDir && out.writable;
258
+ statusChildren.push(
259
+ h("div", { class: "fcp-status-row", key: "__output__" }, [
260
+ h("span", { class: "fcp-status-name" }, "\u8F93\u51FA\u6587\u4EF6\u5939"),
261
+ h(
262
+ ElTag,
263
+ { type: outOk ? "success" : "danger", size: "small", effect: "plain" },
264
+ () => !out.exists ? "\u4E0D\u5B58\u5728" : outOk ? "\u53EF\u5199" : "\u4E0D\u53EF\u5199"
265
+ )
266
+ ])
267
+ );
268
+ }
269
+ const forbiddenAlert = checkResult.value?.forbidden && checkResult.value.forbiddenMessage ? h(
270
+ ElAlert,
271
+ {
272
+ title: checkResult.value.forbiddenMessage,
273
+ type: "warning",
274
+ showIcon: true,
275
+ closable: false,
276
+ style: { marginTop: "8px" }
277
+ }
278
+ ) : null;
279
+ const pickerDialog = h(
280
+ ElDialog,
281
+ {
282
+ modelValue: pickerVisible.value,
283
+ title: "\u9009\u62E9\u8F93\u51FA\u6587\u4EF6\u5939",
284
+ width: "460px",
285
+ appendToBody: true,
286
+ "onUpdate:modelValue": (v) => {
287
+ pickerVisible.value = v;
288
+ }
289
+ },
290
+ {
291
+ default: () => [
292
+ h("div", { class: "fcp-picker-path" }, [
293
+ h(EllipsisPath, { path: pickerPath.value }),
294
+ h(
295
+ ElButton,
296
+ {
297
+ size: "small",
298
+ disabled: !pickerPath.value || pickerPath.value === "/",
299
+ onClick: pickerUp
300
+ },
301
+ () => "\u4E0A\u7EA7"
302
+ )
303
+ ]),
304
+ pickerLoading.value ? h("div", { class: "fcp-dim" }, "\u52A0\u8F7D\u4E2D\u2026") : pickerFolders.value.length === 0 ? h("div", { class: "fcp-picker-empty" }, "\uFF08\u8BE5\u6587\u4EF6\u5939\u4E0B\u6CA1\u6709\u5B50\u6587\u4EF6\u5939\uFF09") : h(
305
+ "div",
306
+ { class: "fcp-picker-list" },
307
+ pickerFolders.value.map(
308
+ (p) => h(
309
+ "div",
310
+ { class: "fcp-picker-row", key: p, onClick: () => pickerEnter(p) },
311
+ [
312
+ h("span", { class: "fcp-picker-icon" }, [renderFolderIcon(h)]),
313
+ h("span", { class: "fcp-sel-name" }, p.split("/").pop() || p)
314
+ ]
315
+ )
316
+ )
317
+ )
318
+ ],
319
+ footer: () => [
320
+ h(
321
+ ElButton,
322
+ { size: "small", onClick: () => pickerVisible.value = false },
323
+ () => "\u53D6\u6D88"
324
+ ),
325
+ h(ElButton, { size: "small", type: "primary", onClick: pickerConfirm }, () => "\u9009\u62E9")
326
+ ]
327
+ }
328
+ );
329
+ return h(
330
+ ElDialog,
331
+ {
332
+ modelValue: visible.value,
333
+ title: `\u538B\u7F29\uFF08${payload.infos.length} \u9879\uFF09`,
334
+ width: "560px",
335
+ appendToBody: true,
336
+ closeOnClickModal: false,
337
+ "onUpdate:modelValue": (v) => {
338
+ visible.value = v;
339
+ if (!v) close();
340
+ }
341
+ },
342
+ {
343
+ default: () => [
344
+ h("div", { class: "fcp-dialog-body" }, [
345
+ h("div", { class: "fcp-sec-title" }, "\u5DF2\u9009\u6761\u76EE"),
346
+ h("div", { class: "fcp-sel-list" }, selectedRows),
347
+ h("div", { class: "fcp-sec-title" }, "\u8F93\u51FA\u4F4D\u7F6E"),
348
+ h("div", { class: "fcp-out-row" }, [
349
+ h(ElInput, {
350
+ modelValue: displayPath(outputDir.value),
351
+ readonly: true,
352
+ placeholder: "\u8BF7\u9009\u62E9\u8F93\u51FA\u6587\u4EF6\u5939",
353
+ style: { flex: 1 }
354
+ }),
355
+ h(ElButton, { size: "default", onClick: openPicker }, () => "\u9009\u62E9")
356
+ ]),
357
+ checkResult.value?.targetPath ? h("div", { class: "fcp-target" }, `\u8F93\u51FA\u6587\u4EF6\uFF1A${checkResult.value.targetPath}`) : null,
358
+ h("div", { class: "fcp-sec-title" }, "\u6743\u9650\u68C0\u67E5"),
359
+ checking.value ? h("div", { class: "fcp-dim" }, "\u6B63\u5728\u68C0\u67E5\u6743\u9650\u2026") : checkResult.value ? h("div", { class: "fcp-status-list" }, statusChildren) : h("div", { class: "fcp-dim" }, "\u6743\u9650\u68C0\u67E5\u5931\u8D25"),
360
+ forbiddenAlert
361
+ ]),
362
+ pickerDialog
363
+ ],
364
+ footer: () => [
365
+ h(ElButton, { onClick: () => visible.value = false }, () => "\u5173\u95ED"),
366
+ h(
367
+ ElButton,
368
+ { type: "primary", disabled: !canStart.value, onClick: startCompress },
369
+ () => "\u5F00\u59CB\u538B\u7F29"
370
+ )
371
+ ]
372
+ }
373
+ );
374
+ };
375
+ }
376
+ });
377
+ const rootApp = createApp(CompressDialog);
378
+ rootApp.mount(host);
379
+ activeDialog = { app: rootApp, host };
380
+ }
381
+
382
+ // src/frontend.ts
383
+ var install = (ctx) => {
384
+ injectStyles();
385
+ const api = window.__fm_bulk_actions;
386
+ if (!api || typeof api.register !== "function") {
387
+ console.warn("[compress] \u4E3B\u5E94\u7528\u672A\u66B4\u9732\u6279\u91CF\u64CD\u4F5C\u6CE8\u518C\u8868\uFF08__fm_bulk_actions\uFF09\uFF0C\u538B\u7F29\u6309\u94AE\u4E0D\u53EF\u7528");
388
+ return;
389
+ }
390
+ api.register({
391
+ id: "compress",
392
+ label: "\u538B\u7F29",
393
+ // 文件/文件夹均可压缩,多选亦支持;只要选中了任意条目即显示
394
+ visible: (p) => p.count > 0,
395
+ run: (payload) => openCompressDialog(ctx, payload)
396
+ });
397
+ return () => {
398
+ api.unregister?.("compress");
399
+ closeCompressDialog();
400
+ removeStyles();
401
+ };
402
+ };
403
+ export {
404
+ install
405
+ };
@@ -0,0 +1,6 @@
1
+ /**
2
+ * compress 插件样式注入(fcp- 前缀,颜色只用主题令牌带 fallback)
3
+ */
4
+ export declare function injectStyles(): void;
5
+ /** 移除注入样式(插件 teardown 调用) */
6
+ export declare function removeStyles(): void;
package/dist/style.js ADDED
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ /**
3
+ * compress 插件样式注入(fcp- 前缀,颜色只用主题令牌带 fallback)
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.injectStyles = injectStyles;
7
+ exports.removeStyles = removeStyles;
8
+ const STYLE_ID = 'fcp-style';
9
+ const CSS = `
10
+ .fcp-dialog-body { padding: 4px 0; }
11
+ .fcp-sec-title { color: var(--app-text-dim, #888); font-size: 12px; margin: 10px 0 6px; }
12
+ .fcp-sel-list {
13
+ max-height: 140px; overflow-y: auto;
14
+ border: 1px solid var(--app-border, #ccc); border-radius: 4px;
15
+ padding: 6px 8px; background: var(--app-accent-bg, transparent);
16
+ }
17
+ .fcp-sel-item {
18
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
19
+ padding: 2px 0; font-size: 13px; color: var(--app-text, #333);
20
+ }
21
+ .fcp-sel-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
22
+ .fcp-sel-meta { flex-shrink: 0; color: var(--app-text-dim, #888); font-size: 12px; }
23
+ .fcp-out-row { display: flex; align-items: center; gap: 8px; }
24
+ .fcp-target { font-size: 12px; color: var(--app-text-dim, #888); margin-top: 6px; word-break: break-all; }
25
+ .fcp-status-list { display: flex; flex-direction: column; gap: 4px; margin-top: 6px; max-height: 180px; overflow-y: auto; }
26
+ .fcp-status-row {
27
+ display: flex; align-items: center; justify-content: space-between; gap: 8px;
28
+ font-size: 13px; color: var(--app-text, #333);
29
+ padding: 2px 0;
30
+ }
31
+ .fcp-status-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
32
+ .fcp-dim { color: var(--app-text-dim, #888); font-size: 12px; }
33
+ .fcp-picker-path { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
34
+ .fcp-picker-list {
35
+ max-height: 320px; overflow-y: auto;
36
+ border: 1px solid var(--app-border, #ccc); border-radius: 4px; padding: 4px;
37
+ background: var(--app-accent-bg, transparent);
38
+ }
39
+ .fcp-picker-row {
40
+ display: flex; align-items: center; gap: 8px; padding: 6px 8px;
41
+ cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--app-text, #333);
42
+ }
43
+ .fcp-picker-icon {
44
+ display: flex; align-items: center; flex-shrink: 0;
45
+ color: var(--app-accent, #409eff);
46
+ }
47
+ .fcp-picker-row:hover { background: var(--app-accent-bg-hover, var(--app-accent-bg, #f0f0f0)); }
48
+ .fcp-picker-empty { color: var(--app-text-dim, #888); font-size: 12px; padding: 8px; }
49
+ `;
50
+ function injectStyles() {
51
+ if (document.getElementById(STYLE_ID))
52
+ return;
53
+ const style = document.createElement('style');
54
+ style.id = STYLE_ID;
55
+ style.textContent = CSS;
56
+ document.head.appendChild(style);
57
+ }
58
+ /** 移除注入样式(插件 teardown 调用) */
59
+ function removeStyles() {
60
+ document.getElementById(STYLE_ID)?.remove();
61
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@mqn00/file-manager-plugin-compress",
3
+ "version": "0.1.0",
4
+ "description": "File Manager 压缩插件:支持多选文件/文件夹压缩为 zip,可指定输出文件夹(默认当前文件夹),压缩前预检源读取权限与输出目录写入权限,SSE 进度与取消",
5
+ "main": "dist/backend.js",
6
+ "exports": {
7
+ ".": "./dist/backend.js",
8
+ "./frontend": "./dist/frontend.js"
9
+ },
10
+ "files": [
11
+ "dist/",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc && node build.mjs",
16
+ "dev": "tsc --watch",
17
+ "build:frontend": "node build.mjs",
18
+ "test": "vitest run"
19
+ },
20
+ "peerDependencies": {
21
+ "@mqn00/file-manager": "^3.0.0"
22
+ },
23
+ "dependencies": {
24
+ "archiver": "^7.0.1"
25
+ },
26
+ "devDependencies": {
27
+ "@mqn00/file-manager": "file:../../backend",
28
+ "@types/archiver": "^7.0.0",
29
+ "esbuild": "^0.28.1",
30
+ "typescript": "^5.4.0",
31
+ "vitest": "^3.0.0"
32
+ },
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/mqnu00/file-manager.git",
36
+ "directory": "plugins/compress"
37
+ },
38
+ "keywords": [
39
+ "file-manager",
40
+ "file-manager-plugin",
41
+ "compress",
42
+ "zip",
43
+ "archive"
44
+ ],
45
+ "author": "mqn00",
46
+ "license": "MIT",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ }
50
+ }