diff --git a/assets/branding/README.md b/assets/branding/README.md index abe2923..d7046cb 100644 --- a/assets/branding/README.md +++ b/assets/branding/README.md @@ -1,8 +1,8 @@ # Framely branding assets / 品牌资源 -The pixel window sprite is the shared identity for Framely and its installer. `framely-mark.svg` and `framely-logo.svg` are dark artwork on transparent backgrounds; `*-light.svg` variants are for dark surfaces. `framely-app-icon.svg` is the application tile on sky blue. The wordmark uses vector paths and needs no font installation. +The pixel window sprite is the shared identity for Framely and its installer. `framely-mark.svg` and `framely-logo.svg` are dark artwork on transparent backgrounds; `*-light.svg` variants are for dark surfaces. `framely-app-icon.svg` uses a charcoal tile with a sky blue sprite. Square marks center the window body, allowing the left tail to extend outward; the native Dock sprite is 10% smaller. The wordmark uses vector paths and needs no font installation. -像素窗口小精灵是本体和安装器的统一标识。`framely-mark.svg` 和 `framely-logo.svg` 为透明底深色版本,`*-light.svg` 用于深色背景;`framely-app-icon.svg` 为浅蓝底应用图标。字标已转成路径,无需安装字体。 +像素窗口小精灵是本体和安装器的统一标识。`framely-mark.svg` 和 `framely-logo.svg` 为透明底深色版本,`*-light.svg` 用于深色背景;`framely-app-icon.svg` 为黑底蓝色应用图标。正方形图标以窗口主体居中,尾巴向左伸出;原生插件入口中的图形缩小 10%。字标已转成路径,无需安装字体。 After editing the SVGs, regenerate PNGs, the Windows ICO and native Dock artwork from the repository root, then commit the generated files together: diff --git a/assets/branding/framely-app-icon.png b/assets/branding/framely-app-icon.png index 5b5484b..f6f5f6c 100644 Binary files a/assets/branding/framely-app-icon.png and b/assets/branding/framely-app-icon.png differ diff --git a/assets/branding/framely-app-icon.svg b/assets/branding/framely-app-icon.svg index 0c070aa..f6693bb 100644 --- a/assets/branding/framely-app-icon.svg +++ b/assets/branding/framely-app-icon.svg @@ -1,8 +1,8 @@ Framely - Charcoal pixel window sprite on a sky blue application tile. + Sky blue pixel window sprite on a charcoal application tile, centered on its window body. - - + + diff --git a/assets/branding/framely-mark-light.svg b/assets/branding/framely-mark-light.svg index 58166d1..703592a 100644 --- a/assets/branding/framely-mark-light.svg +++ b/assets/branding/framely-mark-light.svg @@ -1,7 +1,7 @@ Framely - Sky blue pixel window sprite for dark backgrounds. + Pixel window sprite with two square eyes and a raised hand. - + diff --git a/assets/branding/framely-mark.svg b/assets/branding/framely-mark.svg index 6efbfe6..325491e 100644 --- a/assets/branding/framely-mark.svg +++ b/assets/branding/framely-mark.svg @@ -2,6 +2,6 @@ Framely Pixel window sprite with two square eyes and a raised hand. - + diff --git a/assets/branding/framely.ico b/assets/branding/framely.ico index 886bc21..d9ece7b 100644 Binary files a/assets/branding/framely.ico and b/assets/branding/framely.ico differ diff --git a/assets/branding/icons/1024.png b/assets/branding/icons/1024.png index bb11e15..0c229c7 100644 Binary files a/assets/branding/icons/1024.png and b/assets/branding/icons/1024.png differ diff --git a/assets/branding/icons/128.png b/assets/branding/icons/128.png index 446cdd0..8e130e7 100644 Binary files a/assets/branding/icons/128.png and b/assets/branding/icons/128.png differ diff --git a/assets/branding/icons/16.png b/assets/branding/icons/16.png index 85b527d..a352096 100644 Binary files a/assets/branding/icons/16.png and b/assets/branding/icons/16.png differ diff --git a/assets/branding/icons/24.png b/assets/branding/icons/24.png index 1295325..5622fab 100644 Binary files a/assets/branding/icons/24.png and b/assets/branding/icons/24.png differ diff --git a/assets/branding/icons/256.png b/assets/branding/icons/256.png index 1e4ce4d..7507b37 100644 Binary files a/assets/branding/icons/256.png and b/assets/branding/icons/256.png differ diff --git a/assets/branding/icons/32.png b/assets/branding/icons/32.png index 6729736..4b39f4d 100644 Binary files a/assets/branding/icons/32.png and b/assets/branding/icons/32.png differ diff --git a/assets/branding/icons/48.png b/assets/branding/icons/48.png index 1e97d88..611cb93 100644 Binary files a/assets/branding/icons/48.png and b/assets/branding/icons/48.png differ diff --git a/assets/branding/icons/512.png b/assets/branding/icons/512.png index 5b5484b..f6f5f6c 100644 Binary files a/assets/branding/icons/512.png and b/assets/branding/icons/512.png differ diff --git a/assets/branding/icons/64.png b/assets/branding/icons/64.png index 8f23380..336afb2 100644 Binary files a/assets/branding/icons/64.png and b/assets/branding/icons/64.png differ diff --git a/docs/README.md b/docs/README.md index 7064ef2..ff42fa1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -8,7 +8,7 @@ Framely manages plugins on Steam Frame through device services, a SteamVR interf 1. [Install, update, repair and uninstall](user-guide/installation.md): desktop installer, SSH, Preview builds, local packages and rollback. 2. [Basic usage](user-guide/basic-usage.md): Dock, manager, enabling plugins, favorites and safe mode. -3. [Install and manage plugins](user-guide/plugins.md): store, sources, subscriptions, local packages and updates. +3. [Install and manage plugins](user-guide/plugins.md): plugin library, sources, subscriptions, local packages and updates. 4. [Network panel](user-guide/network-panel.md): address, first password setup, login and phone/desktop access. ## Plugin development and publishing diff --git a/docs/developer-guide/manifest.md b/docs/developer-guide/manifest.md index 72b335f..02d1459 100644 --- a/docs/developer-guide/manifest.md +++ b/docs/developer-guide/manifest.md @@ -39,7 +39,7 @@ Leave source `files` empty. `framely pack` scans payload and supplies hashes. At | `screenshots` | Default empty; up to eight payload PNG/JPEG paths | | `authorUrl` / `documentationUrl` / `homepage` | Optional HTTP/HTTPS links, no embedded credentials | | `downloadUrl` | Optional fixed-version HTTPS package URL; GitHub database source may omit it for repository/version/ID derivation, but community packages need the resolved URL; see publishing | -| `publish` | Optional external store image configuration | +| `publish` | Optional external plugin library image configuration | | `backend` / `lifecycle` / `ui` | Optional backend/hooks/pages | | `dependencies` / `optionalDependencies` / `conflicts` | Default empty maps | | `exclusiveResources` | Default empty array; matching names prevent simultaneous enabling | diff --git a/docs/developer-guide/publishing.md b/docs/developer-guide/publishing.md index 91e8976..bd4c3f0 100644 --- a/docs/developer-guide/publishing.md +++ b/docs/developer-guide/publishing.md @@ -77,7 +77,7 @@ Automatic URLs always require a Release digest, even when a hash is supplied, an ### Icons and window compatibility -For the community database, set top-level `icon: "icon.png"` and include the same PNG path in source and payload. The database derives a GitHub Raw URL from the pinned repository/commit/path and verifies it against the packaged icon; `publish.icon` is unnecessary. Self-hosted catalogs use fixed HTTPS `publish.icon` / `publish.screenshots`. See [store metadata](../plugin-development.md). +For the community database, set top-level `icon: "icon.png"` and include the same PNG path in source and payload. The database derives a GitHub Raw URL from the pinned repository/commit/path and verifies it against the packaged icon; `publish.icon` is unnecessary. Self-hosted catalogs use fixed HTTPS `publish.icon` / `publish.screenshots`. See [plugin library metadata](../plugin-development.md). The community database supports window `entry`, `title`, `dockIcon`, `localWeb`, `width`, `height`, `widthMeters`. Pixel width is an integer 640–2560, height an integer 360–1440, and physical width follows the core's float32 validation for 0.4–4.0 meters. Omitted dimensions normalize to 1600×900 and 3 meters, matching pack output. Custom dimensions participate in source/package comparison; undeclared changes are rejected. The template's default standalone window passed actual packaging and database validation. Registration still requires whole-package hash, ownership and other field checks. @@ -92,7 +92,7 @@ mkdir -p target/plugin-catalog cargo run --locked -- catalog --name 'My source' --base-url https://example.org/packages --packages ./packages --output ./target/plugin-catalog/catalog.json ``` -Replace `example.org` with your HTTPS address. The CLI computes whole-package SHA256 and generates JSON; it does not upload packages or copy images. Without external `publish.icon`, the local CLI does not convert a packaged icon into a store URL. +Replace `example.org` with your HTTPS address. The CLI computes whole-package SHA256 and generates JSON; it does not upload packages or copy images. Without external `publish.icon`, the local CLI does not convert a packaged icon into a plugin library URL. **Upload the entire generated directory, not only `catalog.json`:** diff --git a/docs/plugin-development.md b/docs/plugin-development.md index 0dffbf8..e8811d9 100644 --- a/docs/plugin-development.md +++ b/docs/plugin-development.md @@ -79,9 +79,9 @@ Manifest supports payload PNG icon ≤1 MiB/1024×1024, details/tags/changelog a } ``` -The fragment above shows explicit URLs; GitHub registration source may omit `downloadUrl`, deriving repository, `v` and `-.framely`. The packaged Manifest still requires the final URL; current pack does not derive it. See [publishing](developer-guide/publishing.md) for temporary-Manifest commands. Community icons normally use top-level `icon`, with derived and verified pinned Raw URLs, without `publish.icon`. The local catalog CLI uses external `publish` images and does not export packaged image paths; absent external icons use the store default. Community registration pins submodule commits, obtains expected SHA256 from Release digests or source `downloadSha256`, then validates downloaded package hashes, declarations and payloads. Generated channels provide catalog/history JSON without mirroring packages/images. It does not execute author code or establish reproducible source/binary equivalence. +The fragment above shows explicit URLs; GitHub registration source may omit `downloadUrl`, deriving repository, `v` and `-.framely`. The packaged Manifest still requires the final URL; current pack does not derive it. See [publishing](developer-guide/publishing.md) for temporary-Manifest commands. Community icons normally use top-level `icon`, with derived and verified pinned Raw URLs, without `publish.icon`. The local catalog CLI uses external `publish` images and does not export packaged image paths; absent external icons use the plugin library default. Community registration pins submodule commits, obtains expected SHA256 from Release digests or source `downloadSha256`, then validates downloaded package hashes, declarations and payloads. Generated channels provide catalog/history JSON without mirroring packages/images. It does not execute author code or establish reproducible source/binary equivalence. -Catalog CLI only emits JSON, preferring `downloadUrl` over base URL/filename and using external publish images. It does not copy resources. Stores filter keywords/tags/origins/states, retain session caches on failures and install reviewed bytes without downloading again after confirmation. +Catalog CLI only emits JSON, preferring `downloadUrl` over base URL/filename and using external publish images. It does not copy resources. The plugin library filters keywords/tags/origins/states, retains session caches on failures and installs reviewed bytes without downloading again after confirmation. ## Scaffold and preview diff --git a/docs/user-guide/basic-usage.md b/docs/user-guide/basic-usage.md index c38fb2a..9503618 100644 --- a/docs/user-guide/basic-usage.md +++ b/docs/user-guide/basic-usage.md @@ -12,7 +12,7 @@ Click Framely in the SteamVR Dashboard Dock. First use requires accepting the te - **Installed**: browse local plugins and their pages. - **Settings**: language, safe mode and the plugin manager entry. -The manager opens a separate large window with navigation for plugins, store, sources, settings and About. Installation, updates and source management happen here. A phone or computer can also use the [network panel](network-panel.md). +The manager opens a separate large window with navigation for plugins, plugin library, sources, settings and About. Installation, updates and source management happen here. A phone or computer can also use the [network panel](network-panel.md). ## Plugins and windows diff --git a/docs/user-guide/network-panel.md b/docs/user-guide/network-panel.md index 09b4413..b69f9de 100644 --- a/docs/user-guide/network-panel.md +++ b/docs/user-guide/network-panel.md @@ -24,7 +24,7 @@ Change passwords from authenticated settings or the device's native manager. If ## Available operations -Manage sources and subscriptions, browse the store, upload local packages, confirm installation, enable/disable plugins, change settings and check device updates. Backends always run on Frame; uploading a file transfers it from your phone/computer. +Manage sources and subscriptions, browse the plugin library, upload local packages, confirm installation, enable/disable plugins, change settings and check device updates. Backends always run on Frame; uploading a file transfers it from your phone/computer. VR windows, controller vibration and SteamVR keyboard need headset validation; a browser cannot replace those interactions. Some external links open in the device's default browser. diff --git a/docs/user-guide/plugins.md b/docs/user-guide/plugins.md index bab0df3..0bade5b 100644 --- a/docs/user-guide/plugins.md +++ b/docs/user-guide/plugins.md @@ -4,9 +4,9 @@ [Documentation](../README.md) · [Network panel](network-panel.md) -## Install from the store +## Install from the plugin library -1. Open **Store** in the manager. Current code initializes community stable/testing sources; availability depends on the actual service and device configuration. If no catalog is available, add a developer's complete HTTPS `catalog.json` URL in Sources. +1. Open **Plugin library** in the manager. Current code initializes community stable/testing sources; availability depends on the actual service and device configuration. If no catalog is available, add a developer's complete HTTPS `catalog.json` URL in Sources. 2. Filter by keywords, tags, source or installation state. 3. Open details and review author, description, changes, runtime user and dependencies. The recommended version is shown first; history loads on demand. 4. Install or inspect the selected version. Read the confirmation after download, verification and dependency resolution. @@ -49,7 +49,7 @@ Uninstall through the management dialog. Hooks handle external cleanup and saved | Symptom | Action | | --- | --- | -| Empty store | Check enabled sources, connectivity and filters | +| Empty plugin library | Check enabled sources, connectivity and filters | | Download/hash failure | Check network and URL; download again without bypassing verification | | Missing history | The source may lack its history file; use latest or a trusted local old package | | Cannot enable | Read dependency/conflict errors and install required plugins | diff --git a/docs/zh-CN/README.md b/docs/zh-CN/README.md index 936703f..eafd437 100644 --- a/docs/zh-CN/README.md +++ b/docs/zh-CN/README.md @@ -10,7 +10,7 @@ Framely 是 Steam Frame 的插件管理器,包含设备端服务、SteamVR 界 1. [安装、更新、修复与卸载](user-guide/installation.md):桌面安装器、SSH、Preview、本地包、回滚。 2. [基本使用](user-guide/basic-usage.md):Dock 入口、管理窗口、启停、收藏、安全模式。 -3. [安装与管理插件](user-guide/plugins.md):商店、来源、订阅、本地包和更新。 +3. [安装与管理插件](user-guide/plugins.md):插件库、来源、订阅、本地包和更新。 4. [网络管理面板](user-guide/network-panel.md):地址、首次密码、登录、手机/电脑使用。 ## 插件开发与发布 diff --git a/docs/zh-CN/developer-guide/README.md b/docs/zh-CN/developer-guide/README.md index 4642e3f..02c0504 100644 --- a/docs/zh-CN/developer-guide/README.md +++ b/docs/zh-CN/developer-guide/README.md @@ -40,7 +40,7 @@ npm run dev 在浏览器打开 `http://127.0.0.1:5173`;修改页面后自动重载。预览只模拟窗口和通知,不启动 Python 后端,不拥有 Frame 权限;读取/保存按钮在预览中报出后端不可用是正常情况。不要据此声称设备功能已验收。 -UI 与后端 API 见 [SDK 文档](sdk.md),字段、默认值和范围见 [Manifest 配置](manifest.md);窗口与商店资料补充见[插件开发参考](../plugin-development.md)。 +UI 与后端 API 见 [SDK 文档](sdk.md),字段、默认值和范围见 [Manifest 配置](manifest.md);窗口与插件库资料补充见[插件开发参考](../plugin-development.md)。 ## 4. 实现后端与生命周期 diff --git a/docs/zh-CN/developer-guide/manifest.md b/docs/zh-CN/developer-guide/manifest.md index 1fabb2f..5dd5879 100644 --- a/docs/zh-CN/developer-guide/manifest.md +++ b/docs/zh-CN/developer-guide/manifest.md @@ -39,7 +39,7 @@ | `screenshots` | 默认空数组,最多 8 个 payload 内 PNG/JPEG 路径 | | `authorUrl` / `documentationUrl` / `homepage` | 可选 HTTP/HTTPS 地址,禁止 URL 内用户名密码 | | `downloadUrl` | 可选,固定版本 HTTPS 包下载地址;GitHub 数据库源码可省略,按仓库/版本/ID 拼接,社区登记的包内需补全;见发布指南 | -| `publish` | 可选外部商店图片配置,见下文 | +| `publish` | 可选外部插件库图片配置,见下文 | | `backend` / `lifecycle` / `ui` | 可选,配置后端、钩子和页面 | | `dependencies` / `optionalDependencies` / `conflicts` | 默认空对象,见关系参考 | | `exclusiveResources` | 默认空数组,禁止同时启用占用同名资源的插件 | diff --git a/docs/zh-CN/developer-guide/publishing.md b/docs/zh-CN/developer-guide/publishing.md index 1596dcd..a161213 100644 --- a/docs/zh-CN/developer-guide/publishing.md +++ b/docs/zh-CN/developer-guide/publishing.md @@ -77,7 +77,7 @@ cargo run --manifest-path "$FRAMELY_REPO/Cargo.toml" --locked -- verify "dist/$P ### 图标与窗口兼容 -社区数据库的图标通常只需顶层 `icon: "icon.png"`:同一路径 PNG 同时提交到源码仓库和 `payload/`。数据库从登记的仓库、固定提交及路径生成 GitHub Raw 地址,并下载比较包内图标;不需要额外填写 `publish.icon`。自行托管目录的图片由 `publish.icon` / `publish.screenshots` 提供固定 HTTPS 地址。[商店资料参考](../plugin-development.md#商店资料与图标) +社区数据库的图标通常只需顶层 `icon: "icon.png"`:同一路径 PNG 同时提交到源码仓库和 `payload/`。数据库从登记的仓库、固定提交及路径生成 GitHub Raw 地址,并下载比较包内图标;不需要额外填写 `publish.icon`。自行托管目录的图片由 `publish.icon` / `publish.screenshots` 提供固定 HTTPS 地址。[插件库资料参考](../plugin-development.md#插件库资料与图标) 社区数据库支持窗口 `entry`、`title`、`dockIcon`、`localWeb`、`width`、`height`、`widthMeters`。像素宽度为整数 640–2560,高度为整数 360–1440,物理宽度按本体 float32 规则校验 0.4–4.0 米。源码省略尺寸时,数据库会按 1600×900、3 米补全,与 pack 输出一致;自定义尺寸也会参与源码和包内清单比较,未声明的变化会被拒绝。模板包含的默认独立窗口已通过实际打包和数据库校验;登记仍需完成整包哈希、归属及其他字段检查。 @@ -92,7 +92,7 @@ mkdir -p target/plugin-catalog cargo run --locked -- catalog --name '我的插件源' --base-url https://example.org/packages --packages ./packages --output ./target/plugin-catalog/catalog.json ``` -将 `example.org` 换成实际 HTTPS 地址。CLI 计算完整包的 SHA256并生成 JSON,不上传包、不复制图片。没有外部 `publish.icon` 时,本地 CLI 不会把包内图标转换成商店 URL。 +将 `example.org` 换成实际 HTTPS 地址。CLI 计算完整包的 SHA256并生成 JSON,不上传包、不复制图片。没有外部 `publish.icon` 时,本地 CLI 不会把包内图标转换成插件库 URL。 **上传整个生成目录,不能只上传 `catalog.json`:** diff --git a/docs/zh-CN/plugin-development.md b/docs/zh-CN/plugin-development.md index 820826a..e8dbd96 100644 --- a/docs/zh-CN/plugin-development.md +++ b/docs/zh-CN/plugin-development.md @@ -87,11 +87,11 @@ Framely 主页面和插件 iframe 自动为可交互控件提供轻微的手柄 振动由 OpenVR 的当前激光控制器执行(12 ms、120 Hz、幅度 0.15),宿主只接受当前可见且被激光命中的视图,限制相邻脉冲间隔为 80 ms。插件发出的反馈消息由 iframe 宿主绑定到实际视图,不能指定另一个窗口或手柄。 -## 商店资料与图标 +## 插件库资料与图标 Manifest 可声明 `icon`(载荷内 PNG,最多 1 MiB、1024×1024)、`details`、`tags`、`screenshots`(最多 8 个载荷内 PNG/JPEG 路径)、`changelog`。这些文件会和页面一样加入文件哈希清单。图标用于插件列表和插件独立窗口的原生 Dock 缩略图,省略时使用默认拼图图标。 -清单还支持三个可选网页地址,未填写的入口不显示:`authorUrl`(作者主页)、`documentationUrl`(插件文档)、`homepage`(插件主页)。地址必须是 HTTP 或 HTTPS,可包含文档锚点,不接受带用户名密码的链接。这些字段会保留在安装后的清单,并由 `framely catalog` 导出到商店目录;作者主页链接直接显示在作者名称上,未提供时作者名称为普通文本;商店列表、详情、管理弹窗和安装确认使用相同行为。文档和插件主页保留独立入口,点击后由系统默认浏览器打开。 +清单还支持三个可选网页地址,未填写的入口不显示:`authorUrl`(作者主页)、`documentationUrl`(插件文档)、`homepage`(插件主页)。地址必须是 HTTP 或 HTTPS,可包含文档锚点,不接受带用户名密码的链接。这些字段会保留在安装后的清单,并由 `framely catalog` 导出到插件库目录;作者主页链接直接显示在作者名称上,未提供时作者名称为普通文本;插件库列表、详情、管理弹窗和安装确认使用相同行为。文档和插件主页保留独立入口,点击后由系统默认浏览器打开。 ```json { @@ -113,11 +113,11 @@ Manifest 可声明 `icon`(载荷内 PNG,最多 1 MiB、1024×1024)、`deta } ``` -上述为显式地址示例,`downloadUrl` 并非源码必填字段:GitHub 社区登记可按仓库、`v` 和 `-.framely` 自动生成。包内仍需最终地址,当前 pack 不会自动读取 Git remote;按[发布指南](developer-guide/publishing.md)生成临时清单即可。社区图标通常只需 `icon`,数据库从固定源码提交生成并校验 Raw 地址;无需额外 `publish.icon`。本地 catalog CLI 使用 `publish` 外部图片,省略时不会导出包内图标/截图,商店采用默认图标。包内资源仍用于已安装插件。 +上述为显式地址示例,`downloadUrl` 并非源码必填字段:GitHub 社区登记可按仓库、`v` 和 `-.framely` 自动生成。包内仍需最终地址,当前 pack 不会自动读取 Git remote;按[发布指南](developer-guide/publishing.md)生成临时清单即可。社区图标通常只需 `icon`,数据库从固定源码提交生成并校验 Raw 地址;无需额外 `publish.icon`。本地 catalog CLI 使用 `publish` 外部图片,省略时不会导出包内图标/截图,插件库采用默认图标。包内资源仍用于已安装插件。 插件数据库只登记 submodule 固定提交,读取该提交的清单,补全默认下载地址,先取得 Release digest 或源码 `downloadSha256` 中的预期整包哈希,再下载并比对 SHA256、清单声明和载荷。发布分支提供各渠道目录及历史版本 JSON,不保存插件包或图片,不需要 entries 登记。数据库 CI 自动展示版本和运行用户变化,发布时禁止同 ID、版本更换 SHA256;不执行作者代码,不要求逐插件人工源码审核,也不验证源码与二进制可复现关系。 -`framely catalog` 也只生成 JSON:下载 URL 优先使用 `downloadUrl`,没有该字段时采用 `--base-url` 加包文件名。图标与截图来自 `publish` 的外部 URL,不复制资源。商店支持关键词、标签、来源、已安装和可更新筛选;源暂时不可用时显示会话缓存。安装使用已经校验的同一份包,不在确认后重新下载。 +`framely catalog` 也只生成 JSON:下载 URL 优先使用 `downloadUrl`,没有该字段时采用 `--base-url` 加包文件名。图标与截图来自 `publish` 的外部 URL,不复制资源。插件库支持关键词、标签、来源、已安装和可更新筛选;源暂时不可用时显示会话缓存。安装使用已经校验的同一份包,不在确认后重新下载。 后端默认按失败自动恢复,包括已经启动的按需插件;可用 `backend.restart: "never"` 关闭。默认第三次连续失败后停用,运行稳定一分钟后重置计数,详见[生命周期](plugin-lifecycle.md)。 @@ -145,8 +145,8 @@ SDK 提供 `Section`、`Button`、`Toggle`、`Slider`、`TextField`、`Select` 插件只使用 `tags` 描述用途,不再使用分类;为兼容已发布的包,旧 `category` 字段读取时忽略,新的清单及目录不再导出它。插件数据库每次生成目录时同时生成 `tags.json`,格式为 `{ "schemaVersion": 1, "tags": ["工具", "温控"] }`,聚合当前目录全部插件的标签、精确去重并排序。stable/testing 分别生成自己的标签文件,不生成跨渠道并集;程序加载多个源后在本地聚合去重。标签随插件更新自动增删,无须手工维护。 -上传的插件包、商店下载及其依赖包暂存于 `/tmp/framely-package-*`,确认安装后才写入插件目录。取消、安装完成或失败后删除暂存包;未完成上传和待确认安装包闲置 15 分钟后过期,每 30 秒清理一次。 +上传的插件包、插件库下载及其依赖包暂存于 `/tmp/framely-package-*`,确认安装后才写入插件目录。取消、安装完成或失败后删除暂存包;未完成上传和待确认安装包闲置 15 分钟后过期,每 30 秒清理一次。 本机网页窗口在 `ui.windows.` 设置 `localWeb: true`,宿主通过后端 `window.get` 获取窗口 URL;这是窗口加载方式,不是权限声明。URL 使用 `http://localhost:<端口>/framely-window/`。普通窗口无需设置此项。 -商店主目录 `catalog.json` 中每个插件 ID 仅出现一次,代表最新版。历史条目存放在相对目录 `plugins/<插件ID>/versions.json`,格式为 `{ "schemaVersion": 1, "id": "作者.插件", "versions": [...] }`;版本条目沿用目录条目的字段,列表不包含主目录的最新版。商店进入详情时按需读取历史,依赖解析仅在最新版不满足范围时读取历史。附加目录字段可扩展,读取端忽略未知字段,但校验已知字段;改变必需语义应升级 `schemaVersion`。插件安装包清单仍严格校验。 +插件库主目录 `catalog.json` 中每个插件 ID 仅出现一次,代表最新版。历史条目存放在相对目录 `plugins/<插件ID>/versions.json`,格式为 `{ "schemaVersion": 1, "id": "作者.插件", "versions": [...] }`;版本条目沿用目录条目的字段,列表不包含主目录的最新版。插件库进入详情时按需读取历史,依赖解析仅在最新版不满足范围时读取历史。附加目录字段可扩展,读取端忽略未知字段,但校验已知字段;改变必需语义应升级 `schemaVersion`。插件安装包清单仍严格校验。 diff --git a/docs/zh-CN/source-subscriptions.md b/docs/zh-CN/source-subscriptions.md index b4be90e..8ce9de0 100644 --- a/docs/zh-CN/source-subscriptions.md +++ b/docs/zh-CN/source-subscriptions.md @@ -19,8 +19,8 @@ 默认每天自动刷新,也可关闭自动刷新或立即刷新。自动检查在 Framely 会话运行期间进行,启动后检查到期项;支持 ETag/Last-Modified 和最多五次 HTTPS 重定向。离线/失败保留已持久化的上次成功源列表,记录错误,按一分钟起的指数退避重试,最多一天。 -新增源自动纳入;名称自动更新。相同 id 的 URL 改变时保留旧地址,显示待确认变更,确认后才采用新地址。刷新订阅后同时刷新插件目录。目录缓存沿用会话内缓存,重启后的离线商店不保证有目录缓存。 +新增源自动纳入;名称自动更新。相同 id 的 URL 改变时保留旧地址,显示待确认变更,确认后才采用新地址。刷新订阅后同时刷新插件目录。目录缓存沿用会话内缓存,重启后的离线插件库不保证有目录缓存。 目录 URL 规范化后去重,一个源可由多个订阅和手动添加共同引用。删除订阅或移除订阅中的源只移除对应引用;没有任何引用才从源列表移除。已安装插件、来源记录和数据保留,不静默切换来源。 -可在插件源列表单独停用源,刷新不会重新开启;即使移除后又加入同一 URL,也保留停用偏好。订阅源的 URL 在待确认变更中管理。刷新只更新源列表、商店资料和更新提示,不自动安装、更新或启用插件。 +可在插件源列表单独停用源,刷新不会重新开启;即使移除后又加入同一 URL,也保留停用偏好。订阅源的 URL 在待确认变更中管理。刷新只更新源列表、插件库资料和更新提示,不自动安装、更新或启用插件。 diff --git a/docs/zh-CN/user-guide/basic-usage.md b/docs/zh-CN/user-guide/basic-usage.md index e68d2a2..7cdc1af 100644 --- a/docs/zh-CN/user-guide/basic-usage.md +++ b/docs/zh-CN/user-guide/basic-usage.md @@ -12,7 +12,7 @@ - **已安装**:查看本机插件,进入插件页面。 - **设置**:语言、安全模式和“管理插件”入口。 -“管理插件”打开独立大窗口,左侧导航进入插件、商店、插件源、设置和关于。安装、更新及来源管理在这里完成;电脑或手机也可使用[网络管理面板](network-panel.md)。 +“管理插件”打开独立大窗口,左侧导航进入插件、插件库、插件源、设置和关于。安装、更新及来源管理在这里完成;电脑或手机也可使用[网络管理面板](network-panel.md)。 ## 插件启停与窗口 diff --git a/docs/zh-CN/user-guide/network-panel.md b/docs/zh-CN/user-guide/network-panel.md index d53a2fc..caa485c 100644 --- a/docs/zh-CN/user-guide/network-panel.md +++ b/docs/zh-CN/user-guide/network-panel.md @@ -24,7 +24,7 @@ http://设备IP:15915 ## 可以做什么 -登录后可以管理来源和订阅、浏览商店、导入本地插件包、确认安装、启停插件、修改设置及检查本体更新。插件后端始终运行在 Frame,上传本地包只是从手机/电脑把文件传到设备。 +登录后可以管理来源和订阅、浏览插件库、导入本地插件包、确认安装、启停插件、修改设置及检查本体更新。插件后端始终运行在 Frame,上传本地包只是从手机/电脑把文件传到设备。 VR 窗口、手柄振动和 SteamVR 虚拟键盘需要头显端验收;浏览器无法替代这些设备交互。部分外部链接由设备默认浏览器打开。 diff --git a/docs/zh-CN/user-guide/plugins.md b/docs/zh-CN/user-guide/plugins.md index 021fe87..893ba86 100644 --- a/docs/zh-CN/user-guide/plugins.md +++ b/docs/zh-CN/user-guide/plugins.md @@ -4,21 +4,21 @@ [文档首页](../README.md) · [网络面板](network-panel.md) -## 从商店安装 +## 从插件库安装 -1. 打开管理窗口“商店”。当前代码首次初始化加入社区 stable/testing 来源;来源内容取决于服务和设备配置。没有目录或插件时,进入“插件源”添加开发者提供的完整 HTTPS `catalog.json` 地址。 +1. 打开管理窗口“插件库”。当前代码首次初始化加入社区 stable/testing 来源;来源内容取决于服务和设备配置。没有目录或插件时,进入“插件源”添加开发者提供的完整 HTTPS `catalog.json` 地址。 2. 可按关键词、标签、来源或安装状态筛选。 3. 打开详情,查看作者、介绍、更新记录、运行用户和依赖。默认显示推荐版本,历史版本会按需加载。 4. 点击安装或检查所选版本。下载、校验和依赖解析完成后,阅读确认页。 5. 检查包内实际运行用户以及依赖带来的 root 权限,确认后安装。安装后在插件列表启用或打开。 -商店提供的运行用户只是目录资料,最终确认以下载包内声明为准。`steamos` 使用 Steam 会话用户,`root` 可修改系统和设备设置。包哈希用于校验内容,不等于对作者身份或安全性的担保。 +插件库提供的运行用户只是目录资料,最终确认以下载包内声明为准。`steamos` 使用 Steam 会话用户,`root` 可修改系统和设备设置。包哈希用于校验内容,不等于对作者身份或安全性的担保。 ## 添加源与订阅 单个目录地址:在“插件源”添加名称和 `https://.../catalog.json`,测试连接后保存。 -多个源的订阅:在“源订阅”粘贴开发者提供的订阅 JSON 地址,预览所含来源后确认添加。它和单个目录不是同一种文件。订阅刷新仅更新来源和商店资料,不自动安装或更新插件。 +多个源的订阅:在“源订阅”粘贴开发者提供的订阅 JSON 地址,预览所含来源后确认添加。它和单个目录不是同一种文件。订阅刷新仅更新来源和插件库资料,不自动安装或更新插件。 停用或删除来源不会自动卸载已安装插件;订阅管理的 URL 变更需另行确认。格式与刷新行为见[源订阅参考](../source-subscriptions.md)。 @@ -41,7 +41,7 @@ sudo /var/lib/framely/current/bin/framely install ./plugin.framely --approve ## 更新、选择旧版与卸载 -商店提示“可更新”时进入详情,查看变化后按安装确认流程更新。需要旧版时,在详情选择历史版本再检查;换来源也会明确提示。设置和收藏通常保留,但运行用户改变会使用另一份数据目录,不自动迁移旧身份的数据。 +插件库提示“可更新”时进入详情,查看变化后按安装确认流程更新。需要旧版时,在详情选择历史版本再检查;换来源也会明确提示。设置和收藏通常保留,但运行用户改变会使用另一份数据目录,不自动迁移旧身份的数据。 从插件管理弹窗卸载。卸载钩子负责清理该插件的外部资源,保存的数据保留;不要直接删除系统中的插件目录。停用只停止使用,不卸载程序和数据。 @@ -49,7 +49,7 @@ sudo /var/lib/framely/current/bin/framely install ./plugin.framely --approve | 现象 | 处理 | | --- | --- | -| 商店空白 | 检查是否添加并启用了来源、连接能否成功、筛选是否过窄 | +| 插件库空白 | 检查是否添加并启用了来源、连接能否成功、筛选是否过窄 | | 下载或校验失败 | 检查网络和包地址,重新下载;不要跳过哈希校验 | | 历史版本不可用 | 来源可能没有历史文件,可先安装最新版或导入可信旧包 | | 插件无法启用 | 查看依赖、冲突和错误说明,确认其他必要插件已安装 | diff --git a/native/brand_mask.h b/native/brand_mask.h index cf580dc..4b22338 100644 --- a/native/brand_mask.h +++ b/native/brand_mask.h @@ -12,34 +12,34 @@ inline constexpr unsigned char mask[48 * 48] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,255,255,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,255,255,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, - 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,255,255,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,255,255,0,0,0,255,255,255,255,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,255,255,0,0,0,255,255,255,255,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,255,255,255,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,255,255,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255,0,0,0,0,0,0,0,255,255,255,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, diff --git a/native/host.cpp b/native/host.cpp index 389453c..c9d7456 100644 --- a/native/host.cpp +++ b/native/host.cpp @@ -82,8 +82,8 @@ static std::vector icon(bool hover,bool active){ int gx=std::min(47,int((x+.5f)*48/128)),gy=std::min(47,int((y+.5f)*48/128)); float mark=framely_brand::mask[gy*48+gx]/255.f; int i=(y*128+x)*4; - const int normal[3]={23,25,28},highlight[3]={61,68,80},muted[3]={147,197,237},bright[3]={240,241,243}; - for(int c=0;c<3;c++){float bg=(hover||active)?highlight[c]:normal[c];float fg=(hover||active)?bright[c]:muted[c];p[i+c]=uint8_t(bg+(fg-bg)*mark);} + const int normal[3]={23,25,28},highlight[3]={32,35,40},blue[3]={147,197,237}; + for(int c=0;c<3;c++){float bg=(hover||active)?highlight[c]:normal[c];p[i+c]=uint8_t(bg+(blue[c]-bg)*mark);} // 32px-wide, 6px-high tab indicator is clipped to its upper 3px by the BarSurface. if(active){float qx=std::abs(x-63.5f)-20.8f,qy=std::abs(y-129.6f);float line=std::clamp(5.3f-std::hypot(std::max(qx,0.f),qy),0.f,1.f);const int blue[3]={26,159,255};for(int c=0;c<3;c++)p[i+c]=uint8_t(p[i+c]+(blue[c]-p[i+c])*line);} p[i+3]=uint8_t(255*coverage); @@ -177,7 +177,7 @@ static void plugin_thumbnail(View& view,const std::string& encoded){ for(int y=0;ySetOverlayRaw(view.thumbnail,pixels.data(),128,128,4); } -static void commands(){std::deque commands;{std::lock_guard lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="haptic"){auto key=c.value("view","");if(views.count(key)&&views.at(key)->shown&&!views.at(key)->closing)hover_haptic(views.at(key)->overlay,views.at(key)->haptic_at);}else if(kind=="manager.open"){if(views.count("framely.manager")&&views.at("framely.manager")->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}create_view("framely.manager","Framely 插件管理",origin+"/manager",true,1440,810);menu_open=false;}else if(kind=="agreement.decline"){auto key=c.value("view","");if(key=="menu"){menu_open=false;if(views.count("menu")){show(*views.at("menu"),false);views.at("menu")->browser->reload(views.at("menu")->browser);}}else if(views.count(key))close_view(*views.at(key));}else if(kind=="manager.close"){if(views.count("framely.manager"))close_view(*views.at("framely.manager"));}else if(kind=="menu.close"){menu_open=false;}else if(kind=="window.open"){std::string plugin=c.at("plugin"),win=c.at("window");auto key="framely.window."+plugin+"."+win;if(views.count(key)&&views.at(key)->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto spec=c.at("spec");auto& opened=create_view("framely.window."+plugin+"."+win,spec.at("title"),origin+"/window/"+plugin+"/"+win,spec.value("dockIcon",false),spec.value("width",1600),spec.value("height",900),plugin,spec.value("widthMeters",3.f));plugin_thumbnail(opened,spec.value("iconData",std::string{}));menu_open=false;}else if(kind=="window.close"){auto key="framely.window."+c.at("plugin").get()+"."+c.at("window").get();if(views.count(key))close_view(*views.at(key));}else if(kind=="plugin.disabled"){for(auto&[key,v]:views)if(v->plugin==c.at("plugin"))close_view(*v);}else if(kind=="notification.changed"){auto& v=create_view("notifications","Framely notifications",origin+"/notifications",false,600,360);anchor_to_head(v,.42f,.42f,.32f,-.8f);v.host->was_hidden(v.host,0);v.host->invalidate(v.host,PET_VIEW);}else if(kind=="keyboard"){auto key=c.value("view","menu");if(!views.count(key))continue;auto& v=*views.at(key);if(v.closing)continue;close_keyboard();v.host->set_focus(v.host,1);keyboard_frame=v.browser->get_focused_frame(v.browser);keyboard_view=key;keyboard_target=v.overlay;++keyboard_token;auto existing=c.value("existing","");if(key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);auto e=overlays->ShowKeyboardForOverlay(v.overlay,c.value("password",false)?vr::k_EGamepadTextInputModePassword:vr::k_EGamepadTextInputModeNormal,c.value("multiline",false)?vr::k_EGamepadTextInputLineModeMultipleLines:vr::k_EGamepadTextInputLineModeSingleLine,vr::KeyboardFlag_Modal|vr::KeyboardFlag_Minimal|vr::KeyboardFlag_ShowArrowKeys,"Framely",4096,existing.c_str(),keyboard_token);keyboard_open=e==vr::VROverlayError_None;keyboard_menu.begin(keyboard_open&&key=="menu");if(!keyboard_open)close_keyboard();}else if(kind=="notifications.empty"){if(views.count("notifications"))show(*views.at("notifications"),false);}}catch(const std::exception& e){std::cerr<<"Native command error: "< commands;{std::lock_guard lock(queue_mutex);commands.swap(queue);}for(const auto& c:commands){try{auto kind=c.value("kind","");if(kind=="haptic"){auto key=c.value("view","");if(views.count(key)&&views.at(key)->shown&&!views.at(key)->closing)hover_haptic(views.at(key)->overlay,views.at(key)->haptic_at);}else if(kind=="manager.open"){if(views.count("framely.manager")&&views.at("framely.manager")->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto page=c.value("page",std::string{});auto& manager=create_view("framely.manager","Framely 插件管理",origin+"/manager"+(page=="catalog"?"#catalog":""),true,1440,810);if(page=="catalog"){auto* frame=manager.browser->get_main_frame(manager.browser);execute(frame,"location.hash=\"catalog\";window.dispatchEvent(new Event(\"hashchange\"))");frame->base.release(&frame->base);}menu_open=false;}else if(kind=="agreement.decline"){auto key=c.value("view","");if(key=="menu"){menu_open=false;if(views.count("menu")){show(*views.at("menu"),false);views.at("menu")->browser->reload(views.at("menu")->browser);}}else if(views.count(key))close_view(*views.at(key));}else if(kind=="manager.close"){if(views.count("framely.manager"))close_view(*views.at("framely.manager"));}else if(kind=="menu.close"){menu_open=false;}else if(kind=="window.open"){std::string plugin=c.at("plugin"),win=c.at("window");auto key="framely.window."+plugin+"."+win;if(views.count(key)&&views.at(key)->closing){std::lock_guard lock(queue_mutex);queue.push_back(c);continue;}auto spec=c.at("spec");auto& opened=create_view("framely.window."+plugin+"."+win,spec.at("title"),origin+"/window/"+plugin+"/"+win,spec.value("dockIcon",false),spec.value("width",1600),spec.value("height",900),plugin,spec.value("widthMeters",3.f));plugin_thumbnail(opened,spec.value("iconData",std::string{}));menu_open=false;}else if(kind=="window.close"){auto key="framely.window."+c.at("plugin").get()+"."+c.at("window").get();if(views.count(key))close_view(*views.at(key));}else if(kind=="plugin.disabled"){for(auto&[key,v]:views)if(v->plugin==c.at("plugin"))close_view(*v);}else if(kind=="notification.changed"){auto& v=create_view("notifications","Framely notifications",origin+"/notifications",false,600,360);anchor_to_head(v,.42f,.42f,.32f,-.8f);v.host->was_hidden(v.host,0);v.host->invalidate(v.host,PET_VIEW);}else if(kind=="keyboard"){auto key=c.value("view","menu");if(!views.count(key))continue;auto& v=*views.at(key);if(v.closing)continue;close_keyboard();v.host->set_focus(v.host,1);keyboard_frame=v.browser->get_focused_frame(v.browser);keyboard_view=key;keyboard_target=v.overlay;++keyboard_token;auto existing=c.value("existing","");if(key=="menu")overlays->SetOverlayFlag(v.overlay,vr::VROverlayFlags_WantsModalBehavior,false);auto e=overlays->ShowKeyboardForOverlay(v.overlay,c.value("password",false)?vr::k_EGamepadTextInputModePassword:vr::k_EGamepadTextInputModeNormal,c.value("multiline",false)?vr::k_EGamepadTextInputLineModeMultipleLines:vr::k_EGamepadTextInputLineModeSingleLine,vr::KeyboardFlag_Modal|vr::KeyboardFlag_Minimal|vr::KeyboardFlag_ShowArrowKeys,"Framely",4096,existing.c_str(),keyboard_token);keyboard_open=e==vr::VROverlayError_None;keyboard_menu.begin(keyboard_open&&key=="menu");if(!keyboard_open)close_keyboard();}else if(kind=="notifications.empty"){if(views.count("notifications"))show(*views.at("notifications"),false);}}catch(const std::exception& e){std::cerr<<"Native command error: "<get_main_frame(v.browser);execute(frame,"window.dispatchEvent(new Event('framely.back'))");frame->base.release(&frame->base);}break;case vr::VREvent_FocusLeave:v.host->send_mouse_move_event(v.host,&mouse,1);break;case vr::VREvent_MouseMove:v.host->send_mouse_move_event(v.host,&mouse,0);break;case vr::VREvent_MouseButtonDown:case vr::VREvent_MouseButtonUp:{v.host->set_focus(v.host,1);auto button=e.data.mouse.button==vr::VRMouseButton_Right?MBT_RIGHT:MBT_LEFT;mouse.modifiers=e.eventType==vr::VREvent_MouseButtonDown?EVENTFLAG_LEFT_MOUSE_BUTTON:0;v.host->send_mouse_click_event(v.host,&mouse,button,e.eventType==vr::VREvent_MouseButtonUp,1);break;}case vr::VREvent_ScrollSmooth:case vr::VREvent_ScrollDiscrete:mouse.x=v.width/2;mouse.y=v.height/2;v.host->send_mouse_wheel_event(v.host,&mouse,int(e.data.scroll.xdelta*120),int(e.data.scroll.ydelta*120));break;case vr::VREvent_OverlayGamepadFocusLost:if(!keyboard_open&&!v.dashboard&&v.key!="menu"&&v.key!="notifications")close_view(v);break;default:break;}} diff --git a/src/session.rs b/src/session.rs index 00de839..c7cc0ed 100644 --- a/src/session.rs +++ b/src/session.rs @@ -695,7 +695,11 @@ impl Agent { "host.manager.close" => "manager.close", _ => "menu.close", }; - push(&self.commands, json!({"kind":kind})); + let mut command = json!({"kind":kind}); + if method == "host.manager.open" && p["page"] == "catalog" { + command["page"] = json!("catalog"); + } + push(&self.commands, command); Ok(json!(true)) } "network.status" => Ok( @@ -1464,6 +1468,25 @@ mod tests { }) } #[test] + fn manager_store_entry_preserves_catalog_target_and_rejects_arbitrary_pages() { + let root = tempfile::tempdir().unwrap(); + let server = Server::http("127.0.0.1:0").unwrap(); + let agent = agent(&server, root.path()); + for params in [ + json!({"page":"catalog"}), + json!({}), + json!({"page":"https://example.org"}), + ] { + agent + .api(json!({"method":"host.manager.open","params":params})) + .unwrap(); + } + let commands = agent.commands.lock().unwrap(); + assert_eq!(commands[0], json!({"kind":"manager.open","page":"catalog"})); + assert_eq!(commands[1], json!({"kind":"manager.open"})); + assert_eq!(commands[2], json!({"kind":"manager.open"})); + } + #[test] fn declining_agreement_only_closes_current_view() { let root = tempfile::tempdir().unwrap(); let server = Server::http("127.0.0.1:0").unwrap(); diff --git a/tests/browser_probe.cpp b/tests/browser_probe.cpp index f64764f..87393e5 100644 --- a/tests/browser_probe.cpp +++ b/tests/browser_probe.cpp @@ -37,6 +37,6 @@ int main(int argc,char** argv){ client.api.get_render_handler=[](cef_client_t*){return render.acquire();};client.api.get_display_handler=[](cef_client_t*){return display.acquire();};client.api.get_life_span_handler=[](cef_client_t*){return life.acquire();}; cef_window_info_t win{};win.size=sizeof(win);win.windowless_rendering_enabled=1;cef_browser_settings_t bs{};bs.size=sizeof(bs);bs.windowless_frame_rate=30;cef_string_t uri{};str(uri,url);auto* browser=cef_browser_host_create_browser_sync(&win,&client.api,&uri,&bs,nullptr,nullptr);cef_string_utf16_clear(&uri);if(!browser)return 4;auto* host=browser->get_host(browser); auto end=std::chrono::steady_clock::now()+std::chrono::seconds(20);bool injected=false; - while(std::chrono::steady_clock::now()get_main_frame(browser);cef_string_t code{},source{};str(code,install_review_mode?"window.runInstallReviewChecks()":notification_mode?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));for(let i=0;i<100&&!document.querySelector('.toast');i++)await wait(30);if(innerWidth!==600||innerHeight!==360||devicePixelRatio!==2)throw Error('notification dimensions');const toast=document.querySelector('.toast');if(!toast?.textContent.includes('Toast integration'))throw Error('notification missing');if(toast.getBoundingClientRect().height>240)throw Error('notification too large');if(toast.querySelectorAll('.toast-actions button').length!==3)throw Error('actions missing');toast.querySelector('[aria-label="确认"]').click();await wait(150);document.querySelector('[aria-label="关闭通知"]').click();await wait(150);const status=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(status.result.notifications.length)throw Error('notification not withdrawn');console.log('FRAMELY_BRIDGE_PASS');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_REMOTE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);const button=document.createElement('button');button.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';button.onclick=()=>console.log('FRAMELY_DPI_CLICK_PASS');document.body.append(button);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);button.remove();for(let i=0;i<100;i++){const plugin=document.querySelector('.plugin-main');if(plugin){plugin.click();return;}await wait(50);}throw Error('remote plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_STORE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));const until=async find=>{for(let i=0;i<160;i++){const value=find();if(value)return value;await wait(50);}throw Error("store timeout "+document.body.innerText);};await wait(300);if(innerWidth!==1440||innerHeight!==810||devicePixelRatio!==4)throw Error("manager layout or DPI mismatch");[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();await wait(200);if(!document.body.textContent.includes("Subscription fixture"))throw Error("subscription settings missing");const autoRefresh=[...document.querySelectorAll("button")].find(b=>b.textContent==="关闭自动刷新");if(!autoRefresh)throw Error("subscription automatic refresh missing");autoRefresh.click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="开启自动刷新"));[...document.querySelectorAll("nav button")].find(b=>b.textContent==="关于").click();await wait(150);const updates=document.querySelector(".system-updates");if(!updates||updates.querySelector("input"))throw Error("release configuration leaked into user settings");if(!updates.textContent.includes("尚未配置更新服务"))throw Error("missing unconfigured update service message");const check=[...updates.querySelectorAll("button")].find(b=>b.textContent==="检查更新");if(!check?.disabled)throw Error("unconfigured check enabled");[...document.querySelectorAll("button")].find(b=>b.textContent==="查看协议与隐私声明").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议"));document.querySelector('.language-picker button').click();const english=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English'));english.click();await until(()=>document.querySelector('h1')?.textContent==='Before using Framely');document.querySelector('.language-picker button').click();const chinese=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='简体中文'));chinese.click();await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='拒绝协议'));[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议").click();await until(()=>document.body.textContent.includes("拒绝协议将停用全部插件"));[...document.querySelectorAll("button")].find(b=>b.textContent==="取消").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于"));[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于").click();await until(()=>document.querySelector(".system-updates"));[...document.querySelectorAll("nav button")].find(b=>b.textContent.includes("插件商店")).click();const card=await until(()=>document.querySelector(".store-card"));if(!card.textContent.includes("可更新"))throw Error("update not detected");if(card.querySelector(".plugin-avatar").getBoundingClientRect().width<64)throw Error("store icon not enlarged");if(document.querySelectorAll(".store-card").length!==1)throw Error("versions duplicated plugin cards");document.querySelector(".framely-select button").click();const tag=await until(()=>[...document.querySelectorAll("[role=option]")].find(b=>b.textContent==="工具"));if(!tag)throw Error("tag missing");tag.click();await wait(100);if(document.querySelector(".store-card .run-user-badge")?.textContent!=="root")throw Error("store run user missing");document.querySelector(".store-card").click();await wait(100);if(!document.body.textContent.includes("Store integration details"))throw Error("detail missing");if(!document.querySelector(".store-detail .run-user-notice.is-root"))throw Error("store root warning missing");console.log("FRAMELY_PREVIEW_STORE");document.querySelector(".detail-heading button").click();const approve=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="确认安装"));if(document.querySelector(".modal h2")?.textContent!=="更新 Bridge")throw Error("manual install update title missing");if(document.querySelector(".install-review .install-account")?.textContent!=="steamos")throw Error("install must use package identity");if(document.body.textContent.includes("权限变化"))throw Error("unexpected privilege change");approve.click();await until(()=>!document.querySelector(".modal"));const status=await fetch("/api",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"status",params:{}})}).then(r=>r.json());if(status.result.database.plugins["test.bridge"].manifest.version!=="2.0.0")throw Error("install not persisted");const chooseVersion=async v=>{document.querySelector('.store-detail .framely-select button').click();const option=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent.split(' · ')[0]===v));option.click();await wait(100);if(document.querySelector('.store-detail .install-account')?.textContent!==(v==='2.0.0'?'root':'steamos'))throw Error('selected version user incorrect');document.querySelector('.detail-heading button').click();const approveVersion=await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='确认安装'));approveVersion.click();await until(()=>!document.querySelector('.modal'));const result=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(result.result.database.plugins['test.bridge'].manifest.version!==v)throw Error('selected version not installed '+v);};await chooseVersion('1');await chooseVersion('2.0.0');console.log('FRAMELY_VERSION_SELECTION_PASS');[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();const removeSub=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="删除订阅"));removeSub.click();const confirm=await until(()=>[...document.querySelectorAll(".modal button")].find(b=>b.textContent==="确认"));confirm.click();await until(()=>!document.body.textContent.includes("Subscription fixture"));console.log("FRAMELY_BRIDGE_PASS");})().catch(e=>console.error("FRAMELY_BRIDGE_FAIL "+e)))JS":R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);if(innerWidth!==600||innerHeight!==840||devicePixelRatio!==2)throw Error('quick layout or DPI changed');const dpiButton=document.createElement('button');dpiButton.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';dpiButton.textContent='DPI test';let clicked=false;dpiButton.onclick=()=>{clicked=true;console.log('FRAMELY_DPI_CLICK_PASS');};document.body.append(dpiButton);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);dpiButton.remove();if(!clicked)throw Error('logical pointer coordinates failed');const originalFetch=window.fetch;let pulses=0;window.fetch=function(url,init){if(init?.body&&JSON.parse(init.body).method==='host.haptic')pulses++;return originalFetch.call(this,url,init);};const fixture=document.createElement('div');fixture.innerHTML='';document.body.append(fixture);const children=fixture.querySelectorAll('span'),buttons=fixture.querySelectorAll('button');const over=e=>e.dispatchEvent(new PointerEvent('pointerover',{bubbles:true}));const leave=(e,target)=>e.dispatchEvent(new PointerEvent('pointerout',{bubbles:true,relatedTarget:target}));over(children[0]);leave(children[0],children[1]);over(children[1]);leave(children[1],children[2]);over(children[2]);leave(children[2],buttons[2]);over(buttons[2]);await wait(100);fixture.remove();window.fetch=originalFetch;if(pulses!==2)throw Error('hover count='+pulses);console.log('FRAMELY_HOVER_PASS');console.log('FRAMELY_PREVIEW_FAVORITES');[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置').click();await wait(200);console.log('FRAMELY_PREVIEW_SETTINGS');if(!document.querySelector('.quick-settings').firstElementChild.classList.contains('setting-entry'))throw Error('manager entry is below language settings');document.querySelector('.setting-entry').click();await wait(100);[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装').click();await wait(200);console.log('FRAMELY_PREVIEW_INSTALLED');for(let i=0;i<100;i++){await wait(50);const b=document.querySelector('.plugin-main');if(b){b.click();return;}}console.error('FRAMELY_BRIDGE_FAIL manager plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS");f->execute_java_script(f,&code,&source,1);cef_string_utf16_clear(&code);f->base.release(&f->base);injected=true;}std::this_thread::sleep_for(std::chrono::milliseconds(5));} + while(std::chrono::steady_clock::now()get_main_frame(browser);cef_string_t code{},source{};str(code,install_review_mode?"window.runInstallReviewChecks()":notification_mode?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));for(let i=0;i<100&&!document.querySelector('.toast');i++)await wait(30);if(innerWidth!==600||innerHeight!==360||devicePixelRatio!==2)throw Error('notification dimensions');const toast=document.querySelector('.toast');if(!toast?.textContent.includes('Toast integration'))throw Error('notification missing');if(toast.getBoundingClientRect().height>240)throw Error('notification too large');if(toast.querySelectorAll('.toast-actions button').length!==3)throw Error('actions missing');toast.querySelector('[aria-label="确认"]').click();await wait(150);document.querySelector('[aria-label="关闭通知"]').click();await wait(150);const status=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(status.result.notifications.length)throw Error('notification not withdrawn');console.log('FRAMELY_BRIDGE_PASS');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_REMOTE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);const button=document.createElement('button');button.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';button.onclick=()=>console.log('FRAMELY_DPI_CLICK_PASS');document.body.append(button);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);button.remove();for(let i=0;i<100;i++){const plugin=document.querySelector('.plugin-main');if(plugin){plugin.click();return;}await wait(50);}throw Error('remote plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS":std::getenv("FRAMELY_STORE_TEST")?R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));const until=async find=>{for(let i=0;i<160;i++){const value=find();if(value)return value;await wait(50);}throw Error("store timeout "+document.body.innerText);};await wait(300);if(innerWidth!==1440||innerHeight!==810||devicePixelRatio!==4)throw Error("manager layout or DPI mismatch");[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();await wait(200);if(!document.body.textContent.includes("Subscription fixture"))throw Error("subscription settings missing");const autoRefresh=[...document.querySelectorAll("button")].find(b=>b.textContent==="关闭自动刷新");if(!autoRefresh)throw Error("subscription automatic refresh missing");autoRefresh.click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="开启自动刷新"));[...document.querySelectorAll("nav button")].find(b=>b.textContent==="关于").click();await wait(150);const updates=document.querySelector(".system-updates");if(!updates||updates.querySelector("input"))throw Error("release configuration leaked into user settings");if(!updates.textContent.includes("尚未配置更新服务"))throw Error("missing unconfigured update service message");const check=[...updates.querySelectorAll("button")].find(b=>b.textContent==="检查更新");if(!check?.disabled)throw Error("unconfigured check enabled");[...document.querySelectorAll("button")].find(b=>b.textContent==="查看协议与隐私声明").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议"));document.querySelector('.language-picker button').click();const english=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='English'));english.click();await until(()=>document.querySelector('h1')?.textContent==='Before using Framely');document.querySelector('.language-picker button').click();const chinese=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent==='简体中文'));chinese.click();await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='拒绝协议'));[...document.querySelectorAll("button")].find(b=>b.textContent==="拒绝协议").click();await until(()=>document.body.textContent.includes("拒绝协议将停用全部插件"));[...document.querySelectorAll("button")].find(b=>b.textContent==="取消").click();await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于"));[...document.querySelectorAll("button")].find(b=>b.textContent==="返回关于").click();await until(()=>document.querySelector(".system-updates"));[...document.querySelectorAll("nav button")].find(b=>b.textContent.includes("插件库")).click();const card=await until(()=>document.querySelector(".store-card"));if(!card.textContent.includes("可更新"))throw Error("update not detected");if(card.querySelector(".plugin-avatar").getBoundingClientRect().width<64)throw Error("store icon not enlarged");if(document.querySelectorAll(".store-card").length!==1)throw Error("versions duplicated plugin cards");document.querySelector(".framely-select button").click();const tag=await until(()=>[...document.querySelectorAll("[role=option]")].find(b=>b.textContent==="工具"));if(!tag)throw Error("tag missing");tag.click();await wait(100);if(document.querySelector(".store-card .run-user-badge")?.textContent!=="root")throw Error("store run user missing");document.querySelector(".store-card").click();await wait(100);if(!document.body.textContent.includes("Store integration details"))throw Error("detail missing");if(!document.querySelector(".store-detail .run-user-notice.is-root"))throw Error("store root warning missing");console.log("FRAMELY_PREVIEW_STORE");document.querySelector(".detail-heading button").click();const approve=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="确认安装"));if(document.querySelector(".modal h2")?.textContent!=="更新 Bridge")throw Error("manual install update title missing");if(document.querySelector(".install-review .install-account")?.textContent!=="steamos")throw Error("install must use package identity");if(document.body.textContent.includes("权限变化"))throw Error("unexpected privilege change");approve.click();await until(()=>!document.querySelector(".modal"));const status=await fetch("/api",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({method:"status",params:{}})}).then(r=>r.json());if(status.result.database.plugins["test.bridge"].manifest.version!=="2.0.0")throw Error("install not persisted");const chooseVersion=async v=>{document.querySelector('.store-detail .framely-select button').click();const option=await until(()=>[...document.querySelectorAll('[role=option]')].find(b=>b.textContent.split(' · ')[0]===v));option.click();await wait(100);if(document.querySelector('.store-detail .install-account')?.textContent!==(v==='2.0.0'?'root':'steamos'))throw Error('selected version user incorrect');document.querySelector('.detail-heading button').click();const approveVersion=await until(()=>[...document.querySelectorAll('button')].find(b=>b.textContent==='确认安装'));approveVersion.click();await until(()=>!document.querySelector('.modal'));const result=await fetch('/api',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({method:'status',params:{}})}).then(r=>r.json());if(result.result.database.plugins['test.bridge'].manifest.version!==v)throw Error('selected version not installed '+v);};await chooseVersion('1');await chooseVersion('2.0.0');console.log('FRAMELY_VERSION_SELECTION_PASS');[...document.querySelectorAll("nav button")].find(b=>b.textContent==="插件源").click();const removeSub=await until(()=>[...document.querySelectorAll("button")].find(b=>b.textContent==="删除订阅"));removeSub.click();const confirm=await until(()=>[...document.querySelectorAll(".modal button")].find(b=>b.textContent==="确认"));confirm.click();await until(()=>!document.body.textContent.includes("Subscription fixture"));console.log("FRAMELY_BRIDGE_PASS");})().catch(e=>console.error("FRAMELY_BRIDGE_FAIL "+e)))JS":R"JS((async()=>{const wait=ms=>new Promise(r=>setTimeout(r,ms));await wait(300);if(innerWidth!==600||innerHeight!==840||devicePixelRatio!==2)throw Error('quick layout or DPI changed');const dpiButton=document.createElement('button');dpiButton.style.cssText='position:fixed;left:20px;top:20px;width:100px;height:50px;z-index:9999';dpiButton.textContent='DPI test';let clicked=false;dpiButton.onclick=()=>{clicked=true;console.log('FRAMELY_DPI_CLICK_PASS');};document.body.append(dpiButton);console.log('FRAMELY_DPI_CLICK_REQUEST');await wait(200);dpiButton.remove();if(!clicked)throw Error('logical pointer coordinates failed');const originalFetch=window.fetch;let pulses=0;window.fetch=function(url,init){if(init?.body&&JSON.parse(init.body).method==='host.haptic')pulses++;return originalFetch.call(this,url,init);};const fixture=document.createElement('div');fixture.innerHTML='';document.body.append(fixture);const children=fixture.querySelectorAll('span'),buttons=fixture.querySelectorAll('button');const over=e=>e.dispatchEvent(new PointerEvent('pointerover',{bubbles:true}));const leave=(e,target)=>e.dispatchEvent(new PointerEvent('pointerout',{bubbles:true,relatedTarget:target}));over(children[0]);leave(children[0],children[1]);over(children[1]);leave(children[1],children[2]);over(children[2]);leave(children[2],buttons[2]);over(buttons[2]);await wait(100);fixture.remove();window.fetch=originalFetch;if(pulses!==2)throw Error('hover count='+pulses);console.log('FRAMELY_HOVER_PASS');console.log('FRAMELY_PREVIEW_FAVORITES');[...document.querySelectorAll('nav button')].find(b=>b.textContent==='设置').click();await wait(200);console.log('FRAMELY_PREVIEW_SETTINGS');if(!document.querySelector('.quick-settings').firstElementChild.classList.contains('setting-entry'))throw Error('manager entry is below language settings');document.querySelector('.setting-entry').click();await wait(100);[...document.querySelectorAll('nav button')].find(b=>b.textContent==='已安装').click();await wait(200);console.log('FRAMELY_PREVIEW_INSTALLED');for(let i=0;i<100;i++){await wait(50);const b=document.querySelector('.plugin-main');if(b){b.click();return;}}console.error('FRAMELY_BRIDGE_FAIL manager plugin list timeout');})().catch(e=>console.error('FRAMELY_BRIDGE_FAIL '+e)))JS");f->execute_java_script(f,&code,&source,1);cef_string_utf16_clear(&code);f->base.release(&f->base);injected=true;}std::this_thread::sleep_for(std::chrono::milliseconds(5));} host->close_browser(host,1);for(int i=0;i<400&&!closed;i++){cef_do_message_loop_work();std::this_thread::sleep_for(std::chrono::milliseconds(5));}bool ok=passed&&!failed&&paints>0&&closed&&scaled_paint&&(notification_mode||rounded_paint)&&(notification_mode||install_review_mode||std::getenv("FRAMELY_STORE_TEST")||dpi_click);std::cout<<"BROWSER_RESULT pass="<base.release(&host->base);browser->base.release(&browser->base);cef_shutdown();return ok?0:5; } diff --git a/tools/build-branding.mjs b/tools/build-branding.mjs index 27284c1..f53f520 100644 --- a/tools/build-branding.mjs +++ b/tools/build-branding.mjs @@ -7,7 +7,8 @@ import {Resvg} from '@resvg/resvg-js'; const root = new URL('../', import.meta.url); const branding = new URL('assets/branding/', root); const app = await readFile(new URL('framely-app-icon.svg', branding)); -const mark = await readFile(new URL('framely-mark.svg', branding)); +const mark = await readFile(new URL('framely-mark.svg', branding), 'utf8'); +await writeFile(new URL('framely-mark-light.svg', branding), mark.replace('color="#17191c"', 'color="#93c5ed"')); const logo = await readFile(new URL('framely-logo.svg', branding), 'utf8'); await writeFile(new URL('framely-logo-light.svg', branding), logo.replace('color="#17191c"', 'color="#93c5ed"')); const sizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; @@ -39,7 +40,10 @@ await writeFile(new URL('framely.ico', branding), Buffer.concat([directory, ...i // A small alpha mask is enough for the native pixel-art Dock renderer. This // keeps the same SVG geometry without adding a runtime SVG/PNG dependency. -const rendered = new Resvg(mark, {fitTo:{mode:'width', value:48}}).render(); +// Keep the body centered while the left tail extends beyond it. Expand the +// viewBox by 1 / 0.9 so the Dock glyph is 10% smaller than the standard mark. +const dockMark = mark.replace('viewBox="0 0 24 24"', 'viewBox="-1.333333 -1.333333 26.666666 26.666666"'); +const rendered = new Resvg(dockMark, {fitTo:{mode:'width', value:48}}).render(); const mask = Array.from({length:48}, (_, y) => ' ' + Array.from({length:48}, (_, x) => rendered.pixels[(y * 48 + x) * 4 + 3]).join(',')).join(',\n'); await mkdir(new URL('native/', root), {recursive:true}); await writeFile(new URL('native/brand_mask.h', root), `// Generated by tools/build-branding.mjs from assets/branding/framely-mark.svg.\n#pragma once\nnamespace framely_brand {\ninline constexpr unsigned char mask[48 * 48] = {\n${mask}\n};\n}\n`); diff --git a/ui/locales/en-US.json b/ui/locales/en-US.json index 8a48402..7b5723f 100644 --- a/ui/locales/en-US.json +++ b/ui/locales/en-US.json @@ -9,7 +9,7 @@ "使用 Framely 前": "Before using Framely", "用户协议": "Terms of use", "Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。": "Framely provides centralized installation, management, execution and updates for plugins. Third-party plugins are provided by their respective developers, who are responsible for their features, content and behavior.", - "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件商店收录、下载校验或权限提示不代表对插件安全性的认可或担保。": "Framely does not guarantee the safety, reliability or compatibility of third-party plugins. Store listings, download verification and permission notices do not constitute approval or a guarantee of plugin safety.", + "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件库收录、下载校验或权限提示不代表对插件安全性的认可或担保。": "Framely does not guarantee the safety, reliability or compatibility of third-party plugins. Plugin library listings, download verification and permission notices do not constitute approval or a guarantee of plugin safety.", "请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。": "Evaluate plugin sources, safety and requested permissions before installing, authorizing or running them, and back up important data. Plugins may read or modify data, access the network or change device settings. Granting elevated permissions may affect your system and device.", "在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。": "To the extent permitted by applicable law, Framely and its developers are not liable for data loss, privacy breaches, device failures or other losses caused by installing, authorizing, running, updating or uninstalling third-party plugins.", "本协议不排除或限制法律规定不得免除或限制的责任及用户权利。": "These terms do not exclude or limit any liability or user rights that cannot be excluded or limited by law.", @@ -61,7 +61,7 @@ "管理": "Manage", "插件已停用、处于安全模式或没有快捷页面。": "The plugin is disabled, safe mode is active, or no quick page is available.", "主导航": "Main navigation", - "插件商店": "Plugin store", + "插件库": "Plugin library", "插件源": "Plugin sources", "设置": "Settings", "关于": "About", @@ -190,7 +190,7 @@ "较旧版本": "Older version", "不同版本": "Different version", "全部": "All", - "返回商店": "Back to store", + "返回插件库": "Back to library", "更新": "Update", "检查此版本": "Inspect this version", "当前安装来自其他源,确认安装此版本后会更换来源。": "The current installation is from another source. Installing this version will change the source.", @@ -209,7 +209,7 @@ "管理插件源": "Manage sources", "刷新中…": "Refreshing…", "刷新": "Refresh", - "搜索商店插件": "Search store plugins", + "搜索插件库": "Search plugin library", "搜索名称、作者或标签": "Search name, author or tags", "标签": "Tags", "所有来源": "All sources", @@ -347,7 +347,7 @@ " · 运行用户变化": " · Run user changed", "运行用户": "Run user", "steamos 使用 Steam 会话用户运行;root 使用系统管理员运行。安装前请确认插件来源和运行用户。": "steamos runs as the Steam session user; root runs as the system administrator. Check the plugin source and run user before installing.", - "安装前会展示运行用户。商店资料由插件源提供,以包内的信息为准。": "The run user is shown before installation. Store information comes from the plugin source; the package contents are authoritative.", + "安装前会展示运行用户。插件库资料由插件源提供,以包内的信息为准。": "The run user is shown before installation. Plugin library information comes from the plugin source; the package contents are authoritative.", "运行用户:{0}": "Run user: {0}", "目标版本": "Target version", "系统管理员": "System administrator", diff --git a/ui/locales/zh-CN.json b/ui/locales/zh-CN.json index b02b1c0..56c3339 100644 --- a/ui/locales/zh-CN.json +++ b/ui/locales/zh-CN.json @@ -9,7 +9,7 @@ "使用 Framely 前": "使用 Framely 前", "用户协议": "用户协议", "Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。": "Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。", - "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件商店收录、下载校验或权限提示不代表对插件安全性的认可或担保。": "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件商店收录、下载校验或权限提示不代表对插件安全性的认可或担保。", + "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件库收录、下载校验或权限提示不代表对插件安全性的认可或担保。": "Framely 不保证第三方插件的安全性、可靠性或兼容性。插件库收录、下载校验或权限提示不代表对插件安全性的认可或担保。", "请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。": "请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。", "在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。": "在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。", "本协议不排除或限制法律规定不得免除或限制的责任及用户权利。": "本协议不排除或限制法律规定不得免除或限制的责任及用户权利。", @@ -61,7 +61,7 @@ "管理": "管理", "插件已停用、处于安全模式或没有快捷页面。": "插件已停用、处于安全模式或没有快捷页面。", "主导航": "主导航", - "插件商店": "插件商店", + "插件库": "插件库", "插件源": "插件源", "设置": "设置", "关于": "关于", @@ -190,7 +190,7 @@ "较旧版本": "较旧版本", "不同版本": "不同版本", "全部": "全部", - "返回商店": "返回商店", + "返回插件库": "返回插件库", "更新": "更新", "检查此版本": "检查此版本", "当前安装来自其他源,确认安装此版本后会更换来源。": "当前安装来自其他源,确认安装此版本后会更换来源。", @@ -209,7 +209,7 @@ "管理插件源": "管理插件源", "刷新中…": "刷新中…", "刷新": "刷新", - "搜索商店插件": "搜索商店插件", + "搜索插件库": "搜索插件库", "搜索名称、作者或标签": "搜索名称、作者或标签", "标签": "标签", "所有来源": "所有来源", @@ -347,7 +347,7 @@ " · 运行用户变化": " · 运行用户变化", "运行用户": "运行用户", "steamos 使用 Steam 会话用户运行;root 使用系统管理员运行。安装前请确认插件来源和运行用户。": "steamos 使用 Steam 会话用户运行;root 使用系统管理员运行。安装前请确认插件来源和运行用户。", - "安装前会展示运行用户。商店资料由插件源提供,以包内的信息为准。": "安装前会展示运行用户。商店资料由插件源提供,以包内的信息为准。", + "安装前会展示运行用户。插件库资料由插件源提供,以包内的信息为准。": "安装前会展示运行用户。插件库资料由插件源提供,以包内的信息为准。", "运行用户:{0}": "运行用户:{0}", "目标版本": "目标版本", "系统管理员": "系统管理员", diff --git a/ui/src/agreement.tsx b/ui/src/agreement.tsx index 8254caf..9afc3fc 100644 --- a/ui/src/agreement.tsx +++ b/ui/src/agreement.tsx @@ -16,5 +16,5 @@ export function AgreementGate({agreement,refresh,onClose,onRevoked,initialDeclin const header=
void selectLanguage(value)}/>
; if(declined)return
{header}

