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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ Lithe 的插件管理改成和 IntelliJ IDEA 一样的“插件市场 / 已安

**展示的版本。** Core 会跳过当前平台或插件 API 不满足的新版本,选择能安装的旧版本。详情里的版本、发布日期和支持平台都描述“点安装会装的那个版本”;最新版本不同时,单独一行注明它在当前 Lithe 或这台 Mac 上无法使用。

**市场详情的分栏边界。** 市场和“已安装”使用不同的详情视图,必须分别验证。
市场信息行和来源选择器有最小宽度;直接把它们放在纵向 ScrollView(滚动视口)里,
会让 SwiftUI 按内容宽度重新居中,拖窄后文字和图标就跨到分隔线左侧。
市场视口现在先接受分栏给出的矩形,再以该宽度布局详情。正文最小宽度由现有
标签列、间距、来源选择器和提示卡的尺寸推导;较窄时可横向滚动查看完整内容,
较宽时描述仍按可见宽度换行。短详情在高度变化后也保持顶部对齐。
这里只测量视口的宽高,内容在测量闭包外构建;不在拖动中重读市场、重建模型或写偏好。
正确做法是约束视口并保留内容的滚动可达性;不要只裁掉越界正文、把详情最小宽度
传回外层撑宽设置窗口,或因为“已安装”测试通过就认为市场也通过。
这是原生视图布局修改,没有新增运行时文件、缓存或安装资源写入。

正确做法:新增官方插件时登记到 `official-plugins.json`,改插件内容时同时升 `plugin.json` 的版本。

不要这样做:手工改 `index.json` 里的下载地址或 SHA-256,或者把插件包重新挂到 Lithe 的版本 Release 上。
Expand Down Expand Up @@ -93,6 +104,7 @@ Lithe 的插件管理改成和 IntelliJ IDEA 一样的“插件市场 / 已安

