Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,9 @@ powershell -ExecutionPolicy Bypass -File .\windows\scripts\start-dream-skin.ps1
```

After launch, switch directly through **已保存主题 → 桥本有菜**; no cross-folder
manual import is required. **更换背景图** still imports your own UI-free
wallpaper, which can then be saved for one-click switching.
manual import is required. The enhanced **更换背景图或视频** action accepts
PNG/JPEG/WebP artwork and muted looping MP4/WebM video, which can then be saved
for one-click switching.

> The downloadable user source is [`docs/images/presets/romantic-rose-source.png`](./docs/images/presets/romantic-rose-source.png) (`1672 × 941`); the macOS one-click preset uses the normalized derived [`background.jpg`](./macos/presets/preset-romantic-rose/background.jpg) (`2560 × 1440`). Do not import either screenshot above: they contain real UI and are previews only. The background is a user-provided AI-generated example, not an official OpenAI/Codex visual or endorsement; confirm likeness and asset rights before redistributing it.

Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ powershell -ExecutionPolicy Bypass -File .\windows\scripts\install-dream-skin.ps
powershell -ExecutionPolicy Bypass -File .\windows\scripts\start-dream-skin.ps1
```

启动后可直接从「已保存主题 → 桥本有菜」切换;不需要跨目录手动导入。托盘里的「更换背景图」仍可导入你自己的纯背景,保存后继续一键切换
启动后可直接从「已保存主题 → 桥本有菜」切换;不需要跨目录手动导入。增强版托盘里的「更换背景图或视频」支持 PNG/JPEG/WebP 与静音循环的 MP4/WebM,保存后可继续一键切换

> 可下载的用户源图是 [`docs/images/presets/romantic-rose-source.png`](./docs/images/presets/romantic-rose-source.png)(`1672 × 941`);macOS 一键预设使用 [`macos/presets/preset-romantic-rose/background.jpg`](./macos/presets/preset-romantic-rose/background.jpg)(规范化派生 `2560 × 1440`)。上面两个效果图包含真实 UI,**只作预览,绝不能当背景导入**。背景为用户提供的 AI 生成示例,不代表 OpenAI/Codex 官方视觉或背书;公开再分发前请确认人物与素材权利。

Expand Down Expand Up @@ -126,6 +126,7 @@ powershell -ExecutionPolicy Bypass -File .\windows\scripts\start-dream-skin.ps1
- **真·可交互**:侧栏、建议卡、项目选择、输入框都是原生控件,不是整窗假截图贴上去
- **真背景层**:一张 16:9 纯壁纸连续铺满整窗,首页突出氛围,任务页自动降低干扰
- **可换图**:换一张喜欢的纯背景,自适应焦点、安全区和配色后变成你的主题
- **可播视频**:Windows 增强版支持本地 MP4/WebM 静音循环动态壁纸,页面隐藏时自动暂停
- **可存主题**:macOS 菜单栏与 Windows 系统托盘都能保存/切换本地主题
- **可恢复**:一键还原官方外观
- **相对安全**:本机回环 CDP 注入,不改官方二进制与签名
Expand Down
6 changes: 6 additions & 0 deletions windows/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

### 新增

- Windows 主题仓库与托盘支持本地 MP4/WebM 动态壁纸;视频静音循环、页面隐藏时暂停,并可像图片主题一样保存和切换。
- 视频采用上限 128 MB、单块 512 KiB 的 CDP 分块传输,在 renderer 内组装为 Blob URL,避免把完整视频塞进 early bootstrap payload;换主题、暂停和清理时会释放播放器与 Blob URL。
- MP4/WebM 导入会校验扩展名、最小容器签名、大小、受管目录边界和 reparse point,伪造扩展名或越界路径会在复制前拒绝。

### 修复

- 兼容新版 Codex 不再提供 `[role="main"]` 的任务页面:回退到 `main.main-surface` 承载任务背景,收起左侧边栏后也不会清除皮肤。
Expand Down
4 changes: 3 additions & 1 deletion windows/README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ Next, use the generated screenshot to check horizontal overflow and text contras

Open `Codex Dream Skin - Tray` to:

