@copylee/dsh-office 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/LICENSE +21 -0
- package/README.md +104 -0
- package/cordis.patch.yml +4 -0
- package/helper/OfficeHelper.cs +1293 -0
- package/lib/client.js +534 -0
- package/lib/index.js +6417 -0
- package/lib/types/client/accent.d.ts +39 -0
- package/lib/types/client/index.d.ts +39 -0
- package/lib/types/client/nav-icon.d.ts +12 -0
- package/lib/types/format.d.ts +88 -0
- package/lib/types/helper-client.d.ts +31 -0
- package/lib/types/index.d.ts +12 -0
- package/lib/types/prompt.d.ts +2 -0
- package/lib/types/settings.d.ts +16 -0
- package/lib/types/tools.d.ts +24 -0
- package/package.json +122 -0
package/lib/client.js
ADDED
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({
|
|
2
|
+
id: "@copylee/dsh-office",
|
|
3
|
+
factory: (require) => {
|
|
4
|
+
var module = { exports: {} };
|
|
5
|
+
var exports = module.exports;
|
|
6
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
7
|
+
//#region \0rolldown/runtime.js
|
|
8
|
+
var __create = Object.create;
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
11
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
12
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
13
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
16
|
+
key = keys[i];
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
18
|
+
get: ((k) => from[k]).bind(null, key),
|
|
19
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule || !__hasOwnProp.call(mod, "default") ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
//#endregion
|
|
29
|
+
let react = require("react");
|
|
30
|
+
react = __toESM(react, 1);
|
|
31
|
+
//#region lib/types/settings.js
|
|
32
|
+
/**
|
|
33
|
+
* Plugin settings shared by the host and the settings page: defaults and the
|
|
34
|
+
* tolerant reader for the Loader entry config. No schemastery here so the
|
|
35
|
+
* browser bundle stays small.
|
|
36
|
+
*/
|
|
37
|
+
/** The Loader entry id (settings namespace); see cordis.patch.yml. */
|
|
38
|
+
const ENTRY_ID = "copylee-office";
|
|
39
|
+
const DEFAULTS = {
|
|
40
|
+
showOnOpen: true,
|
|
41
|
+
renderWidth: 1100
|
|
42
|
+
};
|
|
43
|
+
/** Unwrap `.volatile()` refs (`{ get() }`) and fall back to defaults for bad values. */
|
|
44
|
+
function resolveConfig(raw) {
|
|
45
|
+
const out = {};
|
|
46
|
+
if (raw !== null && typeof raw === "object") for (const [key, value] of Object.entries(raw)) out[key] = value !== null && typeof value === "object" && typeof value.get === "function" ? value.get() : value;
|
|
47
|
+
const width = out.renderWidth;
|
|
48
|
+
return {
|
|
49
|
+
showOnOpen: typeof out.showOnOpen === "boolean" ? out.showOnOpen : DEFAULTS.showOnOpen,
|
|
50
|
+
renderWidth: typeof width === "number" && Number.isFinite(width) ? Math.min(2e3, Math.max(600, Math.round(width))) : DEFAULTS.renderWidth
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//#endregion
|
|
54
|
+
//#region lib/types/client/accent.js
|
|
55
|
+
/**
|
|
56
|
+
* The accent colour shared by the copylee DSH plugins (docs/ui-spec.md).
|
|
57
|
+
*
|
|
58
|
+
* One choice — terracotta orange by default, blue or black — is kept in the
|
|
59
|
+
* browser under a key every plugin reads, and painted as two CSS variables on
|
|
60
|
+
* `<body>`, where the Host keeps its own theme variables. A plugin's styles
|
|
61
|
+
* only ever write `var(--cl-accent)` / `var(--cl-accent-ink)`, so changing the
|
|
62
|
+
* colour in any plugin's settings recolours all of them at once.
|
|
63
|
+
*
|
|
64
|
+
* This file is identical in every plugin; change it in all of them together.
|
|
65
|
+
*/
|
|
66
|
+
const ACCENTS = {
|
|
67
|
+
orange: {
|
|
68
|
+
name: "陶土橙",
|
|
69
|
+
accent: "#D97757",
|
|
70
|
+
ink: "#FFFFFF"
|
|
71
|
+
},
|
|
72
|
+
blue: {
|
|
73
|
+
name: "蓝色",
|
|
74
|
+
accent: "#3D63E6",
|
|
75
|
+
ink: "#FFFFFF"
|
|
76
|
+
},
|
|
77
|
+
black: {
|
|
78
|
+
name: "黑色",
|
|
79
|
+
accent: "var(--dsw-alias-label-primary, #1F1E1D)",
|
|
80
|
+
ink: "var(--dsw-alias-bg-base, #FFFFFF)"
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const ACCENT_IDS = Object.keys(ACCENTS);
|
|
84
|
+
const DEFAULT_ACCENT = "orange";
|
|
85
|
+
/** Use these in styles; the fallbacks cover the moment before the first paint. */
|
|
86
|
+
const ACCENT = `var(--cl-accent, ${ACCENTS[DEFAULT_ACCENT].accent})`;
|
|
87
|
+
const ACCENT_INK = `var(--cl-accent-ink, ${ACCENTS[DEFAULT_ACCENT].ink})`;
|
|
88
|
+
const KEY = "copylee.dsh.accent";
|
|
89
|
+
const EVENT = "copylee-dsh-accent";
|
|
90
|
+
function isAccent(value) {
|
|
91
|
+
return typeof value === "string" && Object.hasOwn(ACCENTS, value);
|
|
92
|
+
}
|
|
93
|
+
/** The choice made in this browser, or null while none has been made. */
|
|
94
|
+
function storedAccent() {
|
|
95
|
+
try {
|
|
96
|
+
const stored = localStorage.getItem(KEY);
|
|
97
|
+
return isAccent(stored) ? stored : null;
|
|
98
|
+
} catch {
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function readAccent() {
|
|
103
|
+
return storedAccent() ?? "orange";
|
|
104
|
+
}
|
|
105
|
+
function paint() {
|
|
106
|
+
const colors = ACCENTS[readAccent()];
|
|
107
|
+
document.body?.style.setProperty("--cl-accent", colors.accent);
|
|
108
|
+
document.body?.style.setProperty("--cl-accent-ink", colors.ink);
|
|
109
|
+
}
|
|
110
|
+
/** Choose the accent for every plugin. */
|
|
111
|
+
function writeAccent(accent) {
|
|
112
|
+
try {
|
|
113
|
+
localStorage.setItem(KEY, accent);
|
|
114
|
+
} catch {}
|
|
115
|
+
paint();
|
|
116
|
+
window.dispatchEvent(new Event(EVENT));
|
|
117
|
+
}
|
|
118
|
+
/** Paint the current accent and keep it current. Returns the undo for plugin disposal. */
|
|
119
|
+
function installAccent() {
|
|
120
|
+
if (typeof document === "undefined") return () => {};
|
|
121
|
+
paint();
|
|
122
|
+
if (document.body === null) document.addEventListener("DOMContentLoaded", paint, { once: true });
|
|
123
|
+
window.addEventListener(EVENT, paint);
|
|
124
|
+
window.addEventListener("storage", paint);
|
|
125
|
+
return () => {
|
|
126
|
+
window.removeEventListener(EVENT, paint);
|
|
127
|
+
window.removeEventListener("storage", paint);
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
function useAccent() {
|
|
131
|
+
const [accent, setAccent] = react.useState(readAccent);
|
|
132
|
+
react.useEffect(() => {
|
|
133
|
+
const sync = () => setAccent(readAccent());
|
|
134
|
+
window.addEventListener(EVENT, sync);
|
|
135
|
+
window.addEventListener("storage", sync);
|
|
136
|
+
return () => {
|
|
137
|
+
window.removeEventListener(EVENT, sync);
|
|
138
|
+
window.removeEventListener("storage", sync);
|
|
139
|
+
};
|
|
140
|
+
}, []);
|
|
141
|
+
return [accent, writeAccent];
|
|
142
|
+
}
|
|
143
|
+
/** A row of round swatches: one of them is always chosen. */
|
|
144
|
+
function AccentPicker({ label = "强调色", hint = "对 copylee 的全部插件生效", names }) {
|
|
145
|
+
const [accent, choose] = useAccent();
|
|
146
|
+
const h = react.createElement;
|
|
147
|
+
const name = (id) => names?.[id] ?? ACCENTS[id].name;
|
|
148
|
+
return h("div", { style: {
|
|
149
|
+
display: "flex",
|
|
150
|
+
alignItems: "center",
|
|
151
|
+
gap: 10,
|
|
152
|
+
fontSize: 13,
|
|
153
|
+
flexWrap: "wrap"
|
|
154
|
+
} }, h("span", { style: { fontWeight: 500 } }, label), h("span", {
|
|
155
|
+
role: "radiogroup",
|
|
156
|
+
"aria-label": label,
|
|
157
|
+
style: {
|
|
158
|
+
display: "inline-flex",
|
|
159
|
+
gap: 8
|
|
160
|
+
}
|
|
161
|
+
}, ...ACCENT_IDS.map((id) => h("button", {
|
|
162
|
+
key: id,
|
|
163
|
+
type: "button",
|
|
164
|
+
role: "radio",
|
|
165
|
+
"aria-checked": accent === id,
|
|
166
|
+
"aria-label": name(id),
|
|
167
|
+
title: name(id),
|
|
168
|
+
onClick: () => choose(id),
|
|
169
|
+
style: {
|
|
170
|
+
width: 18,
|
|
171
|
+
height: 18,
|
|
172
|
+
padding: 0,
|
|
173
|
+
borderRadius: "50%",
|
|
174
|
+
cursor: "pointer",
|
|
175
|
+
background: ACCENTS[id].accent,
|
|
176
|
+
border: "2px solid var(--dsw-alias-bg-base, #fff)",
|
|
177
|
+
boxShadow: accent === id ? "0 0 0 2px var(--dsw-alias-label-primary, #1F1E1D)" : "0 0 0 1px var(--dsw-alias-border-l2, rgba(127,127,127,.35))"
|
|
178
|
+
}
|
|
179
|
+
}))), h("span", { style: {
|
|
180
|
+
fontSize: 12,
|
|
181
|
+
color: "var(--dsw-alias-label-tertiary, #888)"
|
|
182
|
+
} }, `${name(accent)} · ${hint}`));
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region lib/types/client/nav-icon.js
|
|
186
|
+
/**
|
|
187
|
+
* Give the "Office" row in the DSH settings navigation a document icon
|
|
188
|
+
* instead of the shell's fallback gear.
|
|
189
|
+
*
|
|
190
|
+
* `settings.section` registrations only carry `id`, `order` and `label`; the
|
|
191
|
+
* shell picks icons for built-in ids only. So the row is marked by its label
|
|
192
|
+
* and CSS swaps the glyph (Lucide file-text, drawn as a currentColor mask so
|
|
193
|
+
* hover / active colors still apply).
|
|
194
|
+
*/
|
|
195
|
+
const MARKER = "data-dsh-office-settings-nav";
|
|
196
|
+
const ICON_SVG = "%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M15 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7Z'/%3E%3Cpath d='M14 2v4a2 2 0 0 0 2 2h4'/%3E%3Cpath d='M10 9H8'/%3E%3Cpath d='M16 13H8'/%3E%3Cpath d='M16 17H8'/%3E%3C/svg%3E";
|
|
197
|
+
const navIconCss = `
|
|
198
|
+
[${MARKER}] > svg:first-child { display: none; }
|
|
199
|
+
[${MARKER}]::before {
|
|
200
|
+
content: '';
|
|
201
|
+
flex: none;
|
|
202
|
+
width: 16px;
|
|
203
|
+
height: 16px;
|
|
204
|
+
background: currentColor;
|
|
205
|
+
-webkit-mask: url("data:image/svg+xml,${ICON_SVG}") center / contain no-repeat;
|
|
206
|
+
mask: url("data:image/svg+xml,${ICON_SVG}") center / contain no-repeat;
|
|
207
|
+
}
|
|
208
|
+
`;
|
|
209
|
+
/** Keep the marker on the settings-nav button labelled `label`; returns a disposer. */
|
|
210
|
+
function registerNavIcon(label) {
|
|
211
|
+
const style = document.createElement("style");
|
|
212
|
+
style.setAttribute("data-dsh-office", "nav-icon");
|
|
213
|
+
style.textContent = navIconCss;
|
|
214
|
+
document.head.appendChild(style);
|
|
215
|
+
let disposed = false;
|
|
216
|
+
let frame = 0;
|
|
217
|
+
const sync = () => {
|
|
218
|
+
frame = 0;
|
|
219
|
+
if (disposed) return;
|
|
220
|
+
const dialogs = document.querySelectorAll("[role=\"dialog\"]");
|
|
221
|
+
if (dialogs.length === 0) return;
|
|
222
|
+
for (const dialog of dialogs) for (const button of dialog.querySelectorAll("nav button")) {
|
|
223
|
+
const mine = button.textContent?.trim() === label;
|
|
224
|
+
if (mine && !button.hasAttribute(MARKER)) button.setAttribute(MARKER, "");
|
|
225
|
+
else if (!mine && button.hasAttribute(MARKER)) button.removeAttribute(MARKER);
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
sync();
|
|
229
|
+
const observer = new MutationObserver(() => {
|
|
230
|
+
if (frame === 0) frame = requestAnimationFrame(sync);
|
|
231
|
+
});
|
|
232
|
+
observer.observe(document.body, {
|
|
233
|
+
childList: true,
|
|
234
|
+
subtree: true
|
|
235
|
+
});
|
|
236
|
+
return () => {
|
|
237
|
+
disposed = true;
|
|
238
|
+
if (frame !== 0) cancelAnimationFrame(frame);
|
|
239
|
+
observer.disconnect();
|
|
240
|
+
style.remove();
|
|
241
|
+
document.querySelectorAll(`[${MARKER}]`).forEach((element) => element.removeAttribute(MARKER));
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region lib/types/client/index.js
|
|
246
|
+
/**
|
|
247
|
+
* Browser half: the "Office" section in Settings. It reads the plugin's own
|
|
248
|
+
* Loader entry through `remote.settings.describe()` and writes with
|
|
249
|
+
* `remote.settings.mutate()`; every field is volatile and each change saves
|
|
250
|
+
* itself. The live status (installed apps, open documents) comes from the
|
|
251
|
+
* host route in ../index.ts.
|
|
252
|
+
*/
|
|
253
|
+
const STATUS_ROUTE = "/api/dsh-office/status";
|
|
254
|
+
const APP_NAMES = {
|
|
255
|
+
word: "Word",
|
|
256
|
+
excel: "Excel",
|
|
257
|
+
ppt: "PowerPoint"
|
|
258
|
+
};
|
|
259
|
+
const WIDTHS = [
|
|
260
|
+
800,
|
|
261
|
+
1100,
|
|
262
|
+
1400,
|
|
263
|
+
1800
|
|
264
|
+
];
|
|
265
|
+
const h = react.createElement;
|
|
266
|
+
const S = {
|
|
267
|
+
page: {
|
|
268
|
+
display: "grid",
|
|
269
|
+
gap: 20,
|
|
270
|
+
maxWidth: 880,
|
|
271
|
+
paddingBottom: 32,
|
|
272
|
+
color: "var(--dsw-alias-label-primary, inherit)"
|
|
273
|
+
},
|
|
274
|
+
title: {
|
|
275
|
+
margin: 0,
|
|
276
|
+
fontSize: 20,
|
|
277
|
+
fontWeight: 600
|
|
278
|
+
},
|
|
279
|
+
subtitle: {
|
|
280
|
+
margin: "6px 0 0",
|
|
281
|
+
fontSize: 13,
|
|
282
|
+
lineHeight: 1.6,
|
|
283
|
+
color: "var(--dsw-alias-label-secondary, #666)"
|
|
284
|
+
},
|
|
285
|
+
card: {
|
|
286
|
+
display: "grid",
|
|
287
|
+
gap: 14,
|
|
288
|
+
padding: 16,
|
|
289
|
+
borderRadius: "var(--dsw-radius-lg, 12px)",
|
|
290
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.25))"
|
|
291
|
+
},
|
|
292
|
+
cardTitle: {
|
|
293
|
+
margin: 0,
|
|
294
|
+
fontSize: 15,
|
|
295
|
+
fontWeight: 600
|
|
296
|
+
},
|
|
297
|
+
hint: {
|
|
298
|
+
margin: 0,
|
|
299
|
+
fontSize: 12,
|
|
300
|
+
lineHeight: 1.55,
|
|
301
|
+
color: "var(--dsw-alias-label-tertiary, #888)"
|
|
302
|
+
},
|
|
303
|
+
toggleRow: {
|
|
304
|
+
display: "flex",
|
|
305
|
+
gap: 12,
|
|
306
|
+
alignItems: "flex-start",
|
|
307
|
+
justifyContent: "space-between"
|
|
308
|
+
},
|
|
309
|
+
toggleText: {
|
|
310
|
+
display: "grid",
|
|
311
|
+
gap: 4,
|
|
312
|
+
fontSize: 13,
|
|
313
|
+
fontWeight: 500
|
|
314
|
+
},
|
|
315
|
+
error: {
|
|
316
|
+
fontSize: 12,
|
|
317
|
+
color: "var(--dsw-alias-state-error-primary, #d33)"
|
|
318
|
+
},
|
|
319
|
+
ok: {
|
|
320
|
+
fontSize: 12,
|
|
321
|
+
color: "var(--dsw-alias-state-success-primary, #2a2)"
|
|
322
|
+
},
|
|
323
|
+
statusLine: {
|
|
324
|
+
display: "flex",
|
|
325
|
+
gap: 8,
|
|
326
|
+
alignItems: "baseline",
|
|
327
|
+
fontSize: 13
|
|
328
|
+
},
|
|
329
|
+
dot: {
|
|
330
|
+
width: 8,
|
|
331
|
+
height: 8,
|
|
332
|
+
borderRadius: 4,
|
|
333
|
+
flex: "0 0 auto",
|
|
334
|
+
alignSelf: "center"
|
|
335
|
+
},
|
|
336
|
+
chips: {
|
|
337
|
+
display: "flex",
|
|
338
|
+
gap: 8,
|
|
339
|
+
flexWrap: "wrap"
|
|
340
|
+
},
|
|
341
|
+
secondary: {
|
|
342
|
+
padding: "6px 12px",
|
|
343
|
+
fontSize: 12,
|
|
344
|
+
borderRadius: "var(--dsw-radius-md, 8px)",
|
|
345
|
+
border: "1px solid var(--dsw-alias-border-l2, rgba(127,127,127,.3))",
|
|
346
|
+
background: "transparent",
|
|
347
|
+
color: "inherit",
|
|
348
|
+
cursor: "pointer"
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
function Switch({ checked, disabled, label, onChange }) {
|
|
352
|
+
return h("button", {
|
|
353
|
+
type: "button",
|
|
354
|
+
role: "switch",
|
|
355
|
+
"aria-checked": checked,
|
|
356
|
+
"aria-label": label,
|
|
357
|
+
disabled,
|
|
358
|
+
onClick: () => onChange(!checked),
|
|
359
|
+
style: {
|
|
360
|
+
flex: "0 0 auto",
|
|
361
|
+
width: 40,
|
|
362
|
+
height: 22,
|
|
363
|
+
borderRadius: 11,
|
|
364
|
+
border: "none",
|
|
365
|
+
padding: 2,
|
|
366
|
+
cursor: disabled ? "default" : "pointer",
|
|
367
|
+
background: checked ? ACCENT : "var(--dsw-alias-border-l3, rgba(127,127,127,.35))",
|
|
368
|
+
opacity: disabled ? .5 : 1,
|
|
369
|
+
transition: "background .15s"
|
|
370
|
+
}
|
|
371
|
+
}, h("span", { style: {
|
|
372
|
+
display: "block",
|
|
373
|
+
width: 18,
|
|
374
|
+
height: 18,
|
|
375
|
+
borderRadius: 9,
|
|
376
|
+
background: checked ? ACCENT_INK : "#fff",
|
|
377
|
+
transform: checked ? "translateX(18px)" : "none",
|
|
378
|
+
transition: "transform .15s"
|
|
379
|
+
} }));
|
|
380
|
+
}
|
|
381
|
+
function OfficeSection({ ctx }) {
|
|
382
|
+
const [loaded, setLoaded] = react.useState(false);
|
|
383
|
+
const [writable, setWritable] = react.useState(false);
|
|
384
|
+
const [view, setView] = react.useState(void 0);
|
|
385
|
+
const [settings, setSettings] = react.useState(DEFAULTS);
|
|
386
|
+
const [message, setMessage] = react.useState(null);
|
|
387
|
+
const [status, setStatus] = react.useState(null);
|
|
388
|
+
const [checking, setChecking] = react.useState(false);
|
|
389
|
+
const load = react.useCallback(async () => {
|
|
390
|
+
try {
|
|
391
|
+
const result = await ctx.remote.settings.describe();
|
|
392
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
393
|
+
const found = result.value.namespaces.find((item) => item.ns === ENTRY_ID);
|
|
394
|
+
setWritable(result.value.writable);
|
|
395
|
+
setView(found);
|
|
396
|
+
setSettings(resolveConfig(found?.value));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
setMessage({
|
|
399
|
+
kind: "error",
|
|
400
|
+
text: `读取设置失败:${error.message}`
|
|
401
|
+
});
|
|
402
|
+
} finally {
|
|
403
|
+
setLoaded(true);
|
|
404
|
+
}
|
|
405
|
+
}, [ctx]);
|
|
406
|
+
const refreshStatus = react.useCallback(async () => {
|
|
407
|
+
setChecking(true);
|
|
408
|
+
try {
|
|
409
|
+
const response = await fetch(STATUS_ROUTE, { credentials: "same-origin" });
|
|
410
|
+
setStatus(await response.json());
|
|
411
|
+
} catch {
|
|
412
|
+
setStatus({
|
|
413
|
+
ok: false,
|
|
414
|
+
error: "无法连接到插件"
|
|
415
|
+
});
|
|
416
|
+
} finally {
|
|
417
|
+
setChecking(false);
|
|
418
|
+
}
|
|
419
|
+
}, []);
|
|
420
|
+
react.useEffect(() => {
|
|
421
|
+
load();
|
|
422
|
+
refreshStatus();
|
|
423
|
+
return ctx.remote.$on?.("settings/document-updated", (ns) => {
|
|
424
|
+
if (ns === "copylee-office") load();
|
|
425
|
+
}) ?? void 0;
|
|
426
|
+
}, [
|
|
427
|
+
ctx,
|
|
428
|
+
load,
|
|
429
|
+
refreshStatus
|
|
430
|
+
]);
|
|
431
|
+
const disabled = !writable || view === void 0;
|
|
432
|
+
const change = async (key, value) => {
|
|
433
|
+
if (view === void 0) return;
|
|
434
|
+
setSettings((previous) => ({
|
|
435
|
+
...previous,
|
|
436
|
+
[key]: value
|
|
437
|
+
}));
|
|
438
|
+
try {
|
|
439
|
+
const result = await ctx.remote.settings.mutate(ENTRY_ID, [{
|
|
440
|
+
op: "set",
|
|
441
|
+
path: [key],
|
|
442
|
+
value
|
|
443
|
+
}], view.revision);
|
|
444
|
+
if (!result.ok) throw new Error(result.error.message);
|
|
445
|
+
setMessage({
|
|
446
|
+
kind: "ok",
|
|
447
|
+
text: "已保存,下一次调用即生效"
|
|
448
|
+
});
|
|
449
|
+
} catch (error) {
|
|
450
|
+
setMessage({
|
|
451
|
+
kind: "error",
|
|
452
|
+
text: `保存失败:${error.message}`
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
await load();
|
|
456
|
+
};
|
|
457
|
+
if (!loaded) return h("div", { style: S.page }, "加载中…");
|
|
458
|
+
return h("div", { style: S.page }, h("div", null, h("h2", { style: S.title }, "Office"), h("p", { style: S.subtitle }, "AI 直接在你电脑上打开的 Word、Excel、PowerPoint 里改文档:改动实时出现在窗口中,你可以同时编辑;窗口不必在前台。只有调用保存时才写入文件。")), view === void 0 ? h("div", { style: S.error }, "没有找到本插件的设置项(插件可能未启用)。") : null, view !== void 0 && !writable ? h("div", { style: S.error }, "当前设置为只读。") : null, h("section", { style: S.card }, h("div", { style: {
|
|
459
|
+
display: "flex",
|
|
460
|
+
justifyContent: "space-between",
|
|
461
|
+
alignItems: "center"
|
|
462
|
+
} }, h("h3", { style: S.cardTitle }, "应用状态"), h("button", {
|
|
463
|
+
type: "button",
|
|
464
|
+
style: S.secondary,
|
|
465
|
+
disabled: checking,
|
|
466
|
+
onClick: () => {
|
|
467
|
+
refreshStatus();
|
|
468
|
+
}
|
|
469
|
+
}, checking ? "检查中…" : "重新检查")), status === null ? h("p", { style: S.hint }, "检查中…") : !status.ok ? h("div", { style: S.error }, `控制组件不可用:${status.error ?? ""}`) : (status.apps ?? []).map((app) => h("div", {
|
|
470
|
+
key: app.app,
|
|
471
|
+
style: S.statusLine
|
|
472
|
+
}, h("span", { style: {
|
|
473
|
+
...S.dot,
|
|
474
|
+
background: !app.installed ? "#999" : app.running ? "#2a2" : ACCENT
|
|
475
|
+
} }), h("span", { style: {
|
|
476
|
+
minWidth: 86,
|
|
477
|
+
fontWeight: 500
|
|
478
|
+
} }, APP_NAMES[app.app]), h("span", { style: {
|
|
479
|
+
...S.hint,
|
|
480
|
+
fontSize: 13
|
|
481
|
+
} }, !app.installed ? "未安装" : !app.running ? "已安装,未运行(需要时自动启动)" : app.documents.length === 0 ? "运行中,没有打开的文档" : `运行中:${app.documents.map((doc) => doc.name + (doc.saved ? "" : "(未保存)")).join("、")}`))), h("p", { style: S.hint }, "需要本机装有微软 Office。Word 里 AI 的一批修改可以用一次 Ctrl+Z 撤回;Excel 和 PowerPoint 的修改无法用 Ctrl+Z 撤回。你正在单元格里输入或开着对话框时,AI 会等你。")), h("section", { style: S.card }, h("h3", { style: S.cardTitle }, "行为"), h("div", { style: S.toggleRow }, h("div", { style: S.toggleText }, "打开文档时显示到前台", h("span", { style: {
|
|
482
|
+
...S.hint,
|
|
483
|
+
fontWeight: 400
|
|
484
|
+
} }, "关闭后文档仍会打开,只是不抢你当前的窗口。")), h(Switch, {
|
|
485
|
+
checked: settings.showOnOpen,
|
|
486
|
+
disabled,
|
|
487
|
+
label: "打开文档时显示到前台",
|
|
488
|
+
onChange: (value) => {
|
|
489
|
+
change("showOnOpen", value);
|
|
490
|
+
}
|
|
491
|
+
})), h("div", { style: {
|
|
492
|
+
display: "grid",
|
|
493
|
+
gap: 8
|
|
494
|
+
} }, h("div", { style: S.toggleText }, "核对排版的图片宽度", h("span", { style: {
|
|
495
|
+
...S.hint,
|
|
496
|
+
fontWeight: 400
|
|
497
|
+
} }, "AI 改完后会把页面 / 幻灯片渲染成图片检查排版。越大越清晰,也越费 token。")), h("div", { style: S.chips }, WIDTHS.map((width) => h("button", {
|
|
498
|
+
key: width,
|
|
499
|
+
type: "button",
|
|
500
|
+
disabled,
|
|
501
|
+
onClick: () => {
|
|
502
|
+
change("renderWidth", width);
|
|
503
|
+
},
|
|
504
|
+
style: {
|
|
505
|
+
...S.secondary,
|
|
506
|
+
...settings.renderWidth === width ? {
|
|
507
|
+
background: ACCENT,
|
|
508
|
+
borderColor: ACCENT,
|
|
509
|
+
color: ACCENT_INK
|
|
510
|
+
} : {}
|
|
511
|
+
}
|
|
512
|
+
}, `${width} 像素${width === DEFAULTS.renderWidth ? "(默认)" : ""}`)))), message === null ? null : h("span", { style: message.kind === "ok" ? S.ok : S.error }, message.text)), h(AccentPicker, null));
|
|
513
|
+
}
|
|
514
|
+
const inject = [
|
|
515
|
+
"slots",
|
|
516
|
+
"remote",
|
|
517
|
+
"remote.settings"
|
|
518
|
+
];
|
|
519
|
+
function apply(ctx) {
|
|
520
|
+
ctx.effect(() => installAccent(), "office: accent colour");
|
|
521
|
+
ctx.effect(() => registerNavIcon("Office"), "office: settings nav icon");
|
|
522
|
+
ctx.slots.inject("settings.section", () => ctx.slots.register({
|
|
523
|
+
name: "settings.section",
|
|
524
|
+
id: ENTRY_ID,
|
|
525
|
+
order: 62,
|
|
526
|
+
label: () => "Office"
|
|
527
|
+
}, () => h(OfficeSection, { ctx })));
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
exports.apply = apply;
|
|
531
|
+
exports.inject = inject;
|
|
532
|
+
return module.exports;
|
|
533
|
+
}
|
|
534
|
+
});
|