- `cargo test --manifest-path rust/lithe-core/Cargo.toml --lib marketplace_plugins`:索引校验、平台与 API 判断、命名空间、源地址解析、安装审查的标准答案。
- `./scripts/verify-shared-contracts.sh`:市场 fixture 与错误码登记。
- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'PluginMarketplaceLayoutTests|PluginDetailLayoutTests|PluginSettingsLayoutTests'`:市场用内存索引驱动真实页面,覆盖中英文描述、深浅主题、窗口宽高变化、原生分隔条拖动、释放后刷新不移位,以及窄视口横向滚动到正文末端;单独保留“已安装”详情和路径控件回归。可用 `LITHE_MARKETPLACE_LAYOUT_CAPTURES` 导出市场原生视图截图;测试拥有并清理自己的偏好域和窗口。
- `node --test scripts/official-plugin-releases.test.mjs`:只为版本变化的插件生成标签,内容变化未升版本时失败;已发布未登记的版本会被重新登记。
- `node scripts/test-reuse-worktree-resources.mjs`:磁盘快照、安装 staging、版本和审批记录不出现在可复用列表,显式选择时在访问工作树或复制前拒绝。
- `./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- --filter 'PluginMarketplace|PluginDiskInstallation|MacPluginDiskPackageImporter|MacPluginPackageDownloader|PluginManagementPresentation|PluginManager|PluginPackageStore'`:校验和先于解压、下载和展开上限、磁盘快照隔离、源与签名要求、完整程序审查、安装 staging/扫描/回滚摘要复查、来源保留及同 ID 不自动接管。过滤器只是运行入口,必须核对报告确实包含 `MacPluginDiskPackageImporterTests`、`PluginDiskInstallationTests` 和磁盘 FeatureModel 场景,不能以零匹配当作通过。
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
40 changes: 40 additions & 0 deletions docs/plugin-marketplace-layout-visual-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# 插件市场详情的分栏边界

市场详情在窄面板下曾越过分隔线,遮住左侧列表。原因是详情的信息行、来源
选择器和提示卡有最小宽度,纵向滚动容器按内容宽度布局后,整个详情向左偏移。
“已安装”的详情是另一套视图,因此之前的已安装布局回归没有覆盖这个入口。

修复后,市场详情的视口接受分栏分配的宽度。正文在宽面板内换行,在极窄面板内
保留完整内容并允许横向滚动;图标、名称和说明均不会绘制到分隔线左侧。
短正文在窗口高度改变后保持顶部对齐。

## 同一页面的前后对比

图像来自 `PluginMarketplaceLayoutTests` 驱动的真实市场页,使用内存索引、两个
示例仓库和中文插件描述,没有访问网络或执行安装。窗口为 585 × 700pt,左栏
为 420pt,详情视口为 160pt。原布局取自 preview `1fba19412`,仅补充与当前相同的
辅助功能标识,用同一份测试和断言验证;原布局失败,当前布局通过。

| 主题 | 修复前 | 修复后 |
| --- | --- | --- |
| 浅色 | ![浅色市场详情越过分隔线](assets/plugin-marketplace-layout/before-light.png) | ![浅色市场详情留在右侧](assets/plugin-marketplace-layout/after-light.png) |
| 深色 | ![深色市场详情越过分隔线](assets/plugin-marketplace-layout/before-dark.png) | ![深色市场详情留在右侧](assets/plugin-marketplace-layout/after-dark.png) |

修复后截图处于横向滚动起点,右侧未显示的信息仍在同一滚动文档里。回归另验证
滚动可到达文档末端。图像是隔离测试宿主的原生视图绘制,不代表完整发行应用、
真实桌面合成或用户实机验收;界面固定文字使用测试宿主的语言资源。

## 复现和验证

```sh
LITHE_MARKETPLACE_LAYOUT_CAPTURES="$PWD/.artifacts/marketplace-layout-check" \
./.agents/skills/write-stable-tests/scripts/test-stability-macos.sh -- \
--filter 'PluginMarketplaceLayoutTests|PluginDetailLayoutTests|PluginSettingsLayoutTests'
```

市场回归覆盖英文/中文描述、深浅主题、窗口宽高变化、原生鼠标拖动、释放后刷新
不移位、极窄视口的横向滚动与顶部对齐。已安装详情和路径控件保留各自的回归。
测试使用独占的 UserDefaults 偏好域,并在结束后清理偏好域和窗口。

这些 PNG 是静态文档材料,不由产品读取,不进入 app bundle、安装目录或工作树
资源复用清单。产品修改没有新增运行时文件、下载入口或缓存。
43 changes: 29 additions & 14 deletions macos/Sources/Lithe/Views/App/PluginMarketplaceView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -189,17 +189,22 @@ struct PluginMarketplaceView: View {

@ViewBuilder private var detail: some View {
if let listing = selectedListing, let offer = feature.offer(for: listing) {
ScrollView {
PluginMarketplaceDetail(
feature: feature,
listing: listing,
offer: offer,
install: { install(offer) },
uninstall: { uninstall(listing.pluginID) }
)
let content = PluginMarketplaceDetail(
feature: feature, listing: listing, offer: offer,
install: { install(offer) }, uninstall: { uninstall(listing.pluginID) }
)
// Measure only this viewport. Its content must not widen or center
// the pane across the divider when a row cannot shrink further.
GeometryReader { geometry in
ScrollView([.horizontal, .vertical]) {
content.frame(width: max(geometry.size.width, PluginMarketplaceDetail.minimumWidth), alignment: .leading)
.frame(minHeight: geometry.size.height, alignment: .topLeading)
}
.litheScrollViewChrome()
}
.litheScrollViewChrome()
.frame(minWidth: 0, maxWidth: .infinity, minHeight: 0, maxHeight: .infinity)
.background(LitheTheme.settingsSurface)
.clipped()
} else {
VStack(spacing: 10) {
PluginMarketplaceIcon(icons: feature.icons, entry: nil, size: 36)
Expand Down Expand Up @@ -236,6 +241,13 @@ struct PluginMarketplaceView: View {
/// shows (version, license, release date, platforms, links), and the notice
/// about who maintains it.
private struct PluginMarketplaceDetail: View {
private static let inset: CGFloat = 24
private static let labelWidth: CGFloat = 96
private static let metadataSpacing: CGFloat = 12
private static let sourcePickerWidth: CGFloat = 180
/// Keep the existing source selector and banner usable at the split's
/// smallest width; the viewport scrolls instead of clipping those controls.
static let minimumWidth = 2 * inset + max(LitheTheme.Banner.minimumWidth, labelWidth + metadataSpacing + sourcePickerWidth)
@Environment(\.locale) private var locale
@ObservedObject var feature: PluginMarketplaceFeatureModel
let listing: PluginMarketplaceListing
Expand Down Expand Up @@ -273,13 +285,14 @@ private struct PluginMarketplaceDetail: View {
.font(LitheTheme.settingsFont)
.foregroundStyle(LitheTheme.primaryText)
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("plugin-marketplace-description")
metadata
links
maintainerNotice
Spacer(minLength: 0)
}
.padding(24)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(Self.inset)
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
}

private var header: some View {
Expand All @@ -292,6 +305,7 @@ private struct PluginMarketplaceDetail: View {
Text(verbatim: entry.name)
.font(LitheTheme.uiFont(size: 20, weight: .bold))
.fixedSize(horizontal: false, vertical: true)
.accessibilityIdentifier("plugin-marketplace-name")
HStack(spacing: 8) {
PluginTrustBadge(isOfficial: offer.source.isOfficial)
Text(verbatim: "\(entry.author) · \(PluginMarketplacePresentation.repositoryName(entry.repository))")
Expand All @@ -315,6 +329,7 @@ private struct PluginMarketplaceDetail: View {
case .installable:
Button(LocalizedStringKey("Install"), action: install)
.buttonStyle(LitheActionButtonStyle(isPrimary: true))
.accessibilityIdentifier("plugin-marketplace-install")
case .updateAvailable(_, let available):
Button(String(format: PluginMarketplacePresentation.localized("Update to %@", locale: locale), available), action: install)
.buttonStyle(LitheActionButtonStyle(isPrimary: true))
Expand Down Expand Up @@ -459,7 +474,7 @@ private struct PluginMarketplaceDetail: View {
set: { feature.selectedSourceIDs[listing.pluginID] = $0 }
),
options: listing.offers.map(\.source.id),
width: 180,
width: Self.sourcePickerWidth,
accessibilityLabel: "Install from repository",
title: { id in
listing.offers.first { $0.source.id == id }
Expand Down Expand Up @@ -497,10 +512,10 @@ private struct PluginMarketplaceDetail: View {
}

private func metadataRow<Content: View>(_ title: String, @ViewBuilder value: () -> Content) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 12) {
HStack(alignment: .firstTextBaseline, spacing: Self.metadataSpacing) {
Text(LocalizedStringKey(title))
.foregroundStyle(LitheTheme.secondaryText)
.frame(width: 96, alignment: .leading)
.frame(width: Self.labelWidth, alignment: .leading)
value()
.textSelection(.enabled)
}
Expand Down
Loading
Loading