- Import a PNG, JPEG, or WebP background.
- Import a PNG, JPEG, or WebP background, or an MP4/WebM animated wallpaper.
- Save the active theme and switch through saved themes.
- Pause or resume the skin.
- Reapply the theme or fully restore Codex.

Import a UI-free wallpaper rather than a preview containing a window, sidebar, composer, text, or buttons. Images may be at most 16 MB, 16384 pixels on either side, and 50 million total pixels.

Animated wallpapers are always muted, looped, pointer-transparent, and paused while the Codex page is hidden. Videos may be at most 128 MB and are transferred to the current renderer in bounded CDP chunks; they are not uploaded or embedded wholesale in the bootstrap script. Audio, playlists, and Live2D are outside this MVP.

## Restore and remove shortcuts

Restore the stock appearance. If Codex is running, confirm its closure and relaunch:
Expand Down
4 changes: 3 additions & 1 deletion windows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,15 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\scripts\verify-dream-s

打开 `Codex Dream Skin - Tray` 后可以:

- 更换 PNG、JPEG 或 WebP 背景图
- 更换 PNG、JPEG、WebP 背景图,或 MP4、WebM 动态壁纸
- 保存当前主题并从「已保存主题」切换。
- 暂停或继续显示皮肤。
- 重新应用主题,或完整恢复 Codex。

导入图片必须是纯背景,不要使用包含窗口、侧栏、输入框、文字或按钮的效果截图。图片上限为 16 MB;宽或高不能超过 16384 像素,总像素不能超过 5000 万。

动态壁纸始终静音、循环且不拦截鼠标操作;Codex 页面隐藏时会自动暂停。视频上限为 128 MB,并通过有界 CDP 分块传入当前 renderer,不会上传到网络,也不会把整段视频嵌入启动脚本。当前 MVP 不提供声音、播放列表或 Live2D。

## 恢复与卸载快捷方式

恢复官方外观;如果 Codex 正在运行,确认后关闭并重新打开:
Expand Down
18 changes: 18 additions & 0 deletions windows/assets/dream-skin.css
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
}

html.codex-dream-skin body {
position: relative;
isolation: isolate;
background: var(--dream-canvas) !important;
color: var(--dream-text) !important;
font-family: "Segoe UI Variable Text", "Segoe UI", "Microsoft YaHei UI", system-ui, sans-serif !important;
Expand Down Expand Up @@ -133,6 +135,22 @@ html.codex-dream-skin.dream-theme-dark [class~="group/application-menu-top-bar"]
pointer-events: none !important;
}

#codex-dream-skin-media {
position: fixed;
z-index: -1;
inset: 0;
width: 100vw;
height: 100vh;
border: 0;
object-fit: cover;
object-position: var(--dream-art-position);
pointer-events: none;
}

html.codex-dream-skin.dream-art-video body {
background-image: none !important;
}

html.codex-dream-skin [role="main"] {
color: var(--dream-text);
}
Expand Down
134 changes: 129 additions & 5 deletions windows/assets/renderer-inject.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
const STATE_KEY = "__CODEX_DREAM_SKIN_STATE__";
const STYLE_ID = "codex-dream-skin-style";
const CHROME_ID = "codex-dream-skin-chrome";
const MEDIA_ID = "codex-dream-skin-media";
const ROOT_CLASSES = [
"codex-dream-skin",
"dream-theme-light",
"dream-theme-dark",
"dream-art-video",
"dream-art-wide",
"dream-art-standard",
"dream-focus-left",
Expand Down Expand Up @@ -73,6 +75,15 @@
const taskMode = ["auto", "ambient", "banner", "off"].includes(art.taskMode)
? art.taskMode
: "auto";
const media = config.media && typeof config.media === "object" ? config.media : {};
const mediaType = media.type === "video" ? "video" : "image";
const mediaMime = ["video/mp4", "video/webm"].includes(media.mime) ? media.mime : null;
const mediaSize = Number.isSafeInteger(Number(media.size)) && Number(media.size) > 0
? Number(media.size)
: null;
const playbackRate = Number.isFinite(Number(media.playbackRate))
? clamp(Number(media.playbackRate), .25, 2)
: 1;
const metadataRatio = Number(config?.artMetadata?.ratio);
return {
appearance,
Expand All @@ -82,6 +93,10 @@
focusY: hasNumber(art.focusY) ? clamp(art.focusY) : null,
accent: safeAccent,
initialAspect: Number.isFinite(metadataRatio) && metadataRatio > 0 ? metadataRatio : null,
mediaType,
mediaMime,
mediaSize,
playbackRate,
};
};

