@dfy-plugins/dsh-wallpaper 0.1.2
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 +45 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +1245 -0
- package/lib/index.d.ts +5 -0
- package/lib/index.js +331 -0
- package/lib/logic.d.ts +41 -0
- package/lib/logic.js +145 -0
- package/lib/regions.d.ts +6 -0
- package/lib/regions.js +71 -0
- package/package.json +69 -0
package/lib/index.d.ts
ADDED
package/lib/index.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { writeFileAtomic } from '@deepseek-ai/dsh-atomic-write';
|
|
2
|
+
import { dshHomePath } from '@deepseek-ai/dsh-home-paths';
|
|
3
|
+
import { randomUUID } from 'node:crypto';
|
|
4
|
+
import { createReadStream } from 'node:fs';
|
|
5
|
+
import { mkdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
6
|
+
import { dirname, join } from 'node:path';
|
|
7
|
+
import { DEFAULT_SETTINGS, normalizeSettings, wallpaperTarget } from './logic.js';
|
|
8
|
+
export const name = 'wallpaper';
|
|
9
|
+
export const inject = ['webServer'];
|
|
10
|
+
const DATA_DIR = dshHomePath('storages', 'dfy-plugins', 'wallpaper');
|
|
11
|
+
const LEGACY_DATA_DIR = dshHomePath('storages', 'xiao443', 'dsh-wallpaper');
|
|
12
|
+
const CONFIG_FILE = join(DATA_DIR, 'config.json');
|
|
13
|
+
const IMAGE_FILE = join(DATA_DIR, 'assets', 'current');
|
|
14
|
+
const MAX_JSON_BYTES = 64 * 1024;
|
|
15
|
+
const MAX_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
16
|
+
const DEFAULT_CONFIG = {
|
|
17
|
+
settings: { ...DEFAULT_SETTINGS },
|
|
18
|
+
imageMime: null,
|
|
19
|
+
imageVersion: 0,
|
|
20
|
+
regionImages: { settings: { imageMime: null, imageVersion: 0 }, sidebar: { imageMime: null, imageVersion: 0 } },
|
|
21
|
+
};
|
|
22
|
+
function imageFile(target) {
|
|
23
|
+
return target === 'global' ? IMAGE_FILE : join(DATA_DIR, 'assets', target, 'current');
|
|
24
|
+
}
|
|
25
|
+
function storedImage(value) {
|
|
26
|
+
const item = typeof value === 'object' && value !== null ? value : {};
|
|
27
|
+
return {
|
|
28
|
+
imageMime: typeof item.imageMime === 'string' && /^image\/[a-z0-9.+-]+$/i.test(item.imageMime) ? item.imageMime : null,
|
|
29
|
+
imageVersion: typeof item.imageVersion === 'number' && Number.isFinite(item.imageVersion) ? Math.max(0, Math.floor(item.imageVersion)) : 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function withImage(config, target, image, imageName) {
|
|
33
|
+
if (target === 'global')
|
|
34
|
+
return {
|
|
35
|
+
...config, ...image,
|
|
36
|
+
settings: normalizeSettings({ ...config.settings, enabled: imageName !== null, imageName }),
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
...config,
|
|
40
|
+
regionImages: { ...config.regionImages, [target]: image },
|
|
41
|
+
settings: normalizeSettings({ ...config.settings, regions: {
|
|
42
|
+
...config.settings.regions,
|
|
43
|
+
[target]: { ...config.settings.regions[target], imageName, source: imageName === null ? 'none' : 'custom' },
|
|
44
|
+
} }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
let storageReady;
|
|
48
|
+
async function migrateLegacyStorage() {
|
|
49
|
+
try {
|
|
50
|
+
await stat(DATA_DIR);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
if (errorCode(error) !== 'ENOENT')
|
|
55
|
+
throw error;
|
|
56
|
+
}
|
|
57
|
+
await mkdir(dirname(DATA_DIR), { recursive: true, mode: 0o700 });
|
|
58
|
+
try {
|
|
59
|
+
await rename(LEGACY_DATA_DIR, DATA_DIR);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
const code = errorCode(error);
|
|
63
|
+
if (code !== 'ENOENT' && code !== 'EEXIST')
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function ensureStorageReady() {
|
|
68
|
+
storageReady ??= migrateLegacyStorage();
|
|
69
|
+
return storageReady;
|
|
70
|
+
}
|
|
71
|
+
function errorCode(error) {
|
|
72
|
+
return typeof error === 'object' && error !== null && 'code' in error
|
|
73
|
+
? String(error.code)
|
|
74
|
+
: undefined;
|
|
75
|
+
}
|
|
76
|
+
async function readConfig() {
|
|
77
|
+
await ensureStorageReady();
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(await readFile(CONFIG_FILE, 'utf8'));
|
|
80
|
+
return {
|
|
81
|
+
settings: normalizeSettings(parsed.settings),
|
|
82
|
+
...storedImage(parsed),
|
|
83
|
+
regionImages: {
|
|
84
|
+
settings: storedImage(parsed.regionImages?.settings),
|
|
85
|
+
sidebar: storedImage(parsed.regionImages?.sidebar),
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
if (errorCode(error) === 'ENOENT')
|
|
91
|
+
return { ...DEFAULT_CONFIG, settings: { ...DEFAULT_SETTINGS } };
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
async function writeConfig(config) {
|
|
96
|
+
await ensureStorageReady();
|
|
97
|
+
await writeFileAtomic(CONFIG_FILE, `${JSON.stringify(config, null, 2)}\n`, {
|
|
98
|
+
mode: 0o600,
|
|
99
|
+
dirMode: 0o700,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
async function imageExists(target) {
|
|
103
|
+
await ensureStorageReady();
|
|
104
|
+
try {
|
|
105
|
+
return (await stat(imageFile(target))).isFile();
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (errorCode(error) === 'ENOENT')
|
|
109
|
+
return false;
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
async function toClientState(config) {
|
|
114
|
+
const state = async (target) => {
|
|
115
|
+
const image = target === 'global' ? config : config.regionImages[target];
|
|
116
|
+
const settings = target === 'global' ? config.settings : config.settings.regions[target];
|
|
117
|
+
const hasImage = settings.imageName !== null && image.imageMime !== null && await imageExists(target);
|
|
118
|
+
return { hasImage, imageUrl: hasImage ? `/api/dsh-wallpaper/image?region=${target}&v=${image.imageVersion}` : null };
|
|
119
|
+
};
|
|
120
|
+
const [global, settingsImage, sidebarImage] = await Promise.all([state('global'), state('settings'), state('sidebar')]);
|
|
121
|
+
const { hasImage } = global;
|
|
122
|
+
const settings = hasImage
|
|
123
|
+
? config.settings
|
|
124
|
+
: normalizeSettings({ ...config.settings, enabled: false, imageName: null });
|
|
125
|
+
return {
|
|
126
|
+
settings,
|
|
127
|
+
...global,
|
|
128
|
+
regionImages: { settings: settingsImage, sidebar: sidebarImage },
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
function sendJson(res, status, body) {
|
|
132
|
+
const payload = JSON.stringify(body);
|
|
133
|
+
res.writeHead(status, {
|
|
134
|
+
'content-type': 'application/json; charset=utf-8',
|
|
135
|
+
'content-length': Buffer.byteLength(payload),
|
|
136
|
+
'cache-control': 'no-store',
|
|
137
|
+
});
|
|
138
|
+
res.end(payload);
|
|
139
|
+
}
|
|
140
|
+
function readBody(req, limit) {
|
|
141
|
+
return new Promise((resolve, reject) => {
|
|
142
|
+
const chunks = [];
|
|
143
|
+
let size = 0;
|
|
144
|
+
let settled = false;
|
|
145
|
+
req.on('data', (chunk) => {
|
|
146
|
+
if (settled)
|
|
147
|
+
return;
|
|
148
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
149
|
+
size += buffer.length;
|
|
150
|
+
if (size > limit) {
|
|
151
|
+
settled = true;
|
|
152
|
+
chunks.length = 0;
|
|
153
|
+
reject(new Error('payload too large'));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
chunks.push(buffer);
|
|
157
|
+
});
|
|
158
|
+
req.on('end', () => {
|
|
159
|
+
if (!settled)
|
|
160
|
+
resolve(Buffer.concat(chunks));
|
|
161
|
+
});
|
|
162
|
+
req.on('error', (error) => {
|
|
163
|
+
if (!settled)
|
|
164
|
+
reject(error);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function decodeFileName(value) {
|
|
169
|
+
const raw = Array.isArray(value) ? value[0] : value;
|
|
170
|
+
if (raw === undefined)
|
|
171
|
+
return 'wallpaper';
|
|
172
|
+
try {
|
|
173
|
+
const decoded = decodeURIComponent(raw).trim();
|
|
174
|
+
return decoded.length > 0 ? decoded.slice(0, 260) : 'wallpaper';
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
return 'wallpaper';
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function writeImageAtomic(content, target) {
|
|
181
|
+
await ensureStorageReady();
|
|
182
|
+
const file = imageFile(target);
|
|
183
|
+
await mkdir(dirname(file), { recursive: true, mode: 0o700 });
|
|
184
|
+
const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
|
|
185
|
+
try {
|
|
186
|
+
await writeFile(temporary, content, { flag: 'wx', mode: 0o600 });
|
|
187
|
+
await rename(temporary, file);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
await rm(temporary, { force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
export function apply(ctx) {
|
|
194
|
+
let mutationTail = Promise.resolve();
|
|
195
|
+
const mutate = (operation) => {
|
|
196
|
+
const result = mutationTail.then(operation, operation);
|
|
197
|
+
mutationTail = result.then(() => undefined, () => undefined);
|
|
198
|
+
return result;
|
|
199
|
+
};
|
|
200
|
+
const stateRoute = {
|
|
201
|
+
kind: 'exact',
|
|
202
|
+
path: '/api/dsh-wallpaper/state',
|
|
203
|
+
async handler(req, res) {
|
|
204
|
+
if (req.method !== 'GET')
|
|
205
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
206
|
+
try {
|
|
207
|
+
sendJson(res, 200, await toClientState(await readConfig()));
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
sendJson(res, 500, { error: String(error) });
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
const settingsRoute = {
|
|
215
|
+
kind: 'exact',
|
|
216
|
+
path: '/api/dsh-wallpaper/settings',
|
|
217
|
+
async handler(req, res) {
|
|
218
|
+
if (req.method !== 'PUT')
|
|
219
|
+
return sendJson(res, 405, { error: 'method not allowed' });
|
|
220
|
+
try {
|
|
221
|
+
const body = JSON.parse((await readBody(req, MAX_JSON_BYTES)).toString('utf8'));
|
|
222
|
+
const state = await mutate(async () => {
|
|
223
|
+
const current = await readConfig();
|
|
224
|
+
const requested = normalizeSettings(body);
|
|
225
|
+
const next = {
|
|
226
|
+
...current,
|
|
227
|
+
settings: { ...requested, imageName: current.settings.imageName, regions: {
|
|
228
|
+
settings: { ...requested.regions.settings, imageName: current.settings.regions.settings.imageName },
|
|
229
|
+
sidebar: { ...requested.regions.sidebar, imageName: current.settings.regions.sidebar.imageName },
|
|
230
|
+
} },
|
|
231
|
+
};
|
|
232
|
+
await writeConfig(next);
|
|
233
|
+
return toClientState(next);
|
|
234
|
+
});
|
|
235
|
+
sendJson(res, 200, await state);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
sendJson(res, 400, { error: String(error) });
|
|
239
|
+
}
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
const imageRoute = {
|
|
243
|
+
kind: 'exact',
|
|
244
|
+
path: '/api/dsh-wallpaper/image',
|
|
245
|
+
async handler(req, res) {
|
|
246
|
+
const target = wallpaperTarget(new URL(req.url ?? '/api/dsh-wallpaper/image', 'http://localhost').searchParams.get('region'));
|
|
247
|
+
if (target === undefined)
|
|
248
|
+
return sendJson(res, 400, { error: 'invalid wallpaper region' });
|
|
249
|
+
if (req.method === 'GET') {
|
|
250
|
+
try {
|
|
251
|
+
const config = await readConfig();
|
|
252
|
+
const image = target === 'global' ? config : config.regionImages[target];
|
|
253
|
+
if (image.imageMime === null || !(await imageExists(target))) {
|
|
254
|
+
sendJson(res, 404, { error: 'wallpaper not found' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const info = await stat(imageFile(target));
|
|
258
|
+
res.writeHead(200, {
|
|
259
|
+
'content-type': image.imageMime,
|
|
260
|
+
'content-length': info.size,
|
|
261
|
+
'cache-control': 'private, max-age=31536000, immutable',
|
|
262
|
+
});
|
|
263
|
+
const stream = createReadStream(imageFile(target));
|
|
264
|
+
stream.on('error', (error) => res.destroy(error));
|
|
265
|
+
stream.pipe(res);
|
|
266
|
+
}
|
|
267
|
+
catch (error) {
|
|
268
|
+
if (!res.headersSent)
|
|
269
|
+
sendJson(res, 500, { error: String(error) });
|
|
270
|
+
}
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
if (req.method === 'PUT') {
|
|
274
|
+
try {
|
|
275
|
+
const mime = String(req.headers['content-type'] ?? '').split(';', 1)[0].trim().toLowerCase();
|
|
276
|
+
if (!/^image\/[a-z0-9.+-]+$/.test(mime)) {
|
|
277
|
+
sendJson(res, 415, { error: 'content-type must be image/*' });
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const content = await readBody(req, MAX_IMAGE_BYTES);
|
|
281
|
+
if (content.length === 0) {
|
|
282
|
+
sendJson(res, 400, { error: 'empty image' });
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
const state = await mutate(async () => {
|
|
286
|
+
await writeImageAtomic(content, target);
|
|
287
|
+
const current = await readConfig();
|
|
288
|
+
const previous = target === 'global' ? current : current.regionImages[target];
|
|
289
|
+
const next = withImage(current, target, {
|
|
290
|
+
imageMime: mime,
|
|
291
|
+
imageVersion: Math.max(Date.now(), previous.imageVersion + 1),
|
|
292
|
+
}, decodeFileName(req.headers['x-dsh-wallpaper-filename']));
|
|
293
|
+
await writeConfig(next);
|
|
294
|
+
return toClientState(next);
|
|
295
|
+
});
|
|
296
|
+
sendJson(res, 200, await state);
|
|
297
|
+
}
|
|
298
|
+
catch (error) {
|
|
299
|
+
sendJson(res, error instanceof Error && error.message === 'payload too large' ? 413 : 500, {
|
|
300
|
+
error: String(error),
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (req.method === 'DELETE') {
|
|
306
|
+
try {
|
|
307
|
+
const state = await mutate(async () => {
|
|
308
|
+
await rm(imageFile(target), { force: true });
|
|
309
|
+
const current = await readConfig();
|
|
310
|
+
const previous = target === 'global' ? current : current.regionImages[target];
|
|
311
|
+
const next = withImage(current, target, {
|
|
312
|
+
imageMime: null,
|
|
313
|
+
imageVersion: Math.max(Date.now(), previous.imageVersion + 1),
|
|
314
|
+
}, null);
|
|
315
|
+
await writeConfig(next);
|
|
316
|
+
return toClientState(next);
|
|
317
|
+
});
|
|
318
|
+
sendJson(res, 200, await state);
|
|
319
|
+
}
|
|
320
|
+
catch (error) {
|
|
321
|
+
sendJson(res, 500, { error: String(error) });
|
|
322
|
+
}
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
sendJson(res, 405, { error: 'method not allowed' });
|
|
326
|
+
},
|
|
327
|
+
};
|
|
328
|
+
ctx.webServer.register(stateRoute);
|
|
329
|
+
ctx.webServer.register(settingsRoute);
|
|
330
|
+
ctx.webServer.register(imageRoute);
|
|
331
|
+
}
|
package/lib/logic.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export declare const WALLPAPER_MODES: readonly ["cover", "contain", "stretch", "fit-width", "fit-height", "center", "tile"];
|
|
2
|
+
export type WallpaperMode = (typeof WALLPAPER_MODES)[number];
|
|
3
|
+
export declare const WALLPAPER_POSITIONS: readonly ["left top", "center top", "right top", "left center", "center center", "right center", "left bottom", "center bottom", "right bottom"];
|
|
4
|
+
export type WallpaperPosition = (typeof WALLPAPER_POSITIONS)[number];
|
|
5
|
+
export declare const WALLPAPER_REGIONS: readonly ["settings", "sidebar"];
|
|
6
|
+
export type WallpaperRegion = (typeof WALLPAPER_REGIONS)[number];
|
|
7
|
+
export type WallpaperTarget = 'global' | WallpaperRegion;
|
|
8
|
+
export type WallpaperSource = 'none' | 'global' | 'custom';
|
|
9
|
+
export interface WallpaperSurfaceSettings {
|
|
10
|
+
enabled: boolean;
|
|
11
|
+
imageName: string | null;
|
|
12
|
+
mode: WallpaperMode;
|
|
13
|
+
position: WallpaperPosition;
|
|
14
|
+
offsetXPercent: number;
|
|
15
|
+
offsetYPercent: number;
|
|
16
|
+
imageOpacity: number;
|
|
17
|
+
blur: number;
|
|
18
|
+
maskColor: string;
|
|
19
|
+
maskOpacity: number;
|
|
20
|
+
surfaceOpacity: number;
|
|
21
|
+
}
|
|
22
|
+
export interface WallpaperRegionSettings extends Omit<WallpaperSurfaceSettings, 'enabled'> {
|
|
23
|
+
source: WallpaperSource;
|
|
24
|
+
}
|
|
25
|
+
export interface WallpaperSettings extends WallpaperSurfaceSettings {
|
|
26
|
+
regions: Record<WallpaperRegion, WallpaperRegionSettings>;
|
|
27
|
+
}
|
|
28
|
+
export declare function defaultRegionSettings(): WallpaperRegionSettings;
|
|
29
|
+
export declare const DEFAULT_SETTINGS: WallpaperSettings;
|
|
30
|
+
export interface WallpaperModeStyle {
|
|
31
|
+
size: string;
|
|
32
|
+
repeat: 'no-repeat' | 'repeat';
|
|
33
|
+
}
|
|
34
|
+
export declare function normalizeSettings(value: unknown): WallpaperSettings;
|
|
35
|
+
/** Only these named image stores can be selected by an HTTP request. */
|
|
36
|
+
export declare function wallpaperTarget(value: string | null): WallpaperTarget | undefined;
|
|
37
|
+
/** 把九宫格锚点与相对视口的百分比微调组合成 background-position。 */
|
|
38
|
+
export declare function backgroundPositionWithOffset(position: WallpaperPosition, offsetXPercent: number, offsetYPercent: number): string;
|
|
39
|
+
export declare function modeStyle(mode: WallpaperMode): WallpaperModeStyle;
|
|
40
|
+
export declare function hexToRgb(value: string): [number, number, number];
|
|
41
|
+
export declare function surfaceLayerAlphas(base: number, maxOpacity?: number): [number, number, number];
|
package/lib/logic.js
ADDED
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
export const WALLPAPER_MODES = [
|
|
2
|
+
'cover',
|
|
3
|
+
'contain',
|
|
4
|
+
'stretch',
|
|
5
|
+
'fit-width',
|
|
6
|
+
'fit-height',
|
|
7
|
+
'center',
|
|
8
|
+
'tile',
|
|
9
|
+
];
|
|
10
|
+
export const WALLPAPER_POSITIONS = [
|
|
11
|
+
'left top',
|
|
12
|
+
'center top',
|
|
13
|
+
'right top',
|
|
14
|
+
'left center',
|
|
15
|
+
'center center',
|
|
16
|
+
'right center',
|
|
17
|
+
'left bottom',
|
|
18
|
+
'center bottom',
|
|
19
|
+
'right bottom',
|
|
20
|
+
];
|
|
21
|
+
export const WALLPAPER_REGIONS = ['settings', 'sidebar'];
|
|
22
|
+
const DEFAULT_SURFACE_SETTINGS = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
imageName: null,
|
|
25
|
+
mode: 'cover',
|
|
26
|
+
position: 'center center',
|
|
27
|
+
offsetXPercent: 0,
|
|
28
|
+
offsetYPercent: 0,
|
|
29
|
+
imageOpacity: 1,
|
|
30
|
+
blur: 0,
|
|
31
|
+
maskColor: '#000000',
|
|
32
|
+
maskOpacity: 0.18,
|
|
33
|
+
surfaceOpacity: 0.56,
|
|
34
|
+
};
|
|
35
|
+
export function defaultRegionSettings() {
|
|
36
|
+
const { enabled: _enabled, ...appearance } = DEFAULT_SURFACE_SETTINGS;
|
|
37
|
+
return { ...appearance, source: 'none', imageOpacity: 0.45, blur: 8, maskOpacity: 0, surfaceOpacity: 0.95 };
|
|
38
|
+
}
|
|
39
|
+
export const DEFAULT_SETTINGS = {
|
|
40
|
+
...DEFAULT_SURFACE_SETTINGS,
|
|
41
|
+
regions: { settings: defaultRegionSettings(), sidebar: defaultRegionSettings() },
|
|
42
|
+
};
|
|
43
|
+
function isRecord(value) {
|
|
44
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
function clamp(value, fallback, min, max) {
|
|
47
|
+
const parsed = typeof value === 'number' ? value : Number.NaN;
|
|
48
|
+
if (!Number.isFinite(parsed))
|
|
49
|
+
return fallback;
|
|
50
|
+
return Math.min(max, Math.max(min, parsed));
|
|
51
|
+
}
|
|
52
|
+
function isMode(value) {
|
|
53
|
+
return typeof value === 'string' && WALLPAPER_MODES.includes(value);
|
|
54
|
+
}
|
|
55
|
+
function isPosition(value) {
|
|
56
|
+
return typeof value === 'string' && WALLPAPER_POSITIONS.includes(value);
|
|
57
|
+
}
|
|
58
|
+
function normalizeHexColor(value) {
|
|
59
|
+
if (typeof value !== 'string')
|
|
60
|
+
return DEFAULT_SETTINGS.maskColor;
|
|
61
|
+
const trimmed = value.trim();
|
|
62
|
+
if (/^#[0-9a-f]{6}$/i.test(trimmed))
|
|
63
|
+
return trimmed.toLowerCase();
|
|
64
|
+
if (/^#[0-9a-f]{3}$/i.test(trimmed)) {
|
|
65
|
+
const [r, g, b] = trimmed.slice(1).split('');
|
|
66
|
+
return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
|
|
67
|
+
}
|
|
68
|
+
return DEFAULT_SETTINGS.maskColor;
|
|
69
|
+
}
|
|
70
|
+
function normalizeSurface(value, maxSurfaceOpacity = 0.95) {
|
|
71
|
+
const input = isRecord(value) ? value : {};
|
|
72
|
+
const rawName = typeof input.imageName === 'string' ? input.imageName.trim() : '';
|
|
73
|
+
return {
|
|
74
|
+
enabled: typeof input.enabled === 'boolean' ? input.enabled : DEFAULT_SETTINGS.enabled,
|
|
75
|
+
imageName: rawName.length > 0 ? rawName.slice(0, 260) : null,
|
|
76
|
+
mode: isMode(input.mode) ? input.mode : DEFAULT_SETTINGS.mode,
|
|
77
|
+
position: isPosition(input.position) ? input.position : DEFAULT_SETTINGS.position,
|
|
78
|
+
offsetXPercent: clamp(input.offsetXPercent, DEFAULT_SETTINGS.offsetXPercent, -100, 100),
|
|
79
|
+
offsetYPercent: clamp(input.offsetYPercent, DEFAULT_SETTINGS.offsetYPercent, -100, 100),
|
|
80
|
+
imageOpacity: clamp(input.imageOpacity, DEFAULT_SETTINGS.imageOpacity, 0, 1),
|
|
81
|
+
blur: clamp(input.blur, DEFAULT_SETTINGS.blur, 0, 40),
|
|
82
|
+
maskColor: normalizeHexColor(input.maskColor),
|
|
83
|
+
maskOpacity: clamp(input.maskOpacity, DEFAULT_SETTINGS.maskOpacity, 0, 0.9),
|
|
84
|
+
surfaceOpacity: clamp(input.surfaceOpacity, DEFAULT_SETTINGS.surfaceOpacity, 0, maxSurfaceOpacity),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
export function normalizeSettings(value) {
|
|
88
|
+
const input = isRecord(value) ? value : {};
|
|
89
|
+
const regions = isRecord(input.regions) ? input.regions : {};
|
|
90
|
+
const region = (key) => {
|
|
91
|
+
const value = isRecord(regions[key]) ? regions[key] : {};
|
|
92
|
+
const { enabled: _enabled, ...appearance } = normalizeSurface({ ...defaultRegionSettings(), ...value }, 1);
|
|
93
|
+
return { ...appearance, source: value.source === 'global' || value.source === 'custom' ? value.source : 'none' };
|
|
94
|
+
};
|
|
95
|
+
return { ...normalizeSurface(input), regions: { settings: region('settings'), sidebar: region('sidebar') } };
|
|
96
|
+
}
|
|
97
|
+
/** Only these named image stores can be selected by an HTTP request. */
|
|
98
|
+
export function wallpaperTarget(value) {
|
|
99
|
+
return value === null || value === 'global' ? 'global'
|
|
100
|
+
: value === 'settings' || value === 'sidebar' ? value : undefined;
|
|
101
|
+
}
|
|
102
|
+
function offsetAxis(anchor, offset, viewportUnit) {
|
|
103
|
+
if (offset === 0)
|
|
104
|
+
return anchor;
|
|
105
|
+
const operator = offset < 0 ? '-' : '+';
|
|
106
|
+
return `calc(${anchor} ${operator} ${Math.abs(offset)}${viewportUnit})`;
|
|
107
|
+
}
|
|
108
|
+
/** 把九宫格锚点与相对视口的百分比微调组合成 background-position。 */
|
|
109
|
+
export function backgroundPositionWithOffset(position, offsetXPercent, offsetYPercent) {
|
|
110
|
+
const [horizontal, vertical] = position.split(' ');
|
|
111
|
+
const horizontalAnchor = { left: '0%', center: '50%', right: '100%' }[horizontal];
|
|
112
|
+
const verticalAnchor = { top: '0%', center: '50%', bottom: '100%' }[vertical];
|
|
113
|
+
return `${offsetAxis(horizontalAnchor, offsetXPercent, 'vw')} ${offsetAxis(verticalAnchor, offsetYPercent, 'vh')}`;
|
|
114
|
+
}
|
|
115
|
+
export function modeStyle(mode) {
|
|
116
|
+
switch (mode) {
|
|
117
|
+
case 'contain':
|
|
118
|
+
return { size: 'contain', repeat: 'no-repeat' };
|
|
119
|
+
case 'stretch':
|
|
120
|
+
return { size: '100% 100%', repeat: 'no-repeat' };
|
|
121
|
+
case 'fit-width':
|
|
122
|
+
return { size: '100% auto', repeat: 'no-repeat' };
|
|
123
|
+
case 'fit-height':
|
|
124
|
+
return { size: 'auto 100%', repeat: 'no-repeat' };
|
|
125
|
+
case 'center':
|
|
126
|
+
return { size: 'auto', repeat: 'no-repeat' };
|
|
127
|
+
case 'tile':
|
|
128
|
+
return { size: 'auto', repeat: 'repeat' };
|
|
129
|
+
case 'cover':
|
|
130
|
+
default:
|
|
131
|
+
return { size: 'cover', repeat: 'no-repeat' };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
export function hexToRgb(value) {
|
|
135
|
+
const color = normalizeHexColor(value).slice(1);
|
|
136
|
+
return [
|
|
137
|
+
Number.parseInt(color.slice(0, 2), 16),
|
|
138
|
+
Number.parseInt(color.slice(2, 4), 16),
|
|
139
|
+
Number.parseInt(color.slice(4, 6), 16),
|
|
140
|
+
];
|
|
141
|
+
}
|
|
142
|
+
export function surfaceLayerAlphas(base, maxOpacity = 0.95) {
|
|
143
|
+
const normalized = clamp(base, DEFAULT_SETTINGS.surfaceOpacity, 0, maxOpacity);
|
|
144
|
+
return [normalized, Math.max(normalized, Math.min(0.97, normalized + 0.1)), Math.max(normalized, Math.min(0.99, normalized + 0.2))];
|
|
145
|
+
}
|
package/lib/regions.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type WallpaperRegion, type WallpaperSettings } from './logic.js';
|
|
2
|
+
export declare const REGIONS_ATTRIBUTE = "data-dsh-wallpaper-regions";
|
|
3
|
+
export declare const REGION_SELECTORS: Record<WallpaperRegion, string>;
|
|
4
|
+
export declare const REGION_VARIABLES: string[];
|
|
5
|
+
export declare function regionVariables(settings: WallpaperSettings, globalUrl: string | null, images: Record<WallpaperRegion, string | null>): Record<string, string>;
|
|
6
|
+
export declare const REGION_STYLES: string;
|
package/lib/regions.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { backgroundPositionWithOffset, hexToRgb, modeStyle, surfaceLayerAlphas, WALLPAPER_REGIONS } from './logic.js';
|
|
2
|
+
export const REGIONS_ATTRIBUTE = 'data-dsh-wallpaper-regions';
|
|
3
|
+
export const REGION_SELECTORS = {
|
|
4
|
+
settings: '[role="dialog"][aria-modal="true"]:has([data-slot="settings.header"])',
|
|
5
|
+
sidebar: '[data-sidebar-right-panel]',
|
|
6
|
+
};
|
|
7
|
+
const PROPERTIES = ['image', 'size', 'repeat', 'position', 'opacity', 'blur', 'mask-rgb', 'mask-opacity', 'surface-1', 'surface-2', 'surface-3'];
|
|
8
|
+
export const REGION_VARIABLES = WALLPAPER_REGIONS.flatMap((region) => PROPERTIES.map((property) => `--dsh-wallpaper-${region}-${property}`));
|
|
9
|
+
export function regionVariables(settings, globalUrl, images) {
|
|
10
|
+
const variables = {};
|
|
11
|
+
for (const region of WALLPAPER_REGIONS) {
|
|
12
|
+
const value = settings.regions[region];
|
|
13
|
+
const url = value.source === 'custom' ? images[region] : value.source === 'global' ? globalUrl : null;
|
|
14
|
+
const style = modeStyle(value.mode);
|
|
15
|
+
const surfaces = surfaceLayerAlphas(value.surfaceOpacity, 1);
|
|
16
|
+
const values = [
|
|
17
|
+
url === null ? 'none' : `url(${JSON.stringify(url)})`, style.size, style.repeat,
|
|
18
|
+
backgroundPositionWithOffset(value.position, value.offsetXPercent, value.offsetYPercent),
|
|
19
|
+
String(value.imageOpacity), `${value.blur}px`, hexToRgb(value.maskColor).join(' '),
|
|
20
|
+
String(url === null ? 0 : value.maskOpacity), ...surfaces.map(String),
|
|
21
|
+
];
|
|
22
|
+
PROPERTIES.forEach((property, index) => { variables[`--dsh-wallpaper-${region}-${property}`] = values[index]; });
|
|
23
|
+
}
|
|
24
|
+
return variables;
|
|
25
|
+
}
|
|
26
|
+
export const REGION_STYLES = `
|
|
27
|
+
body[${REGIONS_ATTRIBUTE}] { --dsh-wallpaper-region-rgb: 255 255 255; }
|
|
28
|
+
body[${REGIONS_ATTRIBUTE}][data-ds-dark-theme] { --dsh-wallpaper-region-rgb: 18 22 32; }
|
|
29
|
+
${WALLPAPER_REGIONS.map((region) => {
|
|
30
|
+
const selector = `body[${REGIONS_ATTRIBUTE}] ${REGION_SELECTORS[region]}`;
|
|
31
|
+
const variable = (name) => `var(--dsh-wallpaper-${region}-${name})`;
|
|
32
|
+
return `
|
|
33
|
+
${selector} {
|
|
34
|
+
isolation: isolate;
|
|
35
|
+
background: rgb(var(--dsh-wallpaper-region-rgb) / ${variable('surface-1')});
|
|
36
|
+
--dsh-wallpaper-local-1: rgb(var(--dsh-wallpaper-region-rgb) / ${variable('surface-1')});
|
|
37
|
+
--dsh-wallpaper-local-2: rgb(var(--dsh-wallpaper-region-rgb) / ${variable('surface-2')});
|
|
38
|
+
--dsh-wallpaper-local-3: rgb(var(--dsh-wallpaper-region-rgb) / ${variable('surface-3')});
|
|
39
|
+
--dsw-alias-bg-base: transparent;
|
|
40
|
+
--dsw-specific-sidebar-fill: transparent;
|
|
41
|
+
--dsw-alias-bg-layer-1: var(--dsh-wallpaper-local-1);
|
|
42
|
+
--dsw-alias-bg-layer-2: var(--dsh-wallpaper-local-2);
|
|
43
|
+
--dsw-alias-bg-layer-3: var(--dsh-wallpaper-local-3);
|
|
44
|
+
--dsw-alias-bg-module-platform: var(--dsh-wallpaper-local-2);
|
|
45
|
+
--dsw-specific-input-major: var(--dsh-wallpaper-local-2);
|
|
46
|
+
--dsw-specific-selector: var(--dsh-wallpaper-local-2);
|
|
47
|
+
--dsw-specific-tip: var(--dsh-wallpaper-local-2);
|
|
48
|
+
--dsw-alias-button-elevated-fill: var(--dsh-wallpaper-local-2);
|
|
49
|
+
--dsw-alias-button-floating-fill: var(--dsh-wallpaper-local-3);
|
|
50
|
+
--dsw-alias-markdown-code-block: var(--dsh-wallpaper-local-2);
|
|
51
|
+
--dsw-alias-markdown-code-block-banner: var(--dsh-wallpaper-local-3);
|
|
52
|
+
--dsw-alias-markdown-inline-code: var(--dsh-wallpaper-local-2);
|
|
53
|
+
}
|
|
54
|
+
${selector}::before, ${selector}::after {
|
|
55
|
+
content: ''; position: absolute; inset: 0; pointer-events: none; border-radius: inherit;
|
|
56
|
+
}
|
|
57
|
+
${selector}::before {
|
|
58
|
+
z-index: -2;
|
|
59
|
+
background-image: ${variable('image')};
|
|
60
|
+
background-size: ${variable('size')};
|
|
61
|
+
background-repeat: ${variable('repeat')};
|
|
62
|
+
background-position: ${variable('position')};
|
|
63
|
+
opacity: ${variable('opacity')};
|
|
64
|
+
filter: blur(${variable('blur')});
|
|
65
|
+
clip-path: inset(0);
|
|
66
|
+
}
|
|
67
|
+
${selector}::after {
|
|
68
|
+
z-index: -1; background: rgb(${variable('mask-rgb')} / ${variable('mask-opacity')});
|
|
69
|
+
}`;
|
|
70
|
+
}).join('\n')}
|
|
71
|
+
`;
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dfy-plugins/dsh-wallpaper",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DeepSeek Harness 可配置图片壁纸:适应模式、位置、透明度、模糊与颜色遮罩。",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"types": "lib/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./lib/index.d.ts",
|
|
11
|
+
"default": "./lib/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./client": {
|
|
14
|
+
"default": "./lib/client.js"
|
|
15
|
+
},
|
|
16
|
+
"./package.json": "./package.json"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"lib",
|
|
20
|
+
"cordis.patch.yml"
|
|
21
|
+
],
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public",
|
|
24
|
+
"registry": "https://registry.npmjs.org/"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
29
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
|
|
30
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1",
|
|
31
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2 || ^0.1.2-alpha.1 || ^0.1.5-alpha.1"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
35
|
+
"@deepseek-ai/dsh-atomic-write": "^0.1.1-rc.2",
|
|
36
|
+
"@deepseek-ai/dsh-home-paths": "^0.1.1-rc.2",
|
|
37
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.1-rc.2",
|
|
38
|
+
"@types/node": "^24.0.0",
|
|
39
|
+
"@types/react": "^19.0.0",
|
|
40
|
+
"esbuild": "^0.24.0",
|
|
41
|
+
"typescript": "^5.5.0"
|
|
42
|
+
},
|
|
43
|
+
"dsh": {
|
|
44
|
+
"bundle": {
|
|
45
|
+
"patch": "./cordis.patch.yml"
|
|
46
|
+
},
|
|
47
|
+
"client": {
|
|
48
|
+
"platform": "web"
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"repository": {
|
|
52
|
+
"type": "git",
|
|
53
|
+
"url": "git+https://github.com/xiaoxiao44443/dfy-dsh-plugins.git",
|
|
54
|
+
"directory": "plugins/wallpaper"
|
|
55
|
+
},
|
|
56
|
+
"homepage": "https://github.com/xiaoxiao44443/dfy-dsh-plugins#readme",
|
|
57
|
+
"bugs": {
|
|
58
|
+
"url": "https://github.com/xiaoxiao44443/dfy-dsh-plugins/issues"
|
|
59
|
+
},
|
|
60
|
+
"keywords": [
|
|
61
|
+
"deepseek-harness",
|
|
62
|
+
"dsh-plugin"
|
|
63
|
+
],
|
|
64
|
+
"scripts": {
|
|
65
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
66
|
+
"build": "tsc -p tsconfig.json && node scripts/build-client.mjs",
|
|
67
|
+
"test": "tsc -p tsconfig.json && node --test"
|
|
68
|
+
}
|
|
69
|
+
}
|