{t("请先阅读并同意使用协议")}

; - return
{header}

{t("使用 Framely 前")}

{t("用户协议")}

{t("Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。")}

{t("Framely 不保证第三方插件的安全性、可靠性或兼容性。插件商店收录、下载校验或权限提示不代表对插件安全性的认可或担保。")}

{t("请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。")}

{t("在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。")}{' '}{t("本协议不排除或限制法律规定不得免除或限制的责任及用户权利。")}

{t("不同意本协议时,请停止使用 Framely。协议发生变更时,需要重新确认。")}

{t("隐私声明")}

{t("Framely 本身不会主动收集、上传或出售用户个人信息,不内置用户行为跟踪或遥测上报。")}

{t("为提供管理功能,Framely 会在设备本地保存插件清单、配置、运行日志、协议同意记录及其他必要状态。访问密码仅保存加盐哈希值。")}

{t("获取插件目录、刷新订阅、下载插件和检查更新时,Framely 会按配置访问相应服务器;使用代理时,请求会经过相应代理服务。这些服务可能获知 IP 地址及请求信息,其数据处理由各自运营者负责。")}

{t("第三方插件可能自行收集、读取或上传用户信息。Framely 不保证插件不会收集信息,也不保证插件的数据处理行为符合你的隐私期望。")}{' '}{t("请查看插件的权限、说明及隐私政策,自行决定是否使用。")}