Expand All @@ -90,18 +105,27 @@
if (previous?.timer) clearInterval(previous.timer);
if (previous?.scheduler?.timeout) clearTimeout(previous.scheduler.timeout);
if (previous?.artUrl) URL.revokeObjectURL(previous.artUrl);
if (previous?.mediaUrl) URL.revokeObjectURL(previous.mediaUrl);
if (previous?.visibilityHandler) {
document.removeEventListener?.("visibilitychange", previous.visibilityHandler);
}
document.getElementById(MEDIA_ID)?.remove();
const artUrl = (() => {
if (!artDataUrl) return null;
const comma = artDataUrl.indexOf(",");
if (comma < 0) return null;
const binary = atob(artDataUrl.slice(comma + 1));
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
const mime = /^data:([^;,]+)/.exec(artDataUrl)?.[1] || "image/png";
return URL.createObjectURL(new Blob([bytes], { type: mime }));
})();
const config = normalizeConfig(rawConfig);
let mediaUrl = null;
let mediaTransfer = null;
let profile = {
...defaultProfile,
aspect: config.initialAspect ?? defaultProfile.aspect,
aspect: config.initialAspect ?? (config.mediaType === "video" ? 16 / 9 : defaultProfile.aspect),
};
const existingStyle = document.getElementById(STYLE_ID);
if (existingStyle) {
Expand All @@ -110,7 +134,14 @@
}

