feat: 1.3.0 release, fix export error
This commit is contained in:
@@ -4,6 +4,26 @@
|
|||||||
|
|
||||||
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
本项目遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-08-07
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 本地离线 neo-ppt 编辑器镜像(`editor/`):无 iframe、无 `www.kimi.com`,支持打开 PPTD 文件夹编辑与官方 UI 内导出
|
||||||
|
- Skill install 同步拷贝完整 `editor/` 与 patched WASM,供已安装的 skill 树离线使用
|
||||||
|
- 图片 QA / `--browser` 导出改为打本地编辑器(`?ndExport=1` + `payload.json`),不再依赖 kimi.com
|
||||||
|
|
||||||
|
### 变更
|
||||||
|
|
||||||
|
- 默认 PPTX 导出统一走本地 patched WASM(单一真源 `editor/neo-ppt/assets/pptd_wasm_bg-DPPWdROu.wasm`)
|
||||||
|
- 本地编辑器关闭分享 / 云盘 / Google Drive 入口;拦截相关云端与 Google API 请求
|
||||||
|
- 移除旧 iframe 壳(`app.js` / `styles.css`)、废弃的 `export_host.html` 与未接线的浏览器顶栏导出胶水
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
|
||||||
|
- 修复系统深色主题导致的白屏闪烁(强制 light)
|
||||||
|
- 修复 demo / 只读打开后无法编辑、演示、导出(`isCreate: false` + 可编辑配置)
|
||||||
|
- 浏览器会话剥离 `HTTP(S)_PROXY`,避免公司代理 403 本地回环地址
|
||||||
|
|
||||||
## [1.2.0] - 2026-08-06
|
## [1.2.0] - 2026-08-06
|
||||||
|
|
||||||
### 新增
|
### 新增
|
||||||
|
|||||||
@@ -4,6 +4,26 @@
|
|||||||
|
|
||||||
This project follows [Semantic Versioning](https://semver.org/).
|
This project follows [Semantic Versioning](https://semver.org/).
|
||||||
|
|
||||||
|
## [1.3.0] - 2026-08-07
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Offline neo-ppt editor mirror (`editor/`): no iframe, no `www.kimi.com`; open a PPTD folder and export from the official UI
|
||||||
|
- Skill install copies the full `editor/` tree plus patched WASM into installed skill directories
|
||||||
|
- Image QA / `--browser` export now drives the local editor (`?ndExport=1` + `payload.json`) instead of kimi.com
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Default PPTX export uses the local patched WASM (single source: `editor/neo-ppt/assets/pptd_wasm_bg-DPPWdROu.wasm`)
|
||||||
|
- Local editor hides share / cloud / Google Drive entry points and blocks related cloud/Google API calls
|
||||||
|
- Removed the old iframe shell (`app.js` / `styles.css`), deprecated `export_host.html`, and unused browser top-bar export glue
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- Avoid dark-theme FOUC by forcing light mode
|
||||||
|
- Demo / read-only opens can edit, present, and export again (`isCreate: false` + editable config)
|
||||||
|
- Browser sessions strip `HTTP(S)_PROXY` so corporate proxies do not 403 localhost
|
||||||
|
|
||||||
## [1.2.0] - 2026-08-06
|
## [1.2.0] - 2026-08-06
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -135,7 +135,9 @@ npx open-kimi-ppt-skill serve --port 56000
|
|||||||
|
|
||||||
### Windows:常驻调试浏览器说明
|
### Windows:常驻调试浏览器说明
|
||||||
|
|
||||||
在 Windows 上导出 PPTX 时,脚本会自动启动一个**常驻的调试浏览器**。这是有意设计,不是异常进程:
|
仅在使用 **视觉质检**(`export_images.py`)或 **`--browser` PPTX**(本地编辑器 UI)时需要。默认 Node WASM 导出不启动浏览器。
|
||||||
|
|
||||||
|
在 Windows 上走浏览器路径时,脚本会自动启动一个**常驻的调试浏览器**。这是有意设计,不是异常进程:
|
||||||
|
|
||||||
- **为什么需要**:agent-browser 在 Windows 下无法自行启动 Chrome(Chrome 启动器把进程交接给子进程后立即退出,被误判为崩溃),导出只能改为驱动一个外部启动的浏览器。
|
- **为什么需要**:agent-browser 在 Windows 下无法自行启动 Chrome(Chrome 启动器把进程交接给子进程后立即退出,被误判为崩溃),导出只能改为驱动一个外部启动的浏览器。
|
||||||
- **它是什么**:优先使用本机 Chrome,未安装时回退到 Edge;以 `--remote-debugging-port`(默认 `9337`)和独立配置目录 `%TEMP%\okp-cdp-profile` 启动,窗口定位在屏幕外,不影响日常使用。
|
- **它是什么**:优先使用本机 Chrome,未安装时回退到 Edge;以 `--remote-debugging-port`(默认 `9337`)和独立配置目录 `%TEMP%\okp-cdp-profile` 启动,窗口定位在屏幕外,不影响日常使用。
|
||||||
@@ -233,13 +235,10 @@ deck/
|
|||||||
- CLI 只在 `127.0.0.1` 启动静态文件服务,不会监听局域网地址。
|
- CLI 只在 `127.0.0.1` 启动静态文件服务,不会监听局域网地址。
|
||||||
- 浏览器只在用户主动授权后读取完整 PPTD 项目目录。
|
- 浏览器只在用户主动授权后读取完整 PPTD 项目目录。
|
||||||
- 保存回调只允许修改 `.pptd` 和 `.page` 文件,并拒绝绝对路径与 `..` 路径越界。
|
- 保存回调只允许修改 `.pptd` 和 `.page` 文件,并拒绝绝对路径与 `..` 路径越界。
|
||||||
- PPTD 内容由本地宿主交给公开的 Kimi 网页编辑器处理;远程图片、字体和编辑器资源仍可能从对应服务器加载。
|
- 默认编辑与 PPTX / 图片导出使用本地 neo-ppt 镜像 + patched WASM,**不依赖** `www.kimi.com`;远程图片、字体若被文稿引用仍可能从对应服务器加载。
|
||||||
|
- 视觉质检(`export_images.py`)与 `--browser` 路径驱动的是同一套本地编辑器(需本机 Chromium),不再访问公开 Kimi 站。
|
||||||
- 本项目不会提供或注入 Kimi 登录令牌,也不会访问用户的 Kimi 私有文稿。
|
- 本项目不会提供或注入 Kimi 登录令牌,也不会访问用户的 Kimi 私有文稿。
|
||||||
|
|
||||||
## 兼容性说明
|
|
||||||
|
|
||||||
这是针对当前公开实现的兼容宿主,不是稳定的官方 SDK。Kimi 更新前端资源哈希、PPTD 格式或 iframe/RPC 协议后,本项目可能需要同步升级。成功生成 PPTX 也不代表 PowerPoint、WPS 和 Keynote 对所有动画效果都能完全一致地播放。
|
|
||||||
|
|
||||||
## 本地开发
|
## 本地开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
+5
-6
@@ -123,7 +123,9 @@ Writable folder access requires a Chromium-based browser with the File System Ac
|
|||||||
|
|
||||||
### Windows: a persistent debug browser
|
### Windows: a persistent debug browser
|
||||||
|
|
||||||
On Windows, exporting PPTX automatically starts a **persistent debug browser**. This is by design, not a stray process:
|
Only needed for **visual QA** (`export_images.py`) or the **`--browser` PPTX fallback**. Default local WASM export does not start a browser.
|
||||||
|
|
||||||
|
On Windows, the browser path automatically starts a **persistent debug browser**. This is by design, not a stray process:
|
||||||
|
|
||||||
- **Why it's needed**: agent-browser cannot launch Chrome by itself on Windows (the Chrome launcher hands off to a child process and exits immediately, which is misread as a crash), so the export drives an externally started browser instead.
|
- **Why it's needed**: agent-browser cannot launch Chrome by itself on Windows (the Chrome launcher hands off to a child process and exits immediately, which is misread as a crash), so the export drives an externally started browser instead.
|
||||||
- **What it is**: your installed Chrome (falling back to Edge), launched with `--remote-debugging-port` (default `9337`) and a dedicated profile at `%TEMP%\okp-cdp-profile`, with the window positioned off-screen so it stays out of the way.
|
- **What it is**: your installed Chrome (falling back to Edge), launched with `--remote-debugging-port` (default `9337`) and a dedicated profile at `%TEMP%\okp-cdp-profile`, with the window positioned off-screen so it stays out of the way.
|
||||||
@@ -221,13 +223,10 @@ deck/
|
|||||||
- The CLI serves static files on `127.0.0.1` only and does not listen on LAN interfaces.
|
- The CLI serves static files on `127.0.0.1` only and does not listen on LAN interfaces.
|
||||||
- The browser reads a complete PPTD project directory only after explicit user authorization.
|
- The browser reads a complete PPTD project directory only after explicit user authorization.
|
||||||
- Save callbacks may only modify `.pptd` and `.page` files; absolute paths and `..` traversal are rejected.
|
- Save callbacks may only modify `.pptd` and `.page` files; absolute paths and `..` traversal are rejected.
|
||||||
- The local host passes PPTD content to the public Kimi web editor. Remote images, fonts, and editor resources may still be fetched from their respective servers.
|
- Default editing and PPTX/image export use the local neo-ppt mirror plus patched WASM and do **not** depend on `www.kimi.com`. Remote images or fonts referenced by the deck may still be fetched from their hosts.
|
||||||
|
- Visual QA (`export_images.py`) and `--browser` drive that same local editor (Chromium required) and no longer hit the public Kimi site.
|
||||||
- This project does not provide or inject Kimi login tokens and does not access private Kimi documents.
|
- This project does not provide or inject Kimi login tokens and does not access private Kimi documents.
|
||||||
|
|
||||||
## Compatibility
|
|
||||||
|
|
||||||
This is a compatibility host for the current public implementation, not a stable official SDK. Updates to Kimi frontend asset hashes, the PPTD format, or the iframe/RPC protocol may require a corresponding project update. Successfully generating a PPTX does not guarantee identical animation playback in PowerPoint, WPS, and Keynote.
|
|
||||||
|
|
||||||
## Local development
|
## Local development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -190,22 +190,28 @@ async function promptInstallTargets() {
|
|||||||
const choices = knownTargetDirectories();
|
const choices = knownTargetDirectories();
|
||||||
const selected = new Set([0]);
|
const selected = new Set([0]);
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
const lines = choices.length + 3;
|
// title + help + one line per choice (must match what render() writes)
|
||||||
|
const lines = choices.length + 2;
|
||||||
|
|
||||||
|
const writeLine = (text) => {
|
||||||
|
output.write(`\u001b[2K${text}\n`);
|
||||||
|
};
|
||||||
|
|
||||||
const render = (initial = false) => {
|
const render = (initial = false) => {
|
||||||
if (!initial) {
|
if (!initial) {
|
||||||
output.write(`\u001b[${lines}A`);
|
output.write(`\u001b[${lines}A`);
|
||||||
}
|
}
|
||||||
output.write("Install open-kimi-ppt to which skills directories?\n");
|
writeLine("Install open-kimi-ppt to which skills directories?");
|
||||||
output.write("↑/↓ move · space select · a all · enter confirm · ctrl+c cancel\n");
|
writeLine("↑/↓ move · space select · a all · enter confirm · ctrl+c cancel");
|
||||||
for (const [index, choice] of choices.entries()) {
|
for (const [index, choice] of choices.entries()) {
|
||||||
const pointer = index === cursor ? "❯" : " ";
|
const pointer = index === cursor ? "❯" : " ";
|
||||||
const mark = selected.has(index) ? "◉" : "◯";
|
const mark = selected.has(index) ? "◉" : "◯";
|
||||||
const installed = existsSync(join(choice.directory, SKILL_NAME, "SKILL.md"))
|
const installed = existsSync(join(choice.directory, SKILL_NAME, "SKILL.md"))
|
||||||
? " (installed)"
|
? " (installed)"
|
||||||
: "";
|
: "";
|
||||||
const line = `${pointer}${mark} ${displayPath(choice.directory).padEnd(28)} ${choice.label}${installed}`;
|
writeLine(
|
||||||
output.write(`\u001b[2K${line}\n`);
|
`${pointer}${mark} ${displayPath(choice.directory).padEnd(28)} ${choice.label}${installed}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -311,10 +317,21 @@ async function resolveInstallTargets(options) {
|
|||||||
return promptInstallTargets();
|
return promptInstallTargets();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const CANONICAL_WASM = join(
|
||||||
|
packageRoot,
|
||||||
|
"editor",
|
||||||
|
"neo-ppt",
|
||||||
|
"assets",
|
||||||
|
"pptd_wasm_bg-DPPWdROu.wasm",
|
||||||
|
);
|
||||||
|
|
||||||
function installSkillTo(skillsDirectory) {
|
function installSkillTo(skillsDirectory) {
|
||||||
if (!existsSync(join(sourceDirectory, "SKILL.md"))) {
|
if (!existsSync(join(sourceDirectory, "SKILL.md"))) {
|
||||||
throw new Error(`packaged skill is incomplete: ${sourceDirectory}`);
|
throw new Error(`packaged skill is incomplete: ${sourceDirectory}`);
|
||||||
}
|
}
|
||||||
|
if (!existsSync(CANONICAL_WASM)) {
|
||||||
|
throw new Error(`packaged patched WASM is missing: ${CANONICAL_WASM}`);
|
||||||
|
}
|
||||||
|
|
||||||
const destination = join(skillsDirectory, SKILL_NAME);
|
const destination = join(skillsDirectory, SKILL_NAME);
|
||||||
const replaced = existsSync(destination);
|
const replaced = existsSync(destination);
|
||||||
@@ -328,6 +345,19 @@ function installSkillTo(skillsDirectory) {
|
|||||||
recursive: true,
|
recursive: true,
|
||||||
filter: (source) => ![".DS_Store", "_user_meta.json"].includes(basename(source)),
|
filter: (source) => ![".DS_Store", "_user_meta.json"].includes(basename(source)),
|
||||||
});
|
});
|
||||||
|
// Single canonical WASM in editor/; copy into skill tree for agent scripts.
|
||||||
|
const stagedWasmDir = join(stagedSkill, "scripts", "local-export");
|
||||||
|
mkdirSync(stagedWasmDir, { recursive: true });
|
||||||
|
cpSync(CANONICAL_WASM, join(stagedWasmDir, "pptd_wasm_bg.wasm"));
|
||||||
|
// Offline image/PPTX browser path needs the local neo-ppt mirror.
|
||||||
|
const packagedEditor = join(packageRoot, "editor");
|
||||||
|
if (!existsSync(join(packagedEditor, "index.html"))) {
|
||||||
|
throw new Error(`packaged editor is missing: ${packagedEditor}`);
|
||||||
|
}
|
||||||
|
cpSync(packagedEditor, join(stagedSkill, "editor"), {
|
||||||
|
recursive: true,
|
||||||
|
filter: (source) => basename(source) !== ".DS_Store",
|
||||||
|
});
|
||||||
|
|
||||||
rmSync(destination, { recursive: true, force: true });
|
rmSync(destination, { recursive: true, force: true });
|
||||||
renameSync(stagedSkill, destination);
|
renameSync(stagedSkill, destination);
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# NeoDeck Local(完全离线 · 无 iframe)
|
||||||
|
|
||||||
|
夺舍官方 Kimi neo-ppt 前端镜像,单页运行:
|
||||||
|
|
||||||
|
- 无 iframe、无 www.kimi.com、无云同步 / 分享 / 云盘 / AI
|
||||||
|
- 本地打开 PPTD 文件夹并自动保存
|
||||||
|
- 官方编辑器内「导出」走本地 patched WASM
|
||||||
|
|
||||||
|
## 启动
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd editor
|
||||||
|
npx serve .
|
||||||
|
# 或 python3 -m http.server 55173
|
||||||
|
```
|
||||||
|
|
||||||
|
浏览器打开根路径即可(不要用 file://)。仓库内也可用 `npx open-kimi-ppt-skill serve`。
|
||||||
|
|
||||||
|
## 使用
|
||||||
|
|
||||||
|
1. **打开 PPTD 文件夹**(Chromium 可读写;其它浏览器可退化为只读上传)
|
||||||
|
2. 在官方编辑器 UI 中编辑
|
||||||
|
3. 顶栏 **导出** → 下载 PPTX / 图片(无分享、无 Google 云盘)
|
||||||
|
|
||||||
|
## 结构
|
||||||
|
|
||||||
|
```
|
||||||
|
editor/
|
||||||
|
index.html # 唯一入口(官方 UI + 本地顶栏)
|
||||||
|
local-bridge.js # 取代 Penpal 父页面的本地桥
|
||||||
|
local-shell.css
|
||||||
|
neo-ppt/ # 官方前端镜像
|
||||||
|
assets/
|
||||||
|
pptd_wasm_bg-DPPWdROu.wasm # 唯一一份 patched PPTX WASM(真源)
|
||||||
|
index_bg-*.wasm # resvg(渲染用,勿与上者混淆)
|
||||||
|
```
|
||||||
|
|
||||||
|
Agent 侧 `scripts/local-export/` 在 skill install 时会从上述真源拷贝一份 `pptd_wasm_bg.wasm`;仓库内导出脚本直接引用本路径。
|
||||||
-798
@@ -1,798 +0,0 @@
|
|||||||
import {
|
|
||||||
assertWritableChangePath,
|
|
||||||
basename,
|
|
||||||
dirname,
|
|
||||||
extractPagePaths,
|
|
||||||
joinDeckPath,
|
|
||||||
normalizeRelativePath,
|
|
||||||
titleFromManifest,
|
|
||||||
} from "./lib.js";
|
|
||||||
|
|
||||||
const EDITOR_ORIGIN = "https://www.kimi.com";
|
|
||||||
const PENPAL_MODULE = "https://statics.moonshot.cn/neo-design/assets/penpal-C4NjirZE.js";
|
|
||||||
const MAX_TEXT_BYTES = 20 * 1024 * 1024;
|
|
||||||
const MAX_IMAGE_BYTES = 20 * 1024 * 1024;
|
|
||||||
const MAX_CHANGE_COUNT = 600;
|
|
||||||
|
|
||||||
const elements = {
|
|
||||||
frame: document.querySelector("#editor"),
|
|
||||||
openFolder: document.querySelector("#open-folder"),
|
|
||||||
openDemo: document.querySelector("#open-demo"),
|
|
||||||
reload: document.querySelector("#reload-deck"),
|
|
||||||
documentTitle: document.querySelector("#document-title"),
|
|
||||||
documentPath: document.querySelector("#document-path"),
|
|
||||||
connectionStatus: document.querySelector("#connection-status"),
|
|
||||||
connectionLabel: document.querySelector("#connection-label"),
|
|
||||||
saveState: document.querySelector("#save-state"),
|
|
||||||
loadingCard: document.querySelector("#loading-card"),
|
|
||||||
loadingTitle: document.querySelector("#loading-title"),
|
|
||||||
loadingMessage: document.querySelector("#loading-message"),
|
|
||||||
activityPanel: document.querySelector("#activity-panel"),
|
|
||||||
activityList: document.querySelector("#activity-list"),
|
|
||||||
toggleActivity: document.querySelector("#toggle-activity"),
|
|
||||||
closeActivity: document.querySelector("#close-activity"),
|
|
||||||
openDialog: document.querySelector("#open-dialog"),
|
|
||||||
closeOpenDialog: document.querySelector("#close-open-dialog"),
|
|
||||||
uploadDropzone: document.querySelector("#upload-dropzone"),
|
|
||||||
chooseWritableFolder: document.querySelector("#choose-writable-folder"),
|
|
||||||
uploadFolder: document.querySelector("#upload-folder"),
|
|
||||||
deckDialog: document.querySelector("#deck-dialog"),
|
|
||||||
deckOptions: document.querySelector("#deck-options"),
|
|
||||||
folderFallback: document.querySelector("#folder-fallback"),
|
|
||||||
toastRegion: document.querySelector("#toast-region"),
|
|
||||||
};
|
|
||||||
|
|
||||||
const state = {
|
|
||||||
remote: null,
|
|
||||||
connection: null,
|
|
||||||
source: "demo",
|
|
||||||
directoryHandle: null,
|
|
||||||
fileIndex: new Map(),
|
|
||||||
memoryFiles: new Map(),
|
|
||||||
manifestPath: "presentation.pptd",
|
|
||||||
manifestDirectory: "",
|
|
||||||
manifestContent: "",
|
|
||||||
deckTitle: "演示文稿",
|
|
||||||
lastDeckPayload: null,
|
|
||||||
saveQueue: Promise.resolve(),
|
|
||||||
imageCache: new Map(),
|
|
||||||
dragDepth: 0,
|
|
||||||
readOnlyFallback: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
function setConnection(kind, label) {
|
|
||||||
elements.connectionStatus.className = `status-pill status-${kind}`;
|
|
||||||
elements.connectionLabel.textContent = label;
|
|
||||||
document.documentElement.dataset.connection = kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setSaveState(label, kind = "idle") {
|
|
||||||
elements.saveState.textContent = label;
|
|
||||||
elements.saveState.dataset.kind = kind;
|
|
||||||
document.documentElement.dataset.saveState = kind;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setLoading(visible, title, message) {
|
|
||||||
elements.loadingCard.classList.toggle("is-hidden", !visible);
|
|
||||||
elements.loadingCard.setAttribute("aria-hidden", String(!visible));
|
|
||||||
if (title) elements.loadingTitle.textContent = title;
|
|
||||||
if (message) elements.loadingMessage.textContent = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatTime(date = new Date()) {
|
|
||||||
return new Intl.DateTimeFormat("zh-CN", {
|
|
||||||
hour: "2-digit",
|
|
||||||
minute: "2-digit",
|
|
||||||
second: "2-digit",
|
|
||||||
}).format(date);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addActivity(message, tone = "info") {
|
|
||||||
const item = document.createElement("li");
|
|
||||||
item.dataset.tone = tone;
|
|
||||||
item.innerHTML = `<time>${formatTime()}</time><span></span>`;
|
|
||||||
item.querySelector("span").textContent = message;
|
|
||||||
elements.activityList.prepend(item);
|
|
||||||
while (elements.activityList.children.length > 40) {
|
|
||||||
elements.activityList.lastElementChild.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function toast(message, tone = "info") {
|
|
||||||
const item = document.createElement("div");
|
|
||||||
item.className = `toast toast-${tone}`;
|
|
||||||
item.textContent = message;
|
|
||||||
elements.toastRegion.append(item);
|
|
||||||
requestAnimationFrame(() => item.classList.add("is-visible"));
|
|
||||||
setTimeout(() => {
|
|
||||||
item.classList.remove("is-visible");
|
|
||||||
setTimeout(() => item.remove(), 220);
|
|
||||||
}, 3200);
|
|
||||||
}
|
|
||||||
|
|
||||||
function editorQuery() {
|
|
||||||
return new URLSearchParams({
|
|
||||||
sdkMode: "ppt-editor",
|
|
||||||
pptPlatform: "neodeck-local",
|
|
||||||
functional: JSON.stringify({
|
|
||||||
fullscreen: true,
|
|
||||||
present: true,
|
|
||||||
export: true,
|
|
||||||
close: false,
|
|
||||||
annotation: false,
|
|
||||||
feedback: false,
|
|
||||||
share: false,
|
|
||||||
versionHistory: false,
|
|
||||||
}),
|
|
||||||
sdkSaveMode: "external",
|
|
||||||
sdkImageMode: "external",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function demoDeck() {
|
|
||||||
const manifest = JSON.stringify({
|
|
||||||
version: "v2",
|
|
||||||
title: "NeoDeck Local",
|
|
||||||
size: [960, 540],
|
|
||||||
pages: ["pages/01.page", "pages/02.page"],
|
|
||||||
});
|
|
||||||
const pages = [
|
|
||||||
{
|
|
||||||
path: "pages/01.page",
|
|
||||||
content: JSON.stringify({
|
|
||||||
pageType: "content",
|
|
||||||
background: { color: "#f7f8fc" },
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
elementId: "eyebrow",
|
|
||||||
elementType: "text",
|
|
||||||
bounds: [92, 96, 776, 40],
|
|
||||||
content: {
|
|
||||||
text: '<p><span style="font-size:16px;color:#6c5ce7;font-weight:700;letter-spacing:2px">NEODECK LOCAL</span></p>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
elementId: "title",
|
|
||||||
elementType: "text",
|
|
||||||
bounds: [92, 155, 776, 142],
|
|
||||||
content: {
|
|
||||||
text: '<p><span style="font-size:52px;color:#171923;font-weight:700">直接打开和保存<br/>本地 PPTD</span></p>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
elementId: "subtitle",
|
|
||||||
elementType: "text",
|
|
||||||
bounds: [94, 330, 660, 58],
|
|
||||||
content: {
|
|
||||||
text: '<p><span style="font-size:20px;color:#667085">第三方宿主 · 本地文件 · Kimi neo-ppt 编辑内核</span></p>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
animations: [
|
|
||||||
{
|
|
||||||
elementId: "title",
|
|
||||||
effect: "fade-in",
|
|
||||||
trigger: "onClick",
|
|
||||||
direction: "up",
|
|
||||||
easing: "ease-in-out",
|
|
||||||
durationMs: 700,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "pages/02.page",
|
|
||||||
content: JSON.stringify({
|
|
||||||
pageType: "content",
|
|
||||||
background: { color: "#171923" },
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
elementId: "second-title",
|
|
||||||
elementType: "text",
|
|
||||||
bounds: [100, 120, 760, 96],
|
|
||||||
content: {
|
|
||||||
text: '<p><span style="font-size:42px;color:#ffffff;font-weight:700">文件系统桥接已经就绪</span></p>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
elementId: "steps",
|
|
||||||
elementType: "text",
|
|
||||||
bounds: [102, 250, 730, 165],
|
|
||||||
content: {
|
|
||||||
text: '<p><span style="font-size:22px;color:#cbd5e1">① 选择完整的 PPTD 项目文件夹</span></p><p><span style="font-size:22px;color:#cbd5e1">② 编辑器自动读取清单、页面与素材</span></p><p><span style="font-size:22px;color:#cbd5e1">③ 自动保存变更到原目录</span></p>',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
return {
|
|
||||||
id: "neodeck-demo",
|
|
||||||
title: "NeoDeck Local",
|
|
||||||
manifestPath: "presentation.pptd",
|
|
||||||
manifestContent: manifest,
|
|
||||||
pages,
|
|
||||||
basePath: "",
|
|
||||||
isCreate: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function requireRemote() {
|
|
||||||
if (!state.remote) throw new Error("编辑器还没有连接完成");
|
|
||||||
return state.remote;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function setDeck(payload, sourceLabel) {
|
|
||||||
const remote = requireRemote();
|
|
||||||
setLoading(true, "正在载入 PPTD", sourceLabel);
|
|
||||||
state.lastDeckPayload = payload;
|
|
||||||
state.manifestContent = payload.manifestContent;
|
|
||||||
state.manifestPath = payload.manifestPath;
|
|
||||||
state.manifestDirectory = dirname(payload.manifestPath);
|
|
||||||
state.deckTitle = payload.title;
|
|
||||||
elements.documentTitle.textContent = payload.title;
|
|
||||||
elements.documentPath.textContent = sourceLabel;
|
|
||||||
elements.reload.disabled = false;
|
|
||||||
|
|
||||||
await remote.setPPTD(payload.id, {
|
|
||||||
pptdContent: payload.manifestContent,
|
|
||||||
pages: payload.pages,
|
|
||||||
basePath: payload.basePath,
|
|
||||||
pptdPath: payload.manifestPath,
|
|
||||||
isCreate: true,
|
|
||||||
});
|
|
||||||
await remote.setEditable(true);
|
|
||||||
const status = await remote.getSlideStatus();
|
|
||||||
setLoading(false);
|
|
||||||
addActivity(`已载入「${payload.title}」,共 ${payload.pages.length} 页`, "success");
|
|
||||||
document.documentElement.dataset.deckStatus = "ready";
|
|
||||||
window.dispatchEvent(new CustomEvent("neodeck:ready", { detail: status }));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDemo() {
|
|
||||||
state.source = "demo";
|
|
||||||
state.directoryHandle = null;
|
|
||||||
state.fileIndex.clear();
|
|
||||||
state.imageCache.clear();
|
|
||||||
state.memoryFiles.clear();
|
|
||||||
state.readOnlyFallback = false;
|
|
||||||
const payload = demoDeck();
|
|
||||||
state.memoryFiles.set(payload.manifestPath, payload.manifestContent);
|
|
||||||
for (const page of payload.pages) state.memoryFiles.set(page.path, page.content);
|
|
||||||
await setDeck(payload, "内置示例 · 修改保存在本页内存");
|
|
||||||
setSaveState("示例 · 内存保存", "memory");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function indexDirectory(directoryHandle) {
|
|
||||||
const index = new Map();
|
|
||||||
async function walk(handle, prefix = "") {
|
|
||||||
for await (const [name, entry] of handle.entries()) {
|
|
||||||
if (name === ".DS_Store") continue;
|
|
||||||
const path = prefix ? `${prefix}/${name}` : name;
|
|
||||||
if (entry.kind === "directory") await walk(entry, path);
|
|
||||||
else index.set(normalizeRelativePath(path), entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await walk(directoryHandle);
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
function indexFallbackFiles(fileList) {
|
|
||||||
const index = new Map();
|
|
||||||
const files = [...fileList];
|
|
||||||
const firstPath = files[0]?.webkitRelativePath || files[0]?.name || "";
|
|
||||||
const rootName = firstPath.includes("/") ? firstPath.split("/")[0] : "";
|
|
||||||
for (const file of files) {
|
|
||||||
let path = file.webkitRelativePath || file.name;
|
|
||||||
if (rootName && path.startsWith(`${rootName}/`)) path = path.slice(rootName.length + 1);
|
|
||||||
index.set(normalizeRelativePath(path), { kind: "file", getFile: async () => file });
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function chooseManifest(paths) {
|
|
||||||
if (paths.length === 1) return paths[0];
|
|
||||||
elements.deckOptions.replaceChildren();
|
|
||||||
const choice = new Promise((resolve) => {
|
|
||||||
for (const path of paths) {
|
|
||||||
const button = document.createElement("button");
|
|
||||||
button.className = "deck-option";
|
|
||||||
button.type = "button";
|
|
||||||
button.innerHTML = `<span class="deck-file-icon">P</span><span><strong></strong><small></small></span><span>›</span>`;
|
|
||||||
button.querySelector("strong").textContent = basename(path);
|
|
||||||
button.querySelector("small").textContent = path;
|
|
||||||
button.addEventListener("click", () => {
|
|
||||||
elements.deckDialog.close();
|
|
||||||
resolve(path);
|
|
||||||
});
|
|
||||||
elements.deckOptions.append(button);
|
|
||||||
}
|
|
||||||
elements.deckDialog.addEventListener(
|
|
||||||
"close",
|
|
||||||
() => {
|
|
||||||
if (elements.deckDialog.returnValue === "cancel") resolve(null);
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
});
|
|
||||||
elements.deckDialog.showModal();
|
|
||||||
return choice;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function textFromIndexedFile(path) {
|
|
||||||
const entry = state.fileIndex.get(normalizeRelativePath(path));
|
|
||||||
if (!entry) throw new Error(`找不到文件:${path}`);
|
|
||||||
const file = await entry.getFile();
|
|
||||||
if (file.size > MAX_TEXT_BYTES) throw new Error(`文件超过 20 MiB:${path}`);
|
|
||||||
return file.text();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadManifest(manifestPath, sourceLabel) {
|
|
||||||
const manifestContent = await textFromIndexedFile(manifestPath);
|
|
||||||
const pagePaths = extractPagePaths(manifestContent);
|
|
||||||
const manifestDirectory = dirname(manifestPath);
|
|
||||||
const pages = [];
|
|
||||||
const missing = [];
|
|
||||||
for (const pagePath of pagePaths.slice(0, 500)) {
|
|
||||||
const indexedPath = joinDeckPath(manifestDirectory, pagePath);
|
|
||||||
try {
|
|
||||||
pages.push({ path: pagePath, content: await textFromIndexedFile(indexedPath) });
|
|
||||||
} catch {
|
|
||||||
missing.push(pagePath);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (pages.length === 0) throw new Error(".pptd 清单引用的页面均无法读取");
|
|
||||||
if (missing.length) toast(`跳过 ${missing.length} 个无法读取的页面`, "warning");
|
|
||||||
|
|
||||||
const fallbackTitle = basename(manifestPath).replace(/\.pptd$/i, "");
|
|
||||||
const title = titleFromManifest(manifestContent, fallbackTitle);
|
|
||||||
await setDeck(
|
|
||||||
{
|
|
||||||
id: fallbackTitle,
|
|
||||||
title,
|
|
||||||
manifestPath,
|
|
||||||
manifestContent,
|
|
||||||
pages,
|
|
||||||
basePath: manifestDirectory ? `${manifestDirectory}/` : "",
|
|
||||||
isCreate: true,
|
|
||||||
},
|
|
||||||
sourceLabel,
|
|
||||||
);
|
|
||||||
setSaveState(state.readOnlyFallback ? "只读打开" : "自动保存已开启", state.readOnlyFallback ? "readonly" : "saved");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDirectoryHandle(directoryHandle, label = directoryHandle.name) {
|
|
||||||
setLoading(true, "正在扫描文件夹", label);
|
|
||||||
let permission = await directoryHandle.queryPermission?.({ mode: "readwrite" });
|
|
||||||
if (permission !== "granted") permission = await directoryHandle.requestPermission?.({ mode: "readwrite" });
|
|
||||||
if (permission !== "granted") throw new Error("没有获得文件夹读写权限");
|
|
||||||
|
|
||||||
state.source = "directory";
|
|
||||||
state.directoryHandle = directoryHandle;
|
|
||||||
state.readOnlyFallback = false;
|
|
||||||
state.fileIndex = await indexDirectory(directoryHandle);
|
|
||||||
state.imageCache.clear();
|
|
||||||
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
|
||||||
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
|
||||||
const manifestPath = await chooseManifest(manifests.sort());
|
|
||||||
if (!manifestPath) {
|
|
||||||
setLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await loadManifest(manifestPath, `${label}/${manifestPath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openFallbackFiles(files) {
|
|
||||||
state.source = "fallback";
|
|
||||||
state.directoryHandle = null;
|
|
||||||
state.readOnlyFallback = true;
|
|
||||||
state.fileIndex = indexFallbackFiles(files);
|
|
||||||
state.imageCache.clear();
|
|
||||||
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
|
||||||
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
|
||||||
const manifestPath = await chooseManifest(manifests.sort());
|
|
||||||
if (manifestPath) await loadManifest(manifestPath, `${manifestPath} · 只读兼容模式`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDirectoryHandleReadOnly(directoryHandle, label = directoryHandle.name) {
|
|
||||||
setLoading(true, "正在扫描文件夹", label);
|
|
||||||
state.source = "fallback";
|
|
||||||
state.directoryHandle = null;
|
|
||||||
state.readOnlyFallback = true;
|
|
||||||
state.fileIndex = await indexDirectory(directoryHandle);
|
|
||||||
state.imageCache.clear();
|
|
||||||
const manifests = [...state.fileIndex.keys()].filter((path) => path.toLowerCase().endsWith(".pptd"));
|
|
||||||
if (manifests.length === 0) throw new Error("文件夹里没有找到 .pptd 清单文件");
|
|
||||||
const manifestPath = await chooseManifest(manifests.sort());
|
|
||||||
if (manifestPath) await loadManifest(manifestPath, `${label}/${manifestPath} · 拖放只读模式`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pickDirectory() {
|
|
||||||
try {
|
|
||||||
if ("showDirectoryPicker" in window) {
|
|
||||||
const directoryHandle = await window.showDirectoryPicker({ id: "neodeck-project", mode: "readwrite" });
|
|
||||||
setOpenDialog(false);
|
|
||||||
await openDirectoryHandle(directoryHandle);
|
|
||||||
} else {
|
|
||||||
elements.folderFallback.click();
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.name === "AbortError") return;
|
|
||||||
handleError(error, "打开文件夹失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function openDroppedFolder(handles) {
|
|
||||||
const usableHandles = handles.filter(Boolean);
|
|
||||||
if (usableHandles.length !== 1 || usableHandles[0].kind !== "directory") {
|
|
||||||
throw new Error("只能拖入一个完整的 PPTD 项目文件夹,不能单独拖入 .pptd 文件");
|
|
||||||
}
|
|
||||||
|
|
||||||
const directoryHandle = usableHandles[0];
|
|
||||||
try {
|
|
||||||
setOpenDialog(false);
|
|
||||||
await openDirectoryHandle(directoryHandle);
|
|
||||||
} catch (error) {
|
|
||||||
setOpenDialog(false);
|
|
||||||
toast("无法以可写模式载入,正在尝试只读打开", "warning");
|
|
||||||
await openDirectoryHandleReadOnly(directoryHandle);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveIndexedPath(requestedPath) {
|
|
||||||
if (typeof requestedPath !== "string" || !requestedPath.trim()) return null;
|
|
||||||
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(requestedPath)) return requestedPath;
|
|
||||||
let candidate = requestedPath.replace(/^file:\/\/+/, "").replaceAll("\\", "/");
|
|
||||||
try {
|
|
||||||
candidate = decodeURIComponent(candidate);
|
|
||||||
} catch {
|
|
||||||
// Preserve literal percent signs.
|
|
||||||
}
|
|
||||||
|
|
||||||
const directCandidates = [];
|
|
||||||
try {
|
|
||||||
directCandidates.push(normalizeRelativePath(candidate));
|
|
||||||
} catch {
|
|
||||||
const parts = candidate.split("/").filter(Boolean);
|
|
||||||
for (let index = 0; index < parts.length; index += 1) {
|
|
||||||
try {
|
|
||||||
directCandidates.push(normalizeRelativePath(parts.slice(index).join("/")));
|
|
||||||
} catch {
|
|
||||||
// Keep looking for a safe suffix.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
directCandidates.push(joinDeckPath(state.manifestDirectory, candidate));
|
|
||||||
} catch {
|
|
||||||
// Absolute paths may still match a safe indexed suffix below.
|
|
||||||
}
|
|
||||||
for (const path of directCandidates) if (state.fileIndex.has(path)) return path;
|
|
||||||
for (const path of state.fileIndex.keys()) {
|
|
||||||
if (directCandidates.some((candidatePath) => path.endsWith(`/${candidatePath}`) || path === candidatePath)) return path;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fileToDataUrl(file) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
const reader = new FileReader();
|
|
||||||
reader.addEventListener("load", () => resolve(String(reader.result)));
|
|
||||||
reader.addEventListener("error", () => reject(reader.error || new Error("图片读取失败")));
|
|
||||||
reader.readAsDataURL(file);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function resolveImage(requestedPath) {
|
|
||||||
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(requestedPath)) return requestedPath;
|
|
||||||
const path = resolveIndexedPath(requestedPath);
|
|
||||||
if (!path) return "";
|
|
||||||
const entry = state.fileIndex.get(path);
|
|
||||||
const file = await entry.getFile();
|
|
||||||
if (file.size > MAX_IMAGE_BYTES) return "";
|
|
||||||
const cacheKey = `${path}:${file.lastModified}:${file.size}`;
|
|
||||||
if (!state.imageCache.has(cacheKey)) state.imageCache.set(cacheKey, fileToDataUrl(file));
|
|
||||||
return state.imageCache.get(cacheKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getImages(payload = {}) {
|
|
||||||
const paths = Array.isArray(payload.filePath) ? payload.filePath : [];
|
|
||||||
const result = await Promise.all(paths.map((path) => resolveImage(path).catch(() => "")));
|
|
||||||
const misses = result.filter((value) => !value).length;
|
|
||||||
if (misses) addActivity(`${paths.length} 张图片中有 ${misses} 张未找到`, "warning");
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getHandleForPath(root, path, create = false) {
|
|
||||||
const parts = normalizeRelativePath(path).split("/");
|
|
||||||
const fileName = parts.pop();
|
|
||||||
let directory = root;
|
|
||||||
for (const part of parts) directory = await directory.getDirectoryHandle(part, { create });
|
|
||||||
return { directory, fileName, file: await directory.getFileHandle(fileName, { create }) };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readBackup(path) {
|
|
||||||
try {
|
|
||||||
const { file } = await getHandleForPath(state.directoryHandle, path, false);
|
|
||||||
return { existed: true, bytes: await (await file.getFile()).arrayBuffer() };
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.name === "NotFoundError") return { existed: false, bytes: null };
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function writeFile(path, content) {
|
|
||||||
const { file } = await getHandleForPath(state.directoryHandle, path, true);
|
|
||||||
const writable = await file.createWritable();
|
|
||||||
try {
|
|
||||||
await writable.write(content);
|
|
||||||
} finally {
|
|
||||||
await writable.close();
|
|
||||||
}
|
|
||||||
state.fileIndex.set(path, file);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteFile(path) {
|
|
||||||
const parts = normalizeRelativePath(path).split("/");
|
|
||||||
const fileName = parts.pop();
|
|
||||||
let directory = state.directoryHandle;
|
|
||||||
for (const part of parts) directory = await directory.getDirectoryHandle(part, { create: false });
|
|
||||||
try {
|
|
||||||
await directory.removeEntry(fileName);
|
|
||||||
} catch (error) {
|
|
||||||
if (error?.name !== "NotFoundError") throw error;
|
|
||||||
}
|
|
||||||
state.fileIndex.delete(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function rollbackChanges(backups) {
|
|
||||||
const entries = [...backups.entries()].reverse();
|
|
||||||
for (const [path, backup] of entries) {
|
|
||||||
if (backup.existed) await writeFile(path, backup.bytes);
|
|
||||||
else await deleteFile(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistChanges(payload = {}) {
|
|
||||||
const changes = Array.isArray(payload.changes) ? payload.changes : [];
|
|
||||||
if (changes.length === 0) return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
|
||||||
if (changes.length > MAX_CHANGE_COUNT) throw new Error(`一次保存不能超过 ${MAX_CHANGE_COUNT} 个文件`);
|
|
||||||
|
|
||||||
const normalized = changes.map((change) => {
|
|
||||||
const path = assertWritableChangePath(state.manifestDirectory, change.path);
|
|
||||||
const operation = change.operate === "delete" ? "delete" : "put";
|
|
||||||
const content = operation === "put" ? String(change.content ?? "") : null;
|
|
||||||
if (content && new Blob([content]).size > MAX_TEXT_BYTES) throw new Error(`保存内容超过 20 MiB:${path}`);
|
|
||||||
return { path, operation, content };
|
|
||||||
});
|
|
||||||
|
|
||||||
if (state.source === "demo") {
|
|
||||||
for (const change of normalized) {
|
|
||||||
if (change.operation === "delete") state.memoryFiles.delete(change.path);
|
|
||||||
else state.memoryFiles.set(change.path, change.content);
|
|
||||||
}
|
|
||||||
setSaveState("刚刚保存到内存", "memory");
|
|
||||||
addActivity(`已在内存保存 ${normalized.length} 项变更`, "success");
|
|
||||||
return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.readOnlyFallback || !state.directoryHandle) {
|
|
||||||
throw new Error("当前是只读兼容模式,请用支持文件夹授权的 Chromium 浏览器打开");
|
|
||||||
}
|
|
||||||
|
|
||||||
const permission = await state.directoryHandle.queryPermission({ mode: "readwrite" });
|
|
||||||
if (permission !== "granted") throw new Error("项目文件夹的写入权限已经失效");
|
|
||||||
|
|
||||||
setSaveState("正在保存…", "saving");
|
|
||||||
const backups = new Map();
|
|
||||||
try {
|
|
||||||
for (const change of normalized) backups.set(change.path, await readBackup(change.path));
|
|
||||||
for (const change of normalized) {
|
|
||||||
if (change.operation === "delete") await deleteFile(change.path);
|
|
||||||
else await writeFile(change.path, change.content);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
await rollbackChanges(backups).catch((rollbackError) => {
|
|
||||||
addActivity(`回滚未完全成功:${rollbackError.message}`, "error");
|
|
||||||
});
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
setSaveState("刚刚已保存", "saved");
|
|
||||||
addActivity(`已保存 ${normalized.length} 个文件变更`, "success");
|
|
||||||
return { fileContent: payload.fileContent, lastModifiedTime: Date.now() };
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSave(payload) {
|
|
||||||
const operation = state.saveQueue.then(() => persistChanges(payload));
|
|
||||||
state.saveQueue = operation.catch(() => undefined);
|
|
||||||
return operation.catch((error) => {
|
|
||||||
setSaveState("保存失败", "error");
|
|
||||||
addActivity(error.message, "error");
|
|
||||||
toast(`保存失败:${error.message}`, "error");
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleFullscreen(value) {
|
|
||||||
if (value === false || document.fullscreenElement) {
|
|
||||||
if (document.fullscreenElement) await document.exitFullscreen();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await document.documentElement.requestFullscreen();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleError(error, context = "操作失败") {
|
|
||||||
const message = error?.message || String(error);
|
|
||||||
setLoading(false);
|
|
||||||
addActivity(`${context}:${message}`, "error");
|
|
||||||
toast(`${context}:${message}`, "error");
|
|
||||||
console.error(context, error);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function connectEditor() {
|
|
||||||
setConnection("connecting", "连接中");
|
|
||||||
addActivity("正在连接公开 neo-ppt 编辑器");
|
|
||||||
elements.frame.src = `${EDITOR_ORIGIN}/neo-ppt/?${editorQuery()}`;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const [{ i: connect, r: WindowMessenger }] = await Promise.all([
|
|
||||||
import(PENPAL_MODULE),
|
|
||||||
new Promise((resolve, reject) => {
|
|
||||||
const timeout = setTimeout(() => reject(new Error("编辑器 iframe 加载超时")), 20_000);
|
|
||||||
elements.frame.addEventListener(
|
|
||||||
"load",
|
|
||||||
() => {
|
|
||||||
clearTimeout(timeout);
|
|
||||||
resolve();
|
|
||||||
},
|
|
||||||
{ once: true },
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const messenger = new WindowMessenger({
|
|
||||||
remoteWindow: elements.frame.contentWindow,
|
|
||||||
allowedOrigins: [EDITOR_ORIGIN],
|
|
||||||
});
|
|
||||||
state.connection = connect({
|
|
||||||
messenger,
|
|
||||||
methods: {
|
|
||||||
close() {
|
|
||||||
toast("当前文稿由 NeoDeck Local 托管");
|
|
||||||
},
|
|
||||||
reenter() {
|
|
||||||
return state.lastDeckPayload ? setDeck(state.lastDeckPayload, elements.documentPath.textContent) : undefined;
|
|
||||||
},
|
|
||||||
toggleFullScreen: toggleFullscreen,
|
|
||||||
showFeedback() {},
|
|
||||||
sendPrompt() {
|
|
||||||
toast("AI 提示词回传尚未接入", "warning");
|
|
||||||
},
|
|
||||||
showMessage(payload) {
|
|
||||||
const message = typeof payload === "string" ? payload : payload?.message || payload?.content;
|
|
||||||
if (message) toast(message);
|
|
||||||
},
|
|
||||||
hideMessage() {},
|
|
||||||
onSave,
|
|
||||||
getImages,
|
|
||||||
setAnnotationMode() {},
|
|
||||||
setAnnotationCurrentPage() {},
|
|
||||||
upsertAnnotation() {},
|
|
||||||
removeAnnotation() {},
|
|
||||||
clearAnnotations() {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
state.remote = await Promise.race([
|
|
||||||
state.connection.promise,
|
|
||||||
new Promise((_, reject) => setTimeout(() => reject(new Error("RPC 握手超时")), 20_000)),
|
|
||||||
]);
|
|
||||||
await state.remote.setSlideConfig({ editable: true, locale: "zh-CN", theme: "light" });
|
|
||||||
setConnection("ready", "已连接");
|
|
||||||
addActivity("编辑器 RPC 握手完成", "success");
|
|
||||||
await openDemo();
|
|
||||||
} catch (error) {
|
|
||||||
setConnection("error", "连接失败");
|
|
||||||
setLoading(true, "无法连接编辑器", "请检查网络,或 Kimi 是否更新了公开前端资源。");
|
|
||||||
handleError(error, "编辑器连接失败");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function setActivityPanel(open) {
|
|
||||||
elements.activityPanel.classList.toggle("is-open", open);
|
|
||||||
elements.activityPanel.setAttribute("aria-hidden", String(!open));
|
|
||||||
}
|
|
||||||
|
|
||||||
function setOpenDialog(open) {
|
|
||||||
if (open && !elements.openDialog.open) elements.openDialog.showModal();
|
|
||||||
if (!open && elements.openDialog.open) elements.openDialog.close();
|
|
||||||
elements.openDialog.dataset.dropState = "idle";
|
|
||||||
elements.uploadDropzone.classList.remove("is-dragging");
|
|
||||||
}
|
|
||||||
|
|
||||||
elements.openFolder.addEventListener("click", () => setOpenDialog(true));
|
|
||||||
elements.closeOpenDialog.addEventListener("click", () => setOpenDialog(false));
|
|
||||||
elements.chooseWritableFolder.addEventListener("click", pickDirectory);
|
|
||||||
elements.uploadFolder.addEventListener("click", () => elements.folderFallback.click());
|
|
||||||
elements.uploadDropzone.addEventListener("click", () => elements.folderFallback.click());
|
|
||||||
elements.openDialog.addEventListener("click", (event) => {
|
|
||||||
if (event.target === elements.openDialog) setOpenDialog(false);
|
|
||||||
});
|
|
||||||
elements.openDemo.addEventListener("click", () => openDemo().catch((error) => handleError(error, "示例载入失败")));
|
|
||||||
elements.reload.addEventListener("click", () => {
|
|
||||||
if (!state.lastDeckPayload) return;
|
|
||||||
const reload = state.source === "directory" ? loadManifest(state.manifestPath, elements.documentPath.textContent) : setDeck(state.lastDeckPayload, elements.documentPath.textContent);
|
|
||||||
reload.catch((error) => handleError(error, "重新载入失败"));
|
|
||||||
});
|
|
||||||
elements.toggleActivity.addEventListener("click", () => setActivityPanel(!elements.activityPanel.classList.contains("is-open")));
|
|
||||||
elements.closeActivity.addEventListener("click", () => setActivityPanel(false));
|
|
||||||
elements.folderFallback.addEventListener("change", () => {
|
|
||||||
if (elements.folderFallback.files?.length) {
|
|
||||||
setOpenDialog(false);
|
|
||||||
openFallbackFiles(elements.folderFallback.files).catch((error) => handleError(error, "上传文件夹失败"));
|
|
||||||
}
|
|
||||||
elements.folderFallback.value = "";
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("dragenter", (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
state.dragDepth += 1;
|
|
||||||
setOpenDialog(true);
|
|
||||||
elements.openDialog.dataset.dropState = "active";
|
|
||||||
elements.uploadDropzone.classList.add("is-dragging");
|
|
||||||
});
|
|
||||||
window.addEventListener("dragover", (event) => event.preventDefault());
|
|
||||||
window.addEventListener("dragleave", () => {
|
|
||||||
state.dragDepth = Math.max(0, state.dragDepth - 1);
|
|
||||||
if (state.dragDepth === 0) {
|
|
||||||
elements.openDialog.dataset.dropState = "idle";
|
|
||||||
elements.uploadDropzone.classList.remove("is-dragging");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
window.addEventListener("drop", async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
state.dragDepth = 0;
|
|
||||||
elements.openDialog.dataset.dropState = "loading";
|
|
||||||
elements.uploadDropzone.classList.remove("is-dragging");
|
|
||||||
try {
|
|
||||||
const handles = await Promise.all([...event.dataTransfer.items].map((item) => item.getAsFileSystemHandle?.()));
|
|
||||||
if (handles.some(Boolean)) await openDroppedFolder(handles);
|
|
||||||
else if (event.dataTransfer.files?.length) {
|
|
||||||
const files = [...event.dataTransfer.files];
|
|
||||||
const roots = new Set(files.map((file) => file.webkitRelativePath?.split("/")[0]).filter(Boolean));
|
|
||||||
const isSingleFolder = files.length > 0 && roots.size === 1 && files.every((file) => file.webkitRelativePath?.includes("/"));
|
|
||||||
if (!isSingleFolder) throw new Error("只能拖入一个完整的 PPTD 项目文件夹,不能单独拖入 .pptd 文件");
|
|
||||||
setOpenDialog(false);
|
|
||||||
await openFallbackFiles(files);
|
|
||||||
} else throw new Error("请拖入包含 .pptd、pages 和 media 的完整项目文件夹");
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, "拖放打开失败");
|
|
||||||
} finally {
|
|
||||||
elements.openDialog.dataset.dropState = "idle";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("beforeunload", (event) => {
|
|
||||||
if (elements.saveState.dataset.kind === "saving") {
|
|
||||||
event.preventDefault();
|
|
||||||
event.returnValue = "";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
window.neoDeck = {
|
|
||||||
openDemo,
|
|
||||||
getSlideStatus: () => state.remote?.getSlideStatus(),
|
|
||||||
get status() {
|
|
||||||
return {
|
|
||||||
connected: Boolean(state.remote),
|
|
||||||
source: state.source,
|
|
||||||
title: state.deckTitle,
|
|
||||||
manifestPath: state.manifestPath,
|
|
||||||
indexedFiles: state.fileIndex.size,
|
|
||||||
saveState: elements.saveState.dataset.kind,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
connectEditor();
|
|
||||||
+98
-143
@@ -1,150 +1,105 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN" class="light">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
<meta name="color-scheme" content="light" />
|
<meta name="color-scheme" content="light only" />
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="一个可打开、编辑并保存本地 PPTD 项目的 neo-ppt 第三方宿主。"
|
|
||||||
/>
|
|
||||||
<title>NeoDeck Local</title>
|
<title>NeoDeck Local</title>
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
<link rel="icon" href="./neo-ppt/favicon-kimi.ico" />
|
||||||
</head>
|
<style>
|
||||||
<body>
|
/* Avoid dark FOUC before Vue theme init (official default is system → OS dark). */
|
||||||
<div class="app-shell">
|
html, body { background: #ffffff; color-scheme: light; }
|
||||||
<header class="topbar">
|
</style>
|
||||||
<div class="brand" aria-label="NeoDeck Local">
|
<!-- Official neo-ppt assets (local mirror) -->
|
||||||
<span class="brand-mark">N</span>
|
<script type="module" crossorigin src="./neo-ppt/assets/polyfills-CXLN5T9E.js"></script>
|
||||||
<span class="brand-copy">
|
<script>
|
||||||
<strong>NeoDeck</strong>
|
(function () {
|
||||||
<small>LOCAL</small>
|
// Official app defaults theme to "system" and may flip to dark on mount.
|
||||||
</span>
|
// Lock light mode before the Vue bundle runs.
|
||||||
</div>
|
try {
|
||||||
|
localStorage.setItem("CUSTOM_THEME", JSON.stringify("light"));
|
||||||
|
} catch (_) {}
|
||||||
|
window.INJECT_ENV = Object.assign({}, window.INJECT_ENV, { theme: "light" });
|
||||||
|
document.documentElement.classList.remove("dark");
|
||||||
|
document.documentElement.classList.add("light");
|
||||||
|
// Completely offline: block cloud APIs early
|
||||||
|
const blocked = [
|
||||||
|
/\/apiv2\//,
|
||||||
|
/www\.kimi\.com/,
|
||||||
|
/kimi\.ai/,
|
||||||
|
/slides\.kimi\./,
|
||||||
|
/gator\.volces\.com/,
|
||||||
|
/volces\.com/,
|
||||||
|
/byteimg\.com\/obj\/.*log/,
|
||||||
|
/\/list$/,
|
||||||
|
/accounts\.google\.com/,
|
||||||
|
/googleapis\.com/,
|
||||||
|
/docs\.google\.com/,
|
||||||
|
];
|
||||||
|
const nativeFetch = window.fetch.bind(window);
|
||||||
|
window.fetch = function (input, init) {
|
||||||
|
const url = typeof input === "string" ? input : input && input.url;
|
||||||
|
if (url && blocked.some((re) => re.test(String(url)))) {
|
||||||
|
return Promise.resolve(
|
||||||
|
new Response(JSON.stringify({ signature: "offline-bypass", algorithm: "Ed25519", offline: true }), {
|
||||||
|
status: 200,
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return nativeFetch(input, init);
|
||||||
|
};
|
||||||
|
const XO = XMLHttpRequest.prototype.open;
|
||||||
|
XMLHttpRequest.prototype.open = function (method, url) {
|
||||||
|
this.__ndUrl = url;
|
||||||
|
return XO.apply(this, arguments);
|
||||||
|
};
|
||||||
|
const XS = XMLHttpRequest.prototype.send;
|
||||||
|
XMLHttpRequest.prototype.send = function () {
|
||||||
|
if (this.__ndUrl && blocked.some((re) => re.test(String(this.__ndUrl)))) {
|
||||||
|
Object.defineProperty(this, "status", { value: 200 });
|
||||||
|
Object.defineProperty(this, "responseText", { value: '{"offline":true}' });
|
||||||
|
this.onload && this.onload();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return XS.apply(this, arguments);
|
||||||
|
};
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<!-- Local bridge MUST load before official app (defines __NEODECK_CONNECT__) -->
|
||||||
|
<script type="module" src="./local-bridge.js"></script>
|
||||||
|
<script type="module" crossorigin src="./neo-ppt/assets/index-jtNAhQeK.js"></script>
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/rolldown-runtime-DWdDZTNf.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/vue-B11_pI9F.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/common-utils-G6HD4Ji1.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/_plugin-vue_export-helper-B67ILkmu.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/preload-helper-NECkhNH4.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/kimi-DRwOD7EI.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/icons-CgDhqj4h.js" />
|
||||||
|
<link rel="modulepreload" crossorigin href="./neo-ppt/assets/const-BjfH-jZe.js" />
|
||||||
|
<link rel="stylesheet" crossorigin href="./neo-ppt/assets/index-Bn1xM_xZ.css" />
|
||||||
|
<link rel="stylesheet" href="./local-shell.css" />
|
||||||
|
|
||||||
<div class="toolbar" aria-label="文件工具栏">
|
<style id="neodeck-layout-fix">
|
||||||
<button class="button button-primary" id="open-folder" type="button">
|
/* editor-app height fix: avoid 100vh under local topbar */
|
||||||
<span class="button-icon" aria-hidden="true">+</span>
|
.editor-app[data-v-ddddc63d] { height: 100% !important; width: 100% !important; }
|
||||||
打开 PPTD 文件夹
|
.editor-app .editor-content[data-v-ddddc63d] { height: calc(100% - var(--topbar-height, 52px)) !important; }
|
||||||
</button>
|
.feedback-snackbar-wrap[data-v-ddddc63d] { z-index: 20000 !important; }
|
||||||
<button class="button" id="open-demo" type="button">示例</button>
|
</style>
|
||||||
<button class="icon-button" id="reload-deck" type="button" title="重新载入当前文稿" disabled>
|
</head>
|
||||||
↻
|
<body class="neodeck-local">
|
||||||
</button>
|
<header class="nd-topbar" id="nd-topbar">
|
||||||
</div>
|
<div class="nd-brand"><strong>NeoDeck</strong><small>LOCAL</small></div>
|
||||||
|
<div class="nd-actions">
|
||||||
<div class="document-meta" aria-live="polite">
|
<button type="button" class="nd-btn nd-btn-primary" id="nd-open">打开 PPTD 文件夹</button>
|
||||||
<div class="document-title" id="document-title">等待编辑器连接</div>
|
|
||||||
<div class="document-path" id="document-path">当前使用演示文稿</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="topbar-actions">
|
|
||||||
<span class="status-pill status-connecting" id="connection-status">
|
|
||||||
<span class="status-dot"></span>
|
|
||||||
<span id="connection-label">连接中</span>
|
|
||||||
</span>
|
|
||||||
<span class="save-state" id="save-state">未保存到本地</span>
|
|
||||||
<button class="icon-button" id="toggle-activity" type="button" title="打开活动记录">☰</button>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main class="editor-stage">
|
|
||||||
<iframe
|
|
||||||
id="editor"
|
|
||||||
title="neo-ppt 编辑器"
|
|
||||||
allow="fullscreen; clipboard-read; clipboard-write"
|
|
||||||
></iframe>
|
|
||||||
|
|
||||||
<section class="loading-card" id="loading-card" aria-live="polite">
|
|
||||||
<span class="loading-orbit" aria-hidden="true"></span>
|
|
||||||
<h1 id="loading-title">正在连接 neo-ppt</h1>
|
|
||||||
<p id="loading-message">编辑器就绪后会自动载入示例文稿。</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="activity-panel" id="activity-panel" aria-hidden="true">
|
|
||||||
<div class="activity-header">
|
|
||||||
<div>
|
|
||||||
<strong>活动记录</strong>
|
|
||||||
<small>连接、加载与保存状态</small>
|
|
||||||
</div>
|
|
||||||
<button class="icon-button" id="close-activity" type="button" aria-label="关闭活动记录">×</button>
|
|
||||||
</div>
|
|
||||||
<ol class="activity-list" id="activity-list"></ol>
|
|
||||||
<div class="activity-footer">
|
|
||||||
文件只会在你授权的项目文件夹内读写;远程编辑器不会获得任意磁盘访问权限。
|
|
||||||
</div>
|
|
||||||
</aside>
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<dialog class="open-dialog" id="open-dialog" aria-labelledby="open-dialog-title">
|
|
||||||
<div class="open-dialog-shell">
|
|
||||||
<div class="open-dialog-header">
|
|
||||||
<div class="dialog-heading">
|
|
||||||
<span class="dialog-mark">P</span>
|
|
||||||
<div>
|
|
||||||
<h2 id="open-dialog-title">打开 PPTD 项目文件夹</h2>
|
|
||||||
<p>请选择或拖入包含完整项目结构的文件夹</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button class="icon-button" id="close-open-dialog" type="button" aria-label="关闭打开文稿弹窗">×</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button class="upload-dropzone" id="upload-dropzone" type="button">
|
|
||||||
<span class="upload-illustration" aria-hidden="true">
|
|
||||||
<span class="upload-file">P</span>
|
|
||||||
<span class="upload-arrow">↑</span>
|
|
||||||
</span>
|
|
||||||
<strong>拖动 PPTD 项目文件夹到这里</strong>
|
|
||||||
<span>仅接受一个完整文件夹,不支持单独的 .pptd 文件</span>
|
|
||||||
<em>点击上传完整文件夹(只读)</em>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div class="open-choice-row">
|
|
||||||
<button class="open-choice open-choice-primary" id="choose-writable-folder" type="button">
|
|
||||||
<span class="choice-icon" aria-hidden="true">▣</span>
|
|
||||||
<span>
|
|
||||||
<strong>选择项目文件夹</strong>
|
|
||||||
<small>授权后支持自动保存</small>
|
|
||||||
</span>
|
|
||||||
<span aria-hidden="true">›</span>
|
|
||||||
</button>
|
|
||||||
<button class="open-choice" id="upload-folder" type="button">
|
|
||||||
<span class="choice-icon choice-icon-muted" aria-hidden="true">↑</span>
|
|
||||||
<span>
|
|
||||||
<strong>上传项目文件夹</strong>
|
|
||||||
<small>兼容模式,只读打开</small>
|
|
||||||
</span>
|
|
||||||
<span aria-hidden="true">›</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="open-dialog-note">
|
|
||||||
<span aria-hidden="true">ⓘ</span>
|
|
||||||
<span>PPTD 项目由 .pptd 清单、pages 页面目录和 media 素材目录组成,必须上传整个文件夹。若提示含系统文件,请移除 .DS_Store。</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
<div class="nd-meta">
|
||||||
|
<span id="nd-title">未打开文稿</span>
|
||||||
<dialog class="deck-dialog" id="deck-dialog">
|
<span id="nd-status" class="nd-status">初始化中</span>
|
||||||
<form method="dialog">
|
</div>
|
||||||
<div class="dialog-heading">
|
<input type="file" id="nd-folder" webkitdirectory directory multiple hidden />
|
||||||
<span class="dialog-mark">P</span>
|
</header>
|
||||||
<div>
|
<div id="app"></div>
|
||||||
<h2>选择 PPTD 文稿</h2>
|
<div id="nd-toast" class="nd-toast" hidden></div>
|
||||||
<p>这个文件夹里发现了多个 .pptd 清单,请选择一个。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="deck-options" id="deck-options"></div>
|
|
||||||
<div class="dialog-actions">
|
|
||||||
<button class="button" value="cancel">取消</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</dialog>
|
|
||||||
|
|
||||||
<input id="folder-fallback" type="file" webkitdirectory multiple hidden />
|
|
||||||
<div class="toast-region" id="toast-region" aria-live="polite"></div>
|
|
||||||
|
|
||||||
<script type="module" src="./app.js"></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
/**
|
||||||
|
* NeoDeck Local bridge — no iframe, no cloud.
|
||||||
|
* Official neo-ppt talks to us via window.__NEODECK_CONNECT__ instead of Penpal parent.
|
||||||
|
*/
|
||||||
|
import {
|
||||||
|
basename,
|
||||||
|
dirname,
|
||||||
|
extractPagePaths,
|
||||||
|
joinDeckPath,
|
||||||
|
normalizeRelativePath,
|
||||||
|
titleFromManifest,
|
||||||
|
} from "./lib.js";
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
editor: null, // methods exposed by official editor (setPPTD, ...)
|
||||||
|
directoryHandle: null,
|
||||||
|
fileIndex: new Map(),
|
||||||
|
memoryFiles: new Map(),
|
||||||
|
imageMap: Object.create(null), // path → data URL (export host / payload)
|
||||||
|
manifestPath: "",
|
||||||
|
manifestDirectory: "",
|
||||||
|
manifestContent: "",
|
||||||
|
deckTitle: "未打开文稿",
|
||||||
|
saveQueue: Promise.resolve(),
|
||||||
|
imageCache: new Map(),
|
||||||
|
readOnly: false,
|
||||||
|
ready: false,
|
||||||
|
exportMode: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const $ = (sel) => document.querySelector(sel);
|
||||||
|
const exportMode =
|
||||||
|
new URL(location.href).searchParams.get("ndExport") === "1" ||
|
||||||
|
new URL(location.href).searchParams.get("export") === "1";
|
||||||
|
state.exportMode = exportMode;
|
||||||
|
if (exportMode) {
|
||||||
|
document.documentElement.classList.add("nd-export-mode");
|
||||||
|
document.documentElement.dataset.deckStatus = "booting";
|
||||||
|
}
|
||||||
|
|
||||||
|
function toast(msg, kind = "info") {
|
||||||
|
const el = $("#nd-toast");
|
||||||
|
if (!el) return;
|
||||||
|
el.hidden = false;
|
||||||
|
el.dataset.kind = kind;
|
||||||
|
el.textContent = msg;
|
||||||
|
clearTimeout(toast._t);
|
||||||
|
toast._t = setTimeout(() => {
|
||||||
|
el.hidden = true;
|
||||||
|
}, 3200);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(text) {
|
||||||
|
const el = $("#nd-status");
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setTitle(text) {
|
||||||
|
state.deckTitle = text;
|
||||||
|
const el = $("#nd-title");
|
||||||
|
if (el) el.textContent = text;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function indexDirectory(directoryHandle) {
|
||||||
|
const index = new Map();
|
||||||
|
async function walk(handle, prefix = "") {
|
||||||
|
for await (const [name, entry] of handle.entries()) {
|
||||||
|
if (name === ".DS_Store") continue;
|
||||||
|
const path = prefix ? `${prefix}/${name}` : name;
|
||||||
|
if (entry.kind === "directory") await walk(entry, path);
|
||||||
|
else index.set(normalizeRelativePath(path), entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await walk(directoryHandle);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function indexFallbackFiles(fileList) {
|
||||||
|
const index = new Map();
|
||||||
|
const files = [...fileList];
|
||||||
|
const firstPath = files[0]?.webkitRelativePath || files[0]?.name || "";
|
||||||
|
const rootName = firstPath.includes("/") ? firstPath.split("/")[0] : "";
|
||||||
|
for (const file of files) {
|
||||||
|
let path = file.webkitRelativePath || file.name;
|
||||||
|
if (rootName && path.startsWith(`${rootName}/`)) path = path.slice(rootName.length + 1);
|
||||||
|
index.set(normalizeRelativePath(path), { kind: "file", getFile: async () => file });
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function textFromIndexed(path) {
|
||||||
|
const key = normalizeRelativePath(path);
|
||||||
|
if (state.memoryFiles.has(key)) return state.memoryFiles.get(key);
|
||||||
|
const entry = state.fileIndex.get(key);
|
||||||
|
if (!entry) throw new Error(`找不到文件:${path}`);
|
||||||
|
return (await entry.getFile()).text();
|
||||||
|
}
|
||||||
|
|
||||||
|
function fileToDataUrl(file) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const reader = new FileReader();
|
||||||
|
reader.onload = () => resolve(reader.result);
|
||||||
|
reader.onerror = () => reject(reader.error);
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function lookupImageMap(path) {
|
||||||
|
if (!path || !state.imageMap) return "";
|
||||||
|
if (state.imageMap[path]) return state.imageMap[path];
|
||||||
|
const keys = Object.keys(state.imageMap);
|
||||||
|
const hit = keys.find(
|
||||||
|
(key) => path.endsWith(`/${key}`) || key.endsWith(`/${path}`) || key === path || key.endsWith(`/${path.split("/").pop()}`),
|
||||||
|
);
|
||||||
|
return hit ? state.imageMap[hit] : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveImage(requestedPath) {
|
||||||
|
if (requestedPath == null || requestedPath === "") return "";
|
||||||
|
const raw = String(requestedPath);
|
||||||
|
if (/^(?:data:image\/|https?:\/\/|blob:)/i.test(raw)) return raw;
|
||||||
|
|
||||||
|
let path;
|
||||||
|
try {
|
||||||
|
path = normalizeRelativePath(raw.replace(/^file:\/\/+/, "").replace(/^\.\//, ""));
|
||||||
|
} catch {
|
||||||
|
path = raw.replace(/^file:\/\/+/, "").replace(/^\.\//, "").replaceAll("\\", "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mapped = lookupImageMap(path);
|
||||||
|
if (mapped) return mapped;
|
||||||
|
|
||||||
|
const candidates = [];
|
||||||
|
const add = (p) => {
|
||||||
|
if (p && !candidates.includes(p)) candidates.push(p);
|
||||||
|
};
|
||||||
|
add(path);
|
||||||
|
if (state.manifestDirectory) {
|
||||||
|
try {
|
||||||
|
add(joinDeckPath(state.manifestDirectory, path));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// common media layouts
|
||||||
|
const base = path.split("/").pop();
|
||||||
|
if (base) {
|
||||||
|
add(`media/${base}`);
|
||||||
|
add(`assets/${base}`);
|
||||||
|
add(`images/${base}`);
|
||||||
|
if (state.manifestDirectory) {
|
||||||
|
try {
|
||||||
|
add(joinDeckPath(state.manifestDirectory, `media/${base}`));
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// fuzzy: any indexed file ending with same relative suffix or basename
|
||||||
|
for (const key of state.fileIndex.keys()) {
|
||||||
|
if (key === path || key.endsWith("/" + path) || (base && key.endsWith("/" + base)) || key === base) {
|
||||||
|
add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const c of candidates) {
|
||||||
|
if (!state.fileIndex.has(c) && !state.memoryFiles.has(c)) continue;
|
||||||
|
const cacheKey = c;
|
||||||
|
if (!state.imageCache.has(cacheKey)) {
|
||||||
|
try {
|
||||||
|
if (state.fileIndex.has(c)) {
|
||||||
|
const file = await state.fileIndex.get(c).getFile();
|
||||||
|
state.imageCache.set(cacheKey, await fileToDataUrl(file));
|
||||||
|
} else {
|
||||||
|
// memory text shouldn't be image; skip
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[neodeck] image read failed", c, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const url = await state.imageCache.get(cacheKey);
|
||||||
|
if (url) return url;
|
||||||
|
}
|
||||||
|
console.warn("[neodeck] image not found", requestedPath, "tried", candidates.slice(0, 8));
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getImages(payload = {}) {
|
||||||
|
// Official contract: { chatId, filePath: string[] } -> string[] (data URLs / public URLs)
|
||||||
|
let paths = payload?.filePath;
|
||||||
|
if (paths == null) paths = [];
|
||||||
|
if (!Array.isArray(paths)) paths = [paths];
|
||||||
|
const out = [];
|
||||||
|
for (const p of paths) {
|
||||||
|
try {
|
||||||
|
out.push(await resolveImage(p));
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[neodeck] getImages item failed", p, e);
|
||||||
|
out.push("");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeIndexedFile(path, content) {
|
||||||
|
if (state.readOnly || !state.directoryHandle) {
|
||||||
|
state.memoryFiles.set(path, content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const parts = path.split("/");
|
||||||
|
let dir = state.directoryHandle;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
dir = await dir.getDirectoryHandle(parts[i], { create: true });
|
||||||
|
}
|
||||||
|
const handle = await dir.getFileHandle(parts[parts.length - 1], { create: true });
|
||||||
|
const writable = await handle.createWritable();
|
||||||
|
await writable.write(content);
|
||||||
|
await writable.close();
|
||||||
|
state.fileIndex.set(path, handle);
|
||||||
|
state.memoryFiles.set(path, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave(payload) {
|
||||||
|
const changes = payload?.changes || payload?.files || [];
|
||||||
|
const list = Array.isArray(changes) ? changes : [];
|
||||||
|
setStatus("正在保存…");
|
||||||
|
try {
|
||||||
|
for (const change of list) {
|
||||||
|
if (!change?.path) continue;
|
||||||
|
const path = normalizeRelativePath(change.path);
|
||||||
|
if (!/\.(?:pptd|page)$/i.test(path)) continue;
|
||||||
|
await writeIndexedFile(path, change.content ?? "");
|
||||||
|
}
|
||||||
|
setStatus(state.readOnly ? "只读 · 已存内存" : "已保存到本地");
|
||||||
|
} catch (e) {
|
||||||
|
setStatus("保存失败");
|
||||||
|
toast(`保存失败:${e.message || e}`, "error");
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Called by patched official editor instead of Penpal connect */
|
||||||
|
window.__NEODECK_CONNECT__ = function neoDeckConnect(options) {
|
||||||
|
const methods = options?.methods || {};
|
||||||
|
state.editor = methods;
|
||||||
|
state.ready = true;
|
||||||
|
setStatus("编辑器就绪");
|
||||||
|
// Force light chrome immediately (official default is system → OS dark FOUC).
|
||||||
|
Promise.resolve(
|
||||||
|
methods.setSlideConfig?.({ editable: true, locale: "zh-CN", theme: "light" }),
|
||||||
|
).catch((e) => console.warn("[neodeck] setSlideConfig", e));
|
||||||
|
// host methods the editor will call via connection.promise
|
||||||
|
const host = {
|
||||||
|
close() {
|
||||||
|
toast("本地模式无需关闭");
|
||||||
|
},
|
||||||
|
reenter() {},
|
||||||
|
async toggleFullScreen(want) {
|
||||||
|
try {
|
||||||
|
const el = document.documentElement;
|
||||||
|
const isFs = Boolean(document.fullscreenElement);
|
||||||
|
if (want === true || (want !== false && !isFs)) {
|
||||||
|
if (!isFs && el.requestFullscreen) await el.requestFullscreen();
|
||||||
|
} else if (want === false || isFs) {
|
||||||
|
if (isFs && document.exitFullscreen) await document.exitFullscreen();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[neodeck] fullscreen", e);
|
||||||
|
}
|
||||||
|
return Boolean(document.fullscreenElement);
|
||||||
|
},
|
||||||
|
showFeedback() {},
|
||||||
|
sendPrompt() {
|
||||||
|
toast("本地模式已禁用 AI", "warning");
|
||||||
|
},
|
||||||
|
showMessage(payload) {
|
||||||
|
const message = typeof payload === "string" ? payload : payload?.message || payload?.content;
|
||||||
|
if (message) toast(String(message));
|
||||||
|
},
|
||||||
|
hideMessage() {},
|
||||||
|
onSave,
|
||||||
|
getImages,
|
||||||
|
setAnnotationMode() {},
|
||||||
|
setAnnotationCurrentPage() {},
|
||||||
|
upsertAnnotation() {},
|
||||||
|
removeAnnotation() {},
|
||||||
|
clearAnnotations() {},
|
||||||
|
};
|
||||||
|
|
||||||
|
if (state.exportMode) {
|
||||||
|
loadExportPayload().catch((error) => {
|
||||||
|
console.error("[neodeck] export payload failed", error);
|
||||||
|
document.documentElement.dataset.deckStatus = "error";
|
||||||
|
window.exportHostError = String(error?.stack || error);
|
||||||
|
setStatus("导出载荷加载失败");
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Auto-open demo if nothing loaded after a beat
|
||||||
|
setTimeout(() => {
|
||||||
|
if (!state.manifestPath) openDemo().catch((e) => console.error(e));
|
||||||
|
}, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise: Promise.resolve(host),
|
||||||
|
destroy() {
|
||||||
|
state.editor = null;
|
||||||
|
state.ready = false;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Headless / agent-browser export: load deck from ./payload.json (no folder picker). */
|
||||||
|
async function loadExportPayload() {
|
||||||
|
document.documentElement.dataset.deckStatus = "loading";
|
||||||
|
setStatus("载入导出载荷…");
|
||||||
|
const response = await fetch("./payload.json", { cache: "no-store" });
|
||||||
|
if (!response.ok) throw new Error(`payload HTTP ${response.status}`);
|
||||||
|
const payload = await response.json();
|
||||||
|
if (!state.editor?.setPPTD) throw new Error("编辑器尚未就绪");
|
||||||
|
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.readOnly = true;
|
||||||
|
state.fileIndex = new Map();
|
||||||
|
state.memoryFiles = new Map();
|
||||||
|
state.imageCache = new Map();
|
||||||
|
state.imageMap = Object.create(null);
|
||||||
|
for (const [key, value] of Object.entries(payload.imageMap || {})) {
|
||||||
|
try {
|
||||||
|
state.imageMap[normalizeRelativePath(key)] = value;
|
||||||
|
} catch {
|
||||||
|
state.imageMap[String(key).replaceAll("\\", "/")] = value;
|
||||||
|
}
|
||||||
|
state.imageCache.set(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const manifestPath = payload.manifestPath || "deck.pptd";
|
||||||
|
state.manifestPath = manifestPath;
|
||||||
|
state.manifestDirectory = dirname(manifestPath);
|
||||||
|
state.manifestContent = payload.manifestContent || "";
|
||||||
|
state.memoryFiles.set(normalizeRelativePath(manifestPath), state.manifestContent);
|
||||||
|
for (const page of payload.pages || []) {
|
||||||
|
if (!page?.path) continue;
|
||||||
|
state.memoryFiles.set(normalizeRelativePath(page.path), page.content ?? "");
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = payload.title || titleFromManifest(state.manifestContent, basename(manifestPath));
|
||||||
|
setTitle(title);
|
||||||
|
|
||||||
|
await state.editor.setSlideConfig?.({
|
||||||
|
editable: true,
|
||||||
|
locale: "zh-CN",
|
||||||
|
theme: "light",
|
||||||
|
slideId: payload.id,
|
||||||
|
});
|
||||||
|
await state.editor.setPPTD(payload.id || `export-${Date.now()}`, {
|
||||||
|
pptdContent: state.manifestContent,
|
||||||
|
pages: payload.pages || [],
|
||||||
|
pptdPath: manifestPath,
|
||||||
|
basePath: "",
|
||||||
|
isCreate: false,
|
||||||
|
});
|
||||||
|
await state.editor.setEditable?.(true);
|
||||||
|
|
||||||
|
window.exportRemote = state.editor;
|
||||||
|
try {
|
||||||
|
window.exportSlideStatus = await state.editor.getSlideStatus?.();
|
||||||
|
} catch {
|
||||||
|
window.exportSlideStatus = null;
|
||||||
|
}
|
||||||
|
setStatus(`导出模式 · ${title}`);
|
||||||
|
document.documentElement.dataset.deckStatus = "ready";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadDeckFromIndex(manifestPath, sourceLabel, options = {}) {
|
||||||
|
if (typeof options === "boolean") {
|
||||||
|
options = { readOnly: options, editable: !options };
|
||||||
|
}
|
||||||
|
const readOnly = Boolean(options.readOnly);
|
||||||
|
const editable = options.editable !== false;
|
||||||
|
if (!state.editor?.setPPTD) throw new Error("编辑器尚未就绪");
|
||||||
|
const manifestContent = await textFromIndexed(manifestPath);
|
||||||
|
const pagePaths = extractPagePaths(manifestContent);
|
||||||
|
const pages = [];
|
||||||
|
const missing = [];
|
||||||
|
const manifestDirectory = dirname(manifestPath);
|
||||||
|
for (const pagePath of pagePaths.slice(0, 500)) {
|
||||||
|
const indexedPath = joinDeckPath(manifestDirectory, pagePath);
|
||||||
|
try {
|
||||||
|
pages.push({ path: pagePath, content: await textFromIndexed(indexedPath) });
|
||||||
|
} catch {
|
||||||
|
missing.push(pagePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (missing.length) throw new Error(`缺少页面:${missing.slice(0, 5).join(", ")}`);
|
||||||
|
const title = titleFromManifest(manifestContent, basename(manifestPath));
|
||||||
|
state.manifestPath = manifestPath;
|
||||||
|
state.manifestDirectory = manifestDirectory;
|
||||||
|
state.manifestContent = manifestContent;
|
||||||
|
state.readOnly = readOnly;
|
||||||
|
setTitle(title);
|
||||||
|
setStatus(sourceLabel);
|
||||||
|
|
||||||
|
// isCreate:true leaves the official UI in a "generating / loading" state and
|
||||||
|
// disables export / present until generate_end — use false for local opens.
|
||||||
|
await state.editor.setPPTD(`local-${Date.now()}`, {
|
||||||
|
pptdContent: manifestContent,
|
||||||
|
pages,
|
||||||
|
basePath: "",
|
||||||
|
pptdPath: manifestPath,
|
||||||
|
isCreate: false,
|
||||||
|
});
|
||||||
|
await state.editor.setEditable?.(editable);
|
||||||
|
await state.editor.setSlideConfig?.({ editable, locale: "zh-CN", theme: "light" });
|
||||||
|
toast(`已载入「${title}」· ${pages.length} 页`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDemo() {
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.fileIndex.clear();
|
||||||
|
state.imageCache.clear();
|
||||||
|
state.memoryFiles.clear();
|
||||||
|
const manifest = JSON.stringify({
|
||||||
|
version: "v2",
|
||||||
|
title: "NeoDeck Local",
|
||||||
|
size: [960, 540],
|
||||||
|
pages: ["pages/01.page", "pages/02.page"],
|
||||||
|
});
|
||||||
|
const page1 = JSON.stringify({
|
||||||
|
pageType: "content",
|
||||||
|
background: { color: "#f7f8fc" },
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementId: "title",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [80, 160, 800, 120],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:48px;color:#171923;font-weight:700">完全本地离线</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
elementId: "sub",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [80, 300, 720, 60],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:20px;color:#667085">官方编辑器镜像 · 无 iframe · 无云端接口</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const page2 = JSON.stringify({
|
||||||
|
pageType: "content",
|
||||||
|
background: { color: "#171923" },
|
||||||
|
elements: [
|
||||||
|
{
|
||||||
|
elementId: "t",
|
||||||
|
elementType: "text",
|
||||||
|
bounds: [80, 200, 800, 80],
|
||||||
|
content: {
|
||||||
|
text: '<p><span style="font-size:36px;color:#fff;font-weight:700">打开 PPTD 文件夹开始编辑</span></p>',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
state.memoryFiles.set("presentation.pptd", manifest);
|
||||||
|
state.memoryFiles.set("pages/01.page", page1);
|
||||||
|
state.memoryFiles.set("pages/02.page", page2);
|
||||||
|
state.fileIndex.clear();
|
||||||
|
// synthetic index for demo
|
||||||
|
for (const [path, content] of state.memoryFiles) {
|
||||||
|
state.fileIndex.set(path, {
|
||||||
|
kind: "file",
|
||||||
|
getFile: async () => new File([content], basename(path), { type: "text/plain" }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Editable in UI; readOnly only means "don't write to disk" (saves stay in memory).
|
||||||
|
await loadDeckFromIndex("presentation.pptd", "内置示例 · 内存", {
|
||||||
|
readOnly: true,
|
||||||
|
editable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openDirectoryHandle(handle) {
|
||||||
|
setStatus("扫描文件夹…");
|
||||||
|
state.directoryHandle = handle;
|
||||||
|
state.fileIndex = await indexDirectory(handle);
|
||||||
|
state.imageCache.clear();
|
||||||
|
state.memoryFiles.clear();
|
||||||
|
const manifests = [...state.fileIndex.keys()].filter((p) => p.toLowerCase().endsWith(".pptd"));
|
||||||
|
if (!manifests.length) throw new Error("文件夹内没有 .pptd");
|
||||||
|
const manifestPath = manifests.length === 1 ? manifests[0] : manifests.sort()[0];
|
||||||
|
await loadDeckFromIndex(manifestPath, `${handle.name} · 可写`, {
|
||||||
|
readOnly: false,
|
||||||
|
editable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openFallbackFiles(fileList) {
|
||||||
|
setStatus("读取上传…");
|
||||||
|
state.directoryHandle = null;
|
||||||
|
state.readOnly = true;
|
||||||
|
state.fileIndex = indexFallbackFiles(fileList);
|
||||||
|
state.imageCache.clear();
|
||||||
|
state.memoryFiles.clear();
|
||||||
|
const manifests = [...state.fileIndex.keys()].filter((p) => p.toLowerCase().endsWith(".pptd"));
|
||||||
|
if (!manifests.length) throw new Error("没有 .pptd");
|
||||||
|
await loadDeckFromIndex(manifests.sort()[0], "上传 · 只读", {
|
||||||
|
readOnly: true,
|
||||||
|
editable: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function wireUi() {
|
||||||
|
$("#nd-open")?.addEventListener("click", async () => {
|
||||||
|
try {
|
||||||
|
if (window.showDirectoryPicker) {
|
||||||
|
const handle = await window.showDirectoryPicker({ mode: "readwrite" });
|
||||||
|
await openDirectoryHandle(handle);
|
||||||
|
} else {
|
||||||
|
$("#nd-folder")?.click();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
if (e?.name === "AbortError") return;
|
||||||
|
toast(e.message || String(e), "error");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$("#nd-folder")?.addEventListener("change", async (ev) => {
|
||||||
|
const files = ev.target.files;
|
||||||
|
if (!files?.length) return;
|
||||||
|
try {
|
||||||
|
await openFallbackFiles(files);
|
||||||
|
} catch (e) {
|
||||||
|
toast(e.message || String(e), "error");
|
||||||
|
}
|
||||||
|
ev.target.value = "";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force sdk query params so official app enters ppt-editor external mode
|
||||||
|
(function forceSdkQuery() {
|
||||||
|
const url = new URL(location.href);
|
||||||
|
let changed = false;
|
||||||
|
const params = {
|
||||||
|
sdkMode: "ppt-editor",
|
||||||
|
pptPlatform: "neodeck-local",
|
||||||
|
functional: JSON.stringify({
|
||||||
|
fullscreen: true,
|
||||||
|
present: true,
|
||||||
|
export: true,
|
||||||
|
close: false,
|
||||||
|
annotation: false,
|
||||||
|
feedback: false,
|
||||||
|
share: false,
|
||||||
|
versionHistory: false,
|
||||||
|
}),
|
||||||
|
sdkSaveMode: "external",
|
||||||
|
sdkImageMode: "external",
|
||||||
|
};
|
||||||
|
for (const [k, v] of Object.entries(params)) {
|
||||||
|
if (url.searchParams.get(k) !== v) {
|
||||||
|
url.searchParams.set(k, v);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) history.replaceState(null, "", url);
|
||||||
|
})();
|
||||||
|
|
||||||
|
wireUi();
|
||||||
|
setStatus("等待编辑器…");
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
|
||||||
|
html, body {
|
||||||
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||||
|
}
|
||||||
|
body.neodeck-local {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: 48px minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
/* Agent export host: hide NeoDeck chrome, full-bleed official UI */
|
||||||
|
html.nd-export-mode body.neodeck-local {
|
||||||
|
grid-template-rows: minmax(0, 1fr);
|
||||||
|
}
|
||||||
|
html.nd-export-mode .nd-topbar,
|
||||||
|
html.nd-export-mode .nd-toast {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
html.nd-export-mode .editor-app[data-v-ddddc63d] {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
html.nd-export-mode .editor-app .editor-content[data-v-ddddc63d] {
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
.nd-topbar {
|
||||||
|
z-index: 50;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-bottom: 1px solid #e6e9f0;
|
||||||
|
background: rgba(255,255,255,0.96);
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.nd-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 6px;
|
||||||
|
min-width: 96px;
|
||||||
|
}
|
||||||
|
.nd-brand strong { font-size: 14px; color: #171923; }
|
||||||
|
.nd-brand small {
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #5b4dff;
|
||||||
|
}
|
||||||
|
.nd-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
.nd-btn {
|
||||||
|
height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: 1px solid #d8dee9;
|
||||||
|
background: #fff;
|
||||||
|
color: #3a4251;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.nd-btn:hover { background: #f7f8fb; }
|
||||||
|
.nd-btn-primary {
|
||||||
|
border-color: #6c5ce7;
|
||||||
|
background: #6c5ce7;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
.nd-btn-primary:hover { background: #5b4dff; border-color: #5b4dff; }
|
||||||
|
.nd-meta {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: 2px;
|
||||||
|
min-width: 120px;
|
||||||
|
max-width: 280px;
|
||||||
|
}
|
||||||
|
#nd-title {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #171923;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
.nd-status {
|
||||||
|
font-size: 11px;
|
||||||
|
color: #7a8499;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.nd-toast {
|
||||||
|
position: fixed;
|
||||||
|
left: 50%;
|
||||||
|
bottom: 28px;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
z-index: 10001;
|
||||||
|
max-width: min(520px, 90vw);
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #171923;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 13px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
.nd-toast[data-kind="error"] { background: #c0392b; }
|
||||||
|
.nd-toast[data-kind="warning"] { background: #b7791f; }
|
||||||
|
|
||||||
|
/* Offline: hide share / cloud / Google Drive chrome from the official UI */
|
||||||
|
.share-dialog-trigger,
|
||||||
|
[class*="share-dialog"],
|
||||||
|
.export-tabs .tabs,
|
||||||
|
.export-tabs [name="google-slides"],
|
||||||
|
.export-dialog [class*="GoogleSlides"],
|
||||||
|
.export-dialog .tab[name="google-slides"] {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
.export-tabs.without-tabs .tabs {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nd-hint {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #7a8499;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Fix official editor inside local topbar shell ===== */
|
||||||
|
.nd-topbar {
|
||||||
|
z-index: 50;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
/* Official app assumes full viewport; we host it under a 48px topbar */
|
||||||
|
#app .editor-app {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
max-height: 100% !important;
|
||||||
|
}
|
||||||
|
#app .editor-app .editor-content {
|
||||||
|
height: calc(100% - var(--topbar-height, 52px)) !important;
|
||||||
|
}
|
||||||
|
/* Snackbars / toasts above local chrome */
|
||||||
|
#app .feedback-snackbar-wrap {
|
||||||
|
z-index: 20000 !important;
|
||||||
|
bottom: 24px !important;
|
||||||
|
top: auto !important;
|
||||||
|
}
|
||||||
|
#app .feedback-snackbar {
|
||||||
|
z-index: 20000 !important;
|
||||||
|
}
|
||||||
|
/* Message toasts that are fixed to viewport */
|
||||||
|
body .kimi-message,
|
||||||
|
body [class*="message-list"],
|
||||||
|
body [class*="MessageList"],
|
||||||
|
#app [class*="snackbar"] {
|
||||||
|
z-index: 20000 !important;
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
|||||||
|
.loading[data-v-cb4a6e17]{aspect-ratio:1;width:16px;height:16px;color:var(--Labels-Primary);border-radius:50%;position:relative}.loading[data-v-cb4a6e17]:after{content:"";background:conic-gradient(from 90deg,transparent 30deg,currentColor 330.922deg,transparent 330.922deg,transparent 360deg);border-radius:50%;width:calc(90% - 2px);height:calc(90% - 2px);animation:1s linear infinite spin-cb4a6e17;position:absolute;top:2px;left:2px;-webkit-mask:radial-gradient(transparent 0,transparent 57%,currentColor 57%,currentColor 80%);mask:radial-gradient(transparent 0,transparent 57%,currentColor 57%,currentColor 80%)}@keyframes spin-cb4a6e17{0%{transform:rotate(0)}to{transform:rotate(360deg)}}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{K as e,ct as t}from"./vue-B11_pI9F.js";import{t as n}from"./_plugin-vue_export-helper-B67ILkmu.js";var r={},i={class:`loading`};function a(n,r){return t(),e(`div`,i)}var o=n(r,[[`render`,a],[`__scopeId`,`data-v-cb4a6e17`]]);export{o as t};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
System.register(["./vue-legacy-BnJhYd-u.js","./_plugin-vue_export-helper-legacy-DNBbTZ02.js"],function(e,t){var r,a,n,o,i;function c(e,t){return a(),r("div",i)}return{setters:[function(e){r=e.K,a=e.ct},function(e){n=e.t}],execute:function(){(o=document.createElement("style")).textContent='.loading[data-v-cb4a6e17]{aspect-ratio:1;width:16px;height:16px;color:var(--Labels-Primary);border-radius:50%;position:relative}.loading[data-v-cb4a6e17]:after{content:"";background:conic-gradient(from 90deg,transparent 30deg,currentColor 330.922deg,transparent 330.922deg,transparent 360deg);border-radius:50%;width:calc(90% - 2px);height:calc(90% - 2px);animation:1s linear infinite spin-cb4a6e17;position:absolute;top:2px;left:2px;-webkit-mask:radial-gradient(transparent 0,transparent 57%,currentColor 57%,currentColor 80%);mask:radial-gradient(transparent 0,transparent 57%,currentColor 57%,currentColor 80%)}@keyframes spin-cb4a6e17{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n/*$vite$:1*/',document.head.appendChild(o),i={class:"loading"},e("t",n({},[["render",c],["__scopeId","data-v-cb4a6e17"]]))}}});
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{G as e,H as t,K as n,Nt as r,U as i,W as a,Y as o,Z as s,ct as c,kt as l,ot as u,vt as d,z as f}from"./vue-B11_pI9F.js";import{t as p}from"./_plugin-vue_export-helper-B67ILkmu.js";import{r as m,s as h}from"./useMessage-Cpv2N0cC.js";import{I as g,d as _,ot as v,q as y,t as b}from"./ThumbnailSlide-COLdjDXw.js";import{t as x}from"./kimi.button-DiBbOOiZ.js";import{t as S}from"./usePPTDTitle-senR0KSa.js";var C={class:`sdk-player`},w={class:`sdk-player-stage`},T={key:1,class:`sdk-player-controls`},E={class:`page-number`},D=p(s({__name:`Player`,setup(s){let{player:p}=h(),{pages:D,pageOrder:O,pageSize:k,meta:A,currentPageId:j,setCurrentPage:M}=v(),{setEditable:N}=y(),{setPPTDTitle:P}=S();g(`screen`),_(),m();let F=t(()=>j.value??O.value[0]),I=t(()=>{let e=F.value;return e?D.value.get(e):void 0}),L=t(()=>{let e=F.value;return e?O.value.indexOf(e):-1}),R=e=>typeof e==`number`?O.value[Math.max(0,Math.trunc(e)-1)]:e.replace(/\.page$/,``),z=t(()=>O.value.length===0||L.value<0?``:`${L.value+1} / ${O.value.length}`),B=t(()=>L.value>0),V=t(()=>L.value>=0&&L.value<O.value.length-1),H=()=>{if(!B.value)return;let e=O.value[L.value-1];e&&M(e)},U=()=>{if(!V.value)return;let e=O.value[L.value+1];e&&M(e)};return d(()=>A.value.title,e=>P(e),{immediate:!0}),d(()=>[O.value.join(`|`),p.value.page],()=>{if(p.value.page===void 0||O.value.length===0)return;let e=R(p.value.page);e&&M(e)},{immediate:!0}),u(()=>{N(!1)}),(t,s)=>(c(),n(`div`,C,[i(`div`,w,[I.value?(c(),a(b,{key:0,class:`sdk-player-slide`,slide:I.value,"page-width":l(k)[0],"page-height":l(k)[1]},null,8,[`slide`,`page-width`,`page-height`])):e(``,!0),l(p).controls&&l(O).length?(c(),n(`div`,T,[o(l(x),{type:`icon`,size:`small`,icon:`Left_b`,disabled:!B.value,onClick:f(H,[`stop`])},null,8,[`disabled`]),i(`span`,E,r(z.value),1),o(l(x),{type:`icon`,size:`small`,icon:`Right_b`,disabled:!V.value,onClick:f(U,[`stop`])},null,8,[`disabled`])])):e(``,!0)])]))}}),[[`__scopeId`,`data-v-d393e207`]]);export{D as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
html,body{background:var(--Bg-Secondary,#f5f5f5);width:100%;height:auto;margin:0;overflow:hidden}#app{background:var(--Bg-Secondary,#f5f5f5);width:100%}.sdk-player[data-v-d393e207]{background:var(--Bg-Secondary,#f5f5f5);width:100vw}.sdk-player-stage[data-v-d393e207]{width:100%;position:relative}.sdk-player-slide[data-v-d393e207]{width:100%}.sdk-player-controls[data-v-d393e207]{z-index:2;backdrop-filter:blur(8px);color:#fff;pointer-events:auto;background:rgba(0,0,0,.4);border-radius:999px;align-items:center;gap:10px;padding:8px 12px;display:inline-flex;position:absolute;bottom:16px;left:50%;right:auto;transform:translate(-50%)}.page-number[data-v-d393e207]{text-align:center;color:#fff;min-width:72px;font-size:12px;line-height:1}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
System.register(["./vue-legacy-BnJhYd-u.js","./_plugin-vue_export-helper-legacy-DNBbTZ02.js","./useMessage-legacy-BwPIj8SG.js","./ThumbnailSlide-legacy-CSXTq44l.js","./kimi.button-legacy-Dfu-bBlw.js","./usePPTDTitle-legacy-BtadEyXI.js"],function(e,a){var t,l,n,i,s,u,d,r,o,c,v,p,g,f,h,y,m,b,k,x,w,_,j,P,z,S,C,T;return{setters:[function(e){t=e.G,l=e.H,n=e.K,i=e.Nt,s=e.U,u=e.W,d=e.Y,r=e.Z,o=e.ct,c=e.kt,v=e.ot,p=e.vt,g=e.z},function(e){f=e.t},function(e){h=e.r,y=e.s},function(e){m=e.I,b=e.d,k=e.ot,x=e.q,w=e.t},function(e){_=e.t},function(e){j=e.t}],execute:function(){(P=document.createElement("style")).textContent="html,body{background:var(--Bg-Secondary,#f5f5f5);width:100%;height:auto;margin:0;overflow:hidden}#app{background:var(--Bg-Secondary,#f5f5f5);width:100%}.sdk-player[data-v-d393e207]{background:var(--Bg-Secondary,#f5f5f5);width:100vw}.sdk-player-stage[data-v-d393e207]{width:100%;position:relative}.sdk-player-slide[data-v-d393e207]{width:100%}.sdk-player-controls[data-v-d393e207]{z-index:2;backdrop-filter:blur(8px);color:#fff;pointer-events:auto;background:rgba(0,0,0,.4);border-radius:999px;align-items:center;gap:10px;padding:8px 12px;display:inline-flex;position:absolute;bottom:16px;left:50%;right:auto;transform:translate(-50%)}.page-number[data-v-d393e207]{text-align:center;color:#fff;min-width:72px;font-size:12px;line-height:1}\n/*$vite$:1*/",document.head.appendChild(P),z={class:"sdk-player"},S={class:"sdk-player-stage"},C={key:1,class:"sdk-player-controls"},T={class:"page-number"},e("default",f(r({__name:"Player",setup(e){const{player:a}=y(),{pages:r,pageOrder:f,pageSize:P,meta:$,currentPageId:B,setCurrentPage:I}=k(),{setEditable:M}=x(),{setPPTDTitle:D}=j();m("screen"),b(),h();const E=l(()=>B.value??f.value[0]),O=l(()=>{const e=E.value;return e?r.value.get(e):void 0}),W=l(()=>{const e=E.value;return e?f.value.indexOf(e):-1}),q=l(()=>0===f.value.length||W.value<0?"":`${W.value+1} / ${f.value.length}`),G=l(()=>W.value>0),H=l(()=>W.value>=0&&W.value<f.value.length-1),K=()=>{if(!G.value)return;const e=f.value[W.value-1];e&&I(e)},L=()=>{if(!H.value)return;const e=f.value[W.value+1];e&&I(e)};return p(()=>$.value.title,e=>D(e),{immediate:!0}),p(()=>[f.value.join("|"),a.value.page],()=>{if(void 0===a.value.page||0===f.value.length)return;const e="number"==typeof(t=a.value.page)?f.value[Math.max(0,Math.trunc(t)-1)]:t.replace(/\.page$/,"");var t;e&&I(e)},{immediate:!0}),v(()=>{M(!1)}),(e,l)=>(o(),n("div",z,[s("div",S,[O.value?(o(),u(w,{key:0,class:"sdk-player-slide",slide:O.value,"page-width":c(P)[0],"page-height":c(P)[1]},null,8,["slide","page-width","page-height"])):t("",!0),c(a).controls&&c(f).length?(o(),n("div",C,[d(c(_),{type:"icon",size:"small",icon:"Left_b",disabled:!G.value,onClick:g(K,["stop"])},null,8,["disabled"]),s("span",T,i(q.value),1),d(c(_),{type:"icon",size:"small",icon:"Right_b",disabled:!H.value,onClick:g(L,["stop"])},null,8,["disabled"])])):t("",!0)])]))}}),[["__scopeId","data-v-d393e207"]]))}}});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.popover-menu[data-v-2856486f]{cursor:pointer;border-radius:8px;justify-content:center;align-items:center;height:28px;display:inline-flex;position:relative}.popover-menu-trigger[data-v-2856486f]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.popover-menu[data-v-2856486f]:not(.disabled):hover{background-color:var(--Fills-F1)}.popover-menu.disabled[data-v-2856486f]{cursor:not-allowed;opacity:.5}.popover-menu.disabled[data-v-2856486f]:hover{background-color:transparent}.popover-menu-caret[data-v-2856486f]{width:16px;color:var(--Labels-Tertiary)}.popover-menu-content[data-v-2856486f]{top:var(--menu-top-margin,28px);bottom:var(--menu-bottom-margin,auto);background-color:var(--Bg-Tertiary);white-space:nowrap;z-index:1;border-radius:12px;position:absolute;left:50%;overflow:hidden;transform:translate(-50%,20px);box-shadow:0 4px 16.4px rgba(0,0,0,.1)}.popover-menu-content.is-floating[data-v-2856486f]{z-index:auto;position:static;top:auto;bottom:auto;left:auto;transform:none}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{At as e,G as t,H as n,K as r,Tt as i,U as a,W as o,Y as s,Z as c,bt as l,ct as u,dt as d,kt as f,n as p,vt as m,xt as h,z as g}from"./vue-B11_pI9F.js";import{t as _}from"./_plugin-vue_export-helper-B67ILkmu.js";import{v}from"./index-jtNAhQeK.js";import{r as y}from"./dom-CWEl93tC.js";import{n as b,t as x}from"./menuExclusive-DWJqMbvd.js";var S=_(c({__name:`PopoverMenu`,props:{tooltip:{},tooltipPlacement:{},hideCaret:{type:Boolean},floatOptions:{},disabled:{type:Boolean},contentClass:{},contentClickCloses:{type:Boolean,default:!0}},emits:[`update:open`],setup(c,{expose:_,emit:S}){let C=c,w=S,T=i(!1),E=i(),D=n(()=>C.floatOptions?.teleport??!1),{notifyOpen:O}=x(()=>{T.value=!1}),k=()=>{D.value||(T.value=!1)},A=()=>{C.disabled||(T.value=!T.value)},j=()=>{C.contentClickCloses&&A()};return m(()=>C.disabled,e=>{e&&(T.value=!1)}),m(T,e=>{e&&O(),w(`update:open`,e)}),_({close:()=>{T.value=!1}}),(n,i)=>h((u(),r(`div`,{class:e([`popover-menu`,{disabled:c.disabled}]),onClick:g(A,[`stop`])},[s(f(y),{content:c.tooltip,disabled:!c.tooltip,placement:c.tooltipPlacement},{trigger:l(()=>[a(`div`,{ref_key:`triggerRef`,ref:E,class:`popover-menu-trigger`},[d(n.$slots,`trigger`,{popoverVisible:T.value},void 0,!0),c.hideCaret?t(``,!0):(u(),o(f(v),{key:0,class:`popover-menu-caret`,name:`Down_b`,width:`12`,height:`12`}))],512)]),_:3},8,[`content`,`disabled`,`placement`]),s(b,{open:T.value,"onUpdate:open":i[0]||(i[0]=e=>T.value=e),"trigger-el":E.value,options:c.floatOptions},{default:l(()=>[a(`div`,{class:e([`popover-menu-content auto-rtl`,[{"is-floating":D.value},c.contentClass]]),onClick:g(j,[`stop`])},[d(n.$slots,`popover`,{},void 0,!0)],2)]),_:3},8,[`open`,`trigger-el`,`options`])],2)),[[f(p),k]])}}),[[`__scopeId`,`data-v-2856486f`]]);export{S as t};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
System.register(["./vue-legacy-BnJhYd-u.js","./_plugin-vue_export-helper-legacy-DNBbTZ02.js","./index-legacy-Ce-qKpT_.js","./dom-legacy-D-syBzLK.js","./menuExclusive-legacy-CBHfJWDM.js"],function(e,t){var o,n,a,i,r,l,p,s,d,u,c,v,m,f,g,b,x,y,h,C,k,_,w,j;return{setters:[function(e){o=e.At,n=e.G,a=e.H,i=e.K,r=e.Tt,l=e.U,p=e.W,s=e.Y,d=e.Z,u=e.bt,c=e.ct,v=e.dt,m=e.kt,f=e.n,g=e.vt,b=e.xt,x=e.z},function(e){y=e.t},function(e){h=e.v},function(e){C=e.r},function(e){k=e.n,_=e.t}],execute:function(){(w=document.createElement("style")).textContent=".popover-menu[data-v-2856486f]{cursor:pointer;border-radius:8px;justify-content:center;align-items:center;height:28px;display:inline-flex;position:relative}.popover-menu-trigger[data-v-2856486f]{justify-content:center;align-items:center;width:100%;height:100%;display:flex}.popover-menu[data-v-2856486f]:not(.disabled):hover{background-color:var(--Fills-F1)}.popover-menu.disabled[data-v-2856486f]{cursor:not-allowed;opacity:.5}.popover-menu.disabled[data-v-2856486f]:hover{background-color:transparent}.popover-menu-caret[data-v-2856486f]{width:16px;color:var(--Labels-Tertiary)}.popover-menu-content[data-v-2856486f]{top:var(--menu-top-margin,28px);bottom:var(--menu-bottom-margin,auto);background-color:var(--Bg-Tertiary);white-space:nowrap;z-index:1;border-radius:12px;position:absolute;left:50%;overflow:hidden;transform:translate(-50%,20px);box-shadow:0 4px 16.4px rgba(0,0,0,.1)}.popover-menu-content.is-floating[data-v-2856486f]{z-index:auto;position:static;top:auto;bottom:auto;left:auto;transform:none}\n/*$vite$:1*/",document.head.appendChild(w),j=d({__name:"PopoverMenu",props:{tooltip:{},tooltipPlacement:{},hideCaret:{type:Boolean},floatOptions:{},disabled:{type:Boolean},contentClass:{},contentClickCloses:{type:Boolean,default:!0}},emits:["update:open"],setup(e,{expose:t,emit:d}){const y=e,w=d,j=r(!1),B=r(),O=a(()=>y.floatOptions?.teleport??!1),{notifyOpen:$}=_(()=>{j.value=!1}),z=()=>{O.value||(j.value=!1)},E=()=>{y.disabled||(j.value=!j.value)},P=()=>{y.contentClickCloses&&E()};return g(()=>y.disabled,e=>{e&&(j.value=!1)}),g(j,e=>{e&&$(),w("update:open",e)}),t({close:()=>{j.value=!1}}),(t,a)=>b((c(),i("div",{class:o(["popover-menu",{disabled:e.disabled}]),onClick:x(E,["stop"])},[s(m(C),{content:e.tooltip,disabled:!e.tooltip,placement:e.tooltipPlacement},{trigger:u(()=>[l("div",{ref_key:"triggerRef",ref:B,class:"popover-menu-trigger"},[v(t.$slots,"trigger",{popoverVisible:j.value},void 0,!0),e.hideCaret?n("",!0):(c(),p(m(h),{key:0,class:"popover-menu-caret",name:"Down_b",width:"12",height:"12"}))],512)]),_:3},8,["content","disabled","placement"]),s(k,{open:j.value,"onUpdate:open":a[0]||(a[0]=e=>j.value=e),"trigger-el":B.value,options:e.floatOptions},{default:u(()=>[l("div",{class:o(["popover-menu-content auto-rtl",[{"is-floating":O.value},e.contentClass]]),onClick:x(P,["stop"])},[v(t.$slots,"popover",{},void 0,!0)],2)]),_:3},8,["open","trigger-el","options"])],2)),[[m(f),z]])}}),e("t",y(j,[["__scopeId","data-v-2856486f"]]))}}});
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
import{H as e,J as t,K as n,Nt as r,Tt as i,U as a,W as o,Y as ee,Z as s,at as c,ct as l,k as u,kt as d,ot as f,vt as p}from"./vue-B11_pI9F.js";import{t as m}from"./_plugin-vue_export-helper-B67ILkmu.js";import{B as h,I as te,L as g,v as _}from"./index-jtNAhQeK.js";import{ot as v}from"./ThumbnailSlide-COLdjDXw.js";import{i as y,t as b}from"./Screen-e_2r2R5P.js";import{t as x}from"./usePPTDTitle-senR0KSa.js";var S={key:1,class:`presenter-page`},C={class:`presenter-panel`},w=[`disabled`],T={class:`presenter-start-icon`},E=m(s({__name:`Presenter`,setup(s){let m=u(),{t:E}=g(),{restorePPTD:D,setCurrentPage:O,meta:ne,pageOrder:k}=v(),{setPPTDTitle:re}=x(),A=i(!1),j=i(!1),M=i(!1),N=i(),P=i(0),F=null,I=null,L=e(()=>{let e=m.query.sessionId;return typeof e==`string`?e:``}),R=e(()=>!!L.value&&!!window.opener),z=()=>navigator.presentation?.receiver,B=e=>{if(typeof e==`string`)try{return JSON.parse(e)}catch{return null}return typeof e==`object`&&e?e:null},V=()=>h(m.query.lang)??h(m.query.locale)??null;(()=>{let e=V();e&&te(e)})(),p(()=>ne.value.title,e=>re(e),{immediate:!0});let H=e=>{if(I){try{I.send(JSON.stringify(e))}catch{}return}window.opener?.postMessage(e,window.location.origin)},U=()=>{F&&(clearTimeout(F),F=null)},W=()=>{L.value&&(window.opener||I)&&H({type:`connect-failed`,source:`presenter`,sessionId:L.value}),window.close()},G=e=>{let t=k.value.includes(e.pageId)?e.pageId:k.value[e.index];if(!t)return;let n=k.value.indexOf(t);N.value=t,P.value=n===-1?0:n,O(t)},K=e=>{let t=N.value,n=P.value;D(e),A.value=!0;let r=Math.min(n,k.value.length-1),i=t&&k.value.includes(t)?t:k.value[r];if(!i){N.value=void 0,P.value=0;return}let a=k.value.indexOf(i);N.value=i,P.value=a===-1?0:a,O(i)},q=e=>{if(!(!L.value||e.sessionId!==L.value)&&e.source===`editor`)switch(e.type){case`snapshot`:U(),D(e.pptd),A.value=!0,G(e.page),M.value&&(j.value=!0);break;case`content-sync`:K(e.pptd);break;case`page-change`:G(e.page);break;case`close`:j.value=!1,window.close();break}},J=e=>{e.origin===window.location.origin&&(window.opener&&e.source!==window.opener||q(e.data))},Y=()=>{window.addEventListener(`message`,J),H({type:`presenter-ready`,source:`presenter`,sessionId:L.value}),F=setTimeout(()=>{A.value||W()},3e3)},X=e=>{let t=B(e.data);t&&q(t)},Z=()=>{window.close()},Q=e=>{I=e,M.value=!0,I.addEventListener(`message`,X),I.addEventListener(`close`,Z),I.addEventListener(`terminate`,Z),H({type:`presenter-ready`,source:`presenter`,sessionId:L.value}),F=setTimeout(()=>{A.value||W()},3e3)},ie=async()=>{let e=z();if(!e)return!1;let t=await e.connectionList,n=t.connections[0];return n?(Q(n),!0):new Promise(e=>{let n=setTimeout(()=>{t.removeEventListener(`connectionavailable`,r),e(!1)},3e3),r=i=>{clearTimeout(n),t.removeEventListener(`connectionavailable`,r),Q(i.connection),e(!0)};t.addEventListener(`connectionavailable`,r)})},ae=async()=>{if(R.value){Y();return}await ie()||W()},oe=async()=>{if(!A.value)return;let e=k.value[0];e&&$({pageId:e,index:0});try{await y()}catch{}j.value=!0},$=e=>{L.value&&(G(e),H({type:`page-change`,source:`presenter`,sessionId:L.value,page:e}))},se=()=>{L.value&&H({type:`close`,source:`presenter`,sessionId:L.value});try{I?.close()}catch{}window.close()},ce=()=>{if(M.value){se();return}j.value=!1};return f(ae),c(()=>{window.removeEventListener(`message`,J),U(),L.value&&H({type:`close`,source:`presenter`,sessionId:L.value}),I&&(I.removeEventListener(`message`,X),I.removeEventListener(`close`,Z),I.removeEventListener(`terminate`,Z),I=null)}),(e,i)=>j.value&&A.value?(l(),o(b,{key:0,"start-index":P.value,"current-page-id":N.value,"auto-fullscreen":!1,onPageChange:$,onExit:ce},null,8,[`start-index`,`current-page-id`])):(l(),n(`div`,S,[a(`div`,C,[a(`button`,{type:`button`,class:`presenter-start-button`,disabled:!A.value,onClick:oe},[a(`span`,T,[ee(d(_),{name:`Play`,height:24})]),t(` `+r(d(E)(`presenter.start`)),1)],8,w)])]))}}),[[`__scopeId`,`data-v-65fc3c19`]]);export{E as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
.presenter-page[data-v-65fc3c19]{background:var(--Bg-Secondary);width:100vw;height:100vh;color:var(--Labels-Primary);justify-content:center;align-items:center;display:flex}.presenter-panel[data-v-65fc3c19]{box-sizing:border-box;flex-direction:column;justify-content:center;align-items:center;gap:14px;width:100%;height:100%;display:flex}.presenter-start-button[data-v-65fc3c19]{background:var(--Labels-Secondary);color:#fff;cursor:pointer;border:none;justify-content:center;align-items:center;gap:10px;width:100%;height:100%;padding:0 28px;font-size:18px;font-weight:600;line-height:24px;transition:background-color .18s,opacity .18s,transform .18s;display:inline-flex}.presenter-start-button[data-v-65fc3c19]:hover:not(:disabled){background:var(--Labels-Primary)}.presenter-start-button[data-v-65fc3c19]:disabled{opacity:.45;cursor:not-allowed}.presenter-start-icon[data-v-65fc3c19]{justify-content:center;align-items:center;display:flex}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
import{At as e,B as t,H as n,K as r,Mt as i,U as a,W as o,Z as s,ct as c,kt as l,ot as u,ut as d,vt as f}from"./vue-B11_pI9F.js";import{t as p}from"./_plugin-vue_export-helper-B67ILkmu.js";import{r as m,s as h}from"./useMessage-Cpv2N0cC.js";import{d as g,ot as _,q as v,t as y}from"./ThumbnailSlide-COLdjDXw.js";import{t as b}from"./usePPTDTitle-senR0KSa.js";var x={class:`sdk-preview`},S=p(s({__name:`Preview`,setup(s){g(),m();let{pages:p,pageOrder:S,pageSize:C,meta:w,currentPageId:T,setCurrentPage:E}=_(),{setEditable:D}=v(),{setPPTDTitle:O}=b(),{preview:k}=h(),A=n(()=>({"--preview-page-margin":k.value.margin,gridTemplateColumns:`repeat(${k.value.col}, minmax(0, 1fr))`}));function j(e){E(e)}return f(()=>w.value.title,e=>O(e),{immediate:!0}),u(()=>{D(!1)}),(n,s)=>(c(),r(`div`,x,[a(`div`,{class:`sdk-preview-pages`,style:i(A.value)},[(c(!0),r(t,null,d(l(S),(t,n)=>(c(),o(y,{key:t,class:e([`sdk-preview-page`,{current:l(T)===t}]),"data-preview-page-id":t,"data-preview-page-index":n,slide:l(p).get(t),"page-width":l(C)[0],"page-height":l(C)[1],lazy:``,onClick:e=>j(t)},null,8,[`class`,`data-preview-page-id`,`data-preview-page-index`,`slide`,`page-width`,`page-height`,`onClick`]))),128))],4)]))}}),[[`__scopeId`,`data-v-8acb7e59`]]);export{S as default};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
html,body{background:var(--Bg-Secondary,#f5f5f5);height:auto;min-height:100vh;overflow-y:auto}#app{background:var(--Bg-Secondary,#f5f5f5);width:100%;min-height:100vh}.sdk-preview[data-v-8acb7e59]{width:100vw;min-height:100vh;background:var(--Bg-Secondary,#f5f5f5);min-height:100dvh}.sdk-preview-pages[data-v-8acb7e59]{--preview-page-margin:20px;gap:var(--preview-page-margin);width:100vw;min-height:100vh;box-sizing:border-box;min-height:100dvh;padding:var(--preview-page-margin);grid-template-columns:minmax(0,1fr);display:grid}.sdk-preview-page[data-v-8acb7e59]{border:1px solid var(--Always-White,#fff);outline:1px solid var(--Separators-S1,rgba(0,0,0,.13));cursor:pointer;width:100%;min-width:0;scroll-margin:var(--preview-page-margin);background:#fff;border-radius:12px;flex-shrink:0;overflow:hidden;box-shadow:0 4px 16.4px rgba(0,0,0,.1)}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
System.register(["./vue-legacy-BnJhYd-u.js","./_plugin-vue_export-helper-legacy-DNBbTZ02.js","./useMessage-legacy-BwPIj8SG.js","./ThumbnailSlide-legacy-CSXTq44l.js","./usePPTDTitle-legacy-BtadEyXI.js"],function(e,a){var i,t,r,n,d,g,p,s,l,v,o,c,u,h,m,w,f,b,x,y,k,S,P;return{setters:[function(e){i=e.At,t=e.B,r=e.H,n=e.K,d=e.Mt,g=e.U,p=e.W,s=e.Z,l=e.ct,v=e.kt,o=e.ot,c=e.ut,u=e.vt},function(e){h=e.t},function(e){m=e.r,w=e.s},function(e){f=e.d,b=e.ot,x=e.q,y=e.t},function(e){k=e.t}],execute:function(){(S=document.createElement("style")).textContent="html,body{background:var(--Bg-Secondary,#f5f5f5);height:auto;min-height:100vh;overflow-y:auto}#app{background:var(--Bg-Secondary,#f5f5f5);width:100%;min-height:100vh}.sdk-preview[data-v-8acb7e59]{width:100vw;min-height:100vh;background:var(--Bg-Secondary,#f5f5f5);min-height:100dvh}.sdk-preview-pages[data-v-8acb7e59]{--preview-page-margin:20px;gap:var(--preview-page-margin);width:100vw;min-height:100vh;box-sizing:border-box;min-height:100dvh;padding:var(--preview-page-margin);grid-template-columns:minmax(0,1fr);display:grid}.sdk-preview-page[data-v-8acb7e59]{border:1px solid var(--Always-White,#fff);outline:1px solid var(--Separators-S1,rgba(0,0,0,.13));cursor:pointer;width:100%;min-width:0;scroll-margin:var(--preview-page-margin);background:#fff;border-radius:12px;flex-shrink:0;overflow:hidden;box-shadow:0 4px 16.4px rgba(0,0,0,.1)}\n/*$vite$:1*/",document.head.appendChild(S),P={class:"sdk-preview"},e("default",h(s({__name:"Preview",setup(e){f(),m();const{pages:a,pageOrder:s,pageSize:h,meta:S,currentPageId:C,setCurrentPage:T}=b(),{setEditable:_}=x(),{setPPTDTitle:j}=k(),{preview:B}=w(),z=r(()=>({"--preview-page-margin":B.value.margin,gridTemplateColumns:`repeat(${B.value.col}, minmax(0, 1fr))`}));return u(()=>S.value.title,e=>j(e),{immediate:!0}),o(()=>{_(!1)}),(e,r)=>(l(),n("div",P,[g("div",{class:"sdk-preview-pages",style:d(z.value)},[(l(!0),n(t,null,c(v(s),(e,t)=>(l(),p(y,{key:e,class:i(["sdk-preview-page",{current:v(C)===e}]),"data-preview-page-id":e,"data-preview-page-index":t,slide:v(a).get(e),"page-width":v(h)[0],"page-height":v(h)[1],lazy:"",onClick:a=>function(e){T(e)}(e)},null,8,["class","data-preview-page-id","data-preview-page-index","slide","page-width","page-height","onClick"]))),128))],4)]))}}),[["__scopeId","data-v-8acb7e59"]]))}}});
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
html,body,#app{background:#fff;width:100%;height:100%;min-height:100vh}.raw-preview[data-v-2545fa61]{width:100vw;min-height:100vh;background:#fff;min-height:100dvh}.raw-preview-pages[data-v-2545fa61]{flex-direction:column;width:100%;display:flex}.raw-preview-page[data-v-2545fa61]{background:#fff;border:0;border-radius:0;flex-shrink:0;margin:0;overflow:hidden}
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
.thumbnail-slide-switcher[data-v-8b20498e]{width:100%;height:100%}.slide-container[data-v-8b20498e]{background-color:#1d1d1d;justify-content:center;align-items:center;width:100%;height:100%;display:flex}.slide-wrapper[data-v-8b20498e]{max-width:100vw}.screen[data-v-a6f7cc7b]{z-index:200;background-color:#1d1d1d;width:100vw;height:100vh;position:fixed;top:0;left:0}.screen-controls[data-v-a6f7cc7b]{z-index:10;opacity:0;pointer-events:none;justify-content:space-between;align-items:flex-end;transition:opacity .26s,transform .26s;display:flex;position:fixed;bottom:8px;left:8px;right:16px;transform:translateY(4px)}.screen-controls.is-visible[data-v-a6f7cc7b]{opacity:1;transform:translateY(0)}.screen-controls.is-visible .tools-left[data-v-a6f7cc7b],.screen-controls.is-visible .tools-right[data-v-a6f7cc7b]{pointer-events:auto}.tools-left[data-v-a6f7cc7b]{color:#666;pointer-events:none;gap:8px;font-size:25px;display:flex}.tool-btn[data-v-a6f7cc7b]{color:#666;opacity:.3;cursor:pointer;background:0 0;border:0;justify-content:center;align-items:center;width:25px;height:25px;padding:0;transition:opacity .2s;display:inline-flex}.tool-btn[data-v-a6f7cc7b]:hover{opacity:.95}.tools-right[data-v-a6f7cc7b]{pointer-events:none;align-items:center;gap:8px;margin-bottom:4px;display:flex}.presenter-button[data-v-a6f7cc7b]{color:#999;display:inline-flex}.presenter-button[data-v-a6f7cc7b] .button-icon-only.button-small{color:inherit;background:rgba(0,0,0,.28);border-color:rgba(255,255,255,.08);border-radius:4px}.presenter-button[data-v-a6f7cc7b] .button-icon-only.button-small:hover:not([disabled]):not(.button-disabled){color:#ddd;background:rgba(0,0,0,.38);border-color:rgba(255,255,255,.18)}.effect-menu[data-v-a6f7cc7b]{color:#999;background:rgba(0,0,0,.28);border:1px solid rgba(255,255,255,.08);border-radius:4px;height:26px}.effect-menu[data-v-a6f7cc7b]:hover{color:#ddd;background:rgba(0,0,0,.38);border-color:rgba(255,255,255,.18)}.effect-menu-trigger[data-v-a6f7cc7b]{width:136px;padding-left:8px;font-size:12px;line-height:18px}.effect-menu-content{background:rgba(20,20,20,.96)!important;border-radius:6px!important;box-shadow:0 8px 24px rgba(0,0,0,.32)!important}.effect-menu-list{min-width:168px;box-shadow:none!important;background:0 0!important;border-radius:6px!important;padding:4px!important}.effect-menu-list .menu-item{color:#999!important;border-radius:4px!important;height:30px!important}.effect-menu-list .menu-item:hover{color:#ddd!important;background:rgba(255,255,255,.08)!important}.effect-menu-list .menu-item.active{color:#ddd!important;background:rgba(255,255,255,.12)!important}.effect-menu-list .menu-item-text{font-size:12px!important}.page-number[data-v-a6f7cc7b]{color:#666;text-align:center;background:rgba(0,0,0,.28);border-radius:4px;min-width:48px;padding:4px 8px;font-size:13px;line-height:18px}
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
.menu-select[data-v-4c23e34a]{cursor:pointer;user-select:none;border-radius:6px;align-items:center;height:28px;display:inline-flex;position:relative}.menu-select[data-v-4c23e34a]:hover,.menu-select.is-active[data-v-4c23e34a]{background:var(--Fills-F1)}.menu-select.is-active[data-v-4c23e34a]:hover{background:var(--Fills-F1-hover)}.menu-select.is-keep-highlight[data-v-4c23e34a]{background:var(--Fills-F1)}.menu-select.is-keep-highlight[data-v-4c23e34a]:hover,.menu-select.is-keep-highlight.is-active[data-v-4c23e34a]{background:var(--Fills-F1-hover)}.menu-select.is-keep-highlight.is-active[data-v-4c23e34a]:hover{background:var(--Fills-F1-active)}.menu-select.disabled[data-v-4c23e34a]{opacity:.35;cursor:not-allowed}.menu-select.disabled[data-v-4c23e34a]:hover{background:0 0}.menu-select-menu[data-v-4c23e34a]{top:var(--menu-top-margin,28px);bottom:var(--menu-bottom-margin,auto);z-index:1000;white-space:nowrap;min-width:max-content;max-height:172px;position:absolute;left:50%;overflow:auto;transform:translate(-50%,20px)}.menu-select-menu[data-v-4c23e34a]::-webkit-scrollbar-track{margin:8px 0}.menu-select-menu.is-floating[data-v-4c23e34a]{position:static;top:auto;bottom:auto;left:auto;transform:none}.menu-select-title[data-v-4c23e34a]{color:var(--Labels-Primary);align-items:center;padding:4px 6px;font-size:14px;display:flex}.menu-select-title-text.max-width[data-v-4c23e34a]{text-overflow:ellipsis;overflow:hidden}.menu-select-title-icon[data-v-4c23e34a]{margin-right:-10px}.menu-select-title-caret[data-v-4c23e34a]{margin-left:10px}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user