{error&&

{error}

}
{confirmRevoke?
{t("拒绝协议将停用全部插件")}

{t("插件窗口将关闭。重新同意后,需自行启用插件。")}

:agreement.accepted?<>{t("用户协议与隐私声明")}
:<>
}
; + return
{header}

{t("使用 Framely 前")}

{t("用户协议")}

{t("Framely 提供插件的集中安装、管理、运行和更新等功能。第三方插件由各自开发者提供,插件的功能、内容及运行行为由插件开发者负责。")}

{t("Framely 不保证第三方插件的安全性、可靠性或兼容性。插件库收录、下载校验或权限提示不代表对插件安全性的认可或担保。")}

{t("请自行甄别插件来源、安全性及所需权限,谨慎决定是否安装、授权或运行,并备份重要数据。插件可能读取或修改数据、访问网络、调整设备设置;授予较高权限可能影响系统和设备。")}

{t("在适用法律允许的范围内,因第三方插件的安装、授权、运行、更新或卸载造成的数据丢失、隐私泄露、设备故障或其他损失,Framely 及其开发者不承担责任。")}{' '}{t("本协议不排除或限制法律规定不得免除或限制的责任及用户权利。")}

{t("不同意本协议时,请停止使用 Framely。协议发生变更时,需要重新确认。")}