const analyzeArt = () => new Promise((resolve) => {
if (typeof Image !== "function") {
if (config.mediaType === "video") {
resolve({
...defaultProfile,
aspect: config.initialAspect ?? 16 / 9,
});
return;
}
if (!artUrl || typeof Image !== "function") {
resolve(defaultProfile);
return;
}
Expand Down Expand Up @@ -285,6 +316,7 @@
document.querySelectorAll(`.${HOME_UTILITY_CLASS}`).forEach((node) => node.classList.remove(HOME_UTILITY_CLASS));
document.getElementById(STYLE_ID)?.remove();
document.getElementById(CHROME_ID)?.remove();
document.getElementById(MEDIA_ID)?.remove();
};

const applyProfile = (root) => {
Expand All @@ -301,6 +333,7 @@
const accentInk = luminance(...profile.accent) > .42 ? "rgb(26 24 28)" : "rgb(250 248 251)";
root.classList.toggle("dream-theme-light", appearance === "light");
root.classList.toggle("dream-theme-dark", appearance === "dark");
root.classList.toggle("dream-art-video", config.mediaType === "video");
root.classList.toggle("dream-art-wide", profile.aspect >= 1.75);
root.classList.toggle("dream-art-standard", profile.aspect < 1.75);
for (const value of ["left", "center", "right"]) {
Expand All @@ -312,7 +345,7 @@
for (const value of ["ambient", "banner", "off"]) {
root.classList.toggle(`dream-task-${value}`, taskMode === value);
}
root.style.setProperty("--dream-art", `url("${artUrl}")`);
root.style.setProperty("--dream-art", artUrl ? `url("${artUrl}")` : "none");
root.style.setProperty("--dream-art-position", `${Math.round(focusX * 100)}% ${Math.round(focusY * 100)}%`);
root.style.setProperty("--dream-focus-x", String(focusX));
root.style.setProperty("--dream-focus-y", String(focusY));
Expand All @@ -321,6 +354,89 @@
root.style.setProperty("--dream-image-luma", profile.luma.toFixed(3));
};

const syncMediaElement = () => {
let media = document.getElementById(MEDIA_ID);
const shellPresent = Boolean(document.querySelector("main.main-surface"));
if (config.mediaType !== "video" || !shellPresent) {
media?.remove();
return null;
}
if (!media || media.parentElement !== document.body) {
media?.remove();
media = document.createElement("video");
media.id = MEDIA_ID;
media.setAttribute("aria-hidden", "true");
media.autoplay = true;
media.controls = false;
media.disablePictureInPicture = true;
media.loop = true;
media.muted = true;
media.playsInline = true;
media.preload = "auto";
document.body.appendChild(media);
}
media.defaultMuted = true;
media.muted = true;
media.loop = true;
media.playbackRate = config.playbackRate;
if (mediaUrl && media.src !== mediaUrl) {
media.src = mediaUrl;
media.load?.();
}
if (!mediaUrl || document.hidden) {
media.pause?.();
} else {
const playback = media.play?.();
playback?.catch?.(() => {});
}
return media;
};

const beginMedia = ({ mime, size } = {}) => {
if (config.mediaType !== "video" || mime !== config.mediaMime ||
!Number.isSafeInteger(size) || size !== config.mediaSize) return false;
mediaTransfer = { chunks: [], received: 0, expected: size, mime };
if (mediaUrl) URL.revokeObjectURL(mediaUrl);
mediaUrl = null;
const media = syncMediaElement();
if (media) {
media.pause?.();
media.removeAttribute?.("src");
media.load?.();
}
const state = window[STATE_KEY];
if (state?.installToken === installToken) state.mediaUrl = null;
return true;
};

const appendMedia = (encoded) => {
if (!mediaTransfer || typeof encoded !== "string" || encoded.length > 1024 * 1024) return false;
let binary;
try {
binary = atob(encoded);
} catch {
return false;
}
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index);
if (mediaTransfer.received + bytes.length > mediaTransfer.expected) return false;
mediaTransfer.chunks.push(bytes);
mediaTransfer.received += bytes.length;
return true;
};

const commitMedia = () => {
if (!mediaTransfer || mediaTransfer.received !== mediaTransfer.expected) return false;
const nextUrl = URL.createObjectURL(new Blob(mediaTransfer.chunks, { type: mediaTransfer.mime }));
mediaTransfer = null;
if (mediaUrl) URL.revokeObjectURL(mediaUrl);
mediaUrl = nextUrl;
const state = window[STATE_KEY];
if (state?.installToken === installToken) state.mediaUrl = mediaUrl;
syncMediaElement();
return true;
};

const ensure = () => {
if (window.__CODEX_DREAM_SKIN_DISABLED__) return;
const root = document.documentElement;
Expand Down Expand Up @@ -369,6 +485,7 @@
document.body.appendChild(chrome);
}
chrome.classList.toggle("dream-home-shell", Boolean(home));
syncMediaElement();
};

const cleanup = () => {
Expand All @@ -380,6 +497,10 @@
if (state?.timer) clearInterval(state.timer);
if (state?.scheduler?.timeout) clearTimeout(state.scheduler.timeout);
if (state?.artUrl) URL.revokeObjectURL(state.artUrl);
if (state?.mediaUrl) URL.revokeObjectURL(state.mediaUrl);
if (state?.visibilityHandler) {
document.removeEventListener?.("visibilitychange", state.visibilityHandler);
}
delete window[STATE_KEY];
return true;
};
Expand All @@ -403,8 +524,11 @@
attributeFilter: ["class", "data-theme", "data-appearance", "data-color-mode"],
});
const timer = setInterval(ensure, 5000);
const visibilityHandler = () => syncMediaElement();
document.addEventListener?.("visibilitychange", visibilityHandler);
window[STATE_KEY] = {
ensure, cleanup, observer, timer, scheduler, artUrl, profile, config, installToken, version: "1.2.0",
ensure, cleanup, observer, timer, scheduler, artUrl, mediaUrl, beginMedia, appendMedia, commitMedia,
visibilityHandler, profile, config, installToken, version: "1.2.0",
};
ensure();
analyzeArt().then((result) => {
Expand All @@ -414,5 +538,5 @@
state.profile = result;
ensure();
});
return { installed: true, version: "1.2.0", adaptive: true };
return { installed: true, version: "1.2.0", adaptive: true, mediaType: config.mediaType };
})(__DREAM_CSS_JSON__, __DREAM_ART_JSON__, __DREAM_THEME_JSON__)
Loading
Loading