{t("隐私声明")}

{t("Framely 本身不会主动收集、上传或出售用户个人信息,不内置用户行为跟踪或遥测上报。")}

{t("为提供管理功能,Framely 会在设备本地保存插件清单、配置、运行日志、协议同意记录及其他必要状态。访问密码仅保存加盐哈希值。")}

{t("获取插件目录、刷新订阅、下载插件和检查更新时,Framely 会按配置访问相应服务器;使用代理时,请求会经过相应代理服务。这些服务可能获知 IP 地址及请求信息,其数据处理由各自运营者负责。")}

{t("第三方插件可能自行收集、读取或上传用户信息。Framely 不保证插件不会收集信息,也不保证插件的数据处理行为符合你的隐私期望。")}{' '}{t("请查看插件的权限、说明及隐私政策,自行决定是否使用。")}

{error&&

{error}

}
{confirmRevoke?
{t("拒绝协议将停用全部插件")}

{t("插件窗口将关闭。重新同意后,需自行启用插件。")}

:agreement.accepted?<>{t("用户协议与隐私声明")}
:<>
}
; } diff --git a/ui/src/icons.tsx b/ui/src/icons.tsx index a052a3c..945e874 100644 --- a/ui/src/icons.tsx +++ b/ui/src/icons.tsx @@ -1,6 +1,7 @@ import React from 'react'; -export type IconName='star'|'plugins'|'settings'|'arrow'|'close'|'more'|'download'|'search'|'shield'|'link'|'refresh'|'info'|'power'|'connection'|'edit'|'trash'; +export type IconName='star'|'plugins'|'settings'|'arrow'|'close'|'more'|'download'|'search'|'shield'|'link'|'refresh'|'info'|'power'|'connection'|'edit'|'trash'|'store'; const paths:Record={ + store:<>, power:<>, connection:<>, edit:<>, diff --git a/ui/src/main.tsx b/ui/src/main.tsx index dc33234..a833471 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -60,7 +60,7 @@ function PluginFrame({plugin,entry='quick'}:{plugin:string;entry?:string}){ return error?

{error}

:source?