From 43c13c47dfa5a2799774cdc01e3e72bcff5f679d Mon Sep 17 00:00:00 2001 From: eondrcode <2106626532@qq.com> Date: Thu, 9 Jul 2026 21:37:30 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E4=BC=98=E5=8C=96UI=E5=8F=8A=E5=85=BC?= =?UTF-8?q?=E5=AE=B9=E6=80=A7=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/data/types.ts | 5 + src/github-install.ts | 104 +++++++++-- src/lang/locale/en.ts | 11 ++ src/lang/locale/zh_cn.ts | 11 ++ src/main.ts | 34 ++-- src/modal/manager-modal.ts | 107 ++++++++++-- src/modal/update-check-modal.ts | 23 ++- src/modal/update-modal.ts | 30 +++- src/settings/data.ts | 4 + src/source-release.ts | 31 +++- styles.css | 294 +++++++++++++++++++++++++++++++- 11 files changed, 600 insertions(+), 54 deletions(-) diff --git a/src/data/types.ts b/src/data/types.ts index 16bf124..9e32962 100644 --- a/src/data/types.ts +++ b/src/data/types.ts @@ -47,6 +47,8 @@ export interface PluginLayoutItem { title?: string; } +export type ReleaseCompatibilityMode = "compatible" | "all"; + export interface BetaSource { id: string; repo: string; @@ -55,6 +57,7 @@ export interface BetaSource { frozenVersion?: string; includePrerelease?: boolean; updateCheckMode?: "release" | "version"; + compatibilityMode?: ReleaseCompatibilityMode; updateDelayDays?: number; autoUpdate: boolean; enabled: boolean; @@ -63,8 +66,10 @@ export interface BetaSource { latestPublishedAt?: string; installedReleaseTag?: string; installedReleasePublishedAt?: string; + installedReleaseIsGithubLatest?: boolean; latestReleaseTag?: string; latestReleasePublishedAt?: string; + latestReleaseIsGithubLatest?: boolean; installedAt?: number; lastChecked?: number; error?: string; diff --git a/src/github-install.ts b/src/github-install.ts index 0d97ba5..bc7432a 100644 --- a/src/github-install.ts +++ b/src/github-install.ts @@ -62,6 +62,14 @@ export interface ReleaseVersion { body?: string; publishedAt?: string; url?: string; + minAppVersion?: string; + manifestVersion?: string; + isGithubLatest?: boolean; + compatibilityChecked?: boolean; +} + +export interface FetchReleaseVersionOptions { + includeManifest?: boolean; } const API_BASE = "https://api.github.com"; @@ -164,6 +172,13 @@ const cloneReleaseVersions = (versions: ReleaseVersion[]): ReleaseVersion[] => const buildReleaseCacheKey = (repo: string, token?: string, proxy?: string): string => `${repo}\n${token ? "authenticated" : "anonymous"}\n${proxy || "direct"}`; +const buildReleaseCacheKeyWithOptions = ( + repo: string, + token: string | undefined, + proxy: string | undefined, + options?: FetchReleaseVersionOptions +): string => `${buildReleaseCacheKey(repo, token, proxy)}\nmanifest:${options?.includeManifest ? "1" : "0"}`; + /** * 生成 raw 文件候选地址。 * @@ -283,16 +298,77 @@ const upsertInstalledPluginRecord = ( manager.settings.Plugins.push(record); }; +const releaseToVersion = (release: ReleaseResponse, latestTag?: string): ReleaseVersion => ({ + version: release.tag_name || "", + prerelease: Boolean(release.prerelease), + name: release.name || undefined, + body: release.body || undefined, + publishedAt: release.published_at || undefined, + url: release.html_url || undefined, + isGithubLatest: Boolean(latestTag && release.tag_name === latestTag), +}); + +const fetchReleaseManifestText = async ( + manager: Manager, + repo: string, + release: ReleaseResponse, + token?: string +): Promise => { + const manifestUrl = pickAsset(release, "manifest.json"); + if (manifestUrl) { + try { + return await fetchText(manager, manifestUrl, token); + } catch { + // Fall back to the source tag below. + } + } + + const tag = release.tag_name || ""; + if (!tag) return null; + try { + return await fetchRawFromTag(manager, repo, tag, "manifest.json", token); + } catch { + return null; + } +}; + +const enrichReleaseVersionWithManifest = async ( + manager: Manager, + repo: string, + release: ReleaseResponse, + version: ReleaseVersion, + token?: string +): Promise => { + const manifestText = await fetchReleaseManifestText(manager, repo, release, token); + if (!manifestText) return { ...version, compatibilityChecked: true }; + + try { + const manifest = JSON.parse(manifestText) as { version?: string; minAppVersion?: string }; + return { + ...version, + manifestVersion: typeof manifest.version === "string" ? manifest.version : undefined, + minAppVersion: typeof manifest.minAppVersion === "string" ? manifest.minAppVersion : undefined, + compatibilityChecked: true, + }; + } catch { + return { ...version, compatibilityChecked: true }; + } +}; + /** * 拉取指定仓库的 release 版本列表。 * * 返回值用于下拉选择和更新检查。短 TTL 缓存可以避免用户打开弹窗、点击刷新、 * 批量检查更新时反复打同一个 GitHub API。 */ -export const fetchReleaseVersions = async (manager: Manager, repoInput: string): Promise => { +export const fetchReleaseVersions = async ( + manager: Manager, + repoInput: string, + options: FetchReleaseVersionOptions = {} +): Promise => { const repo = sanitizeRepo(repoInput); const token = await getToken(manager); - const cacheKey = buildReleaseCacheKey(repo, token, manager.settings.GITHUB_PROXY); + const cacheKey = buildReleaseCacheKeyWithOptions(repo, token, manager.settings.GITHUB_PROXY, options); const cached = releaseVersionCache.get(cacheKey); const now = Date.now(); @@ -300,6 +376,8 @@ export const fetchReleaseVersions = async (manager: Manager, repoInput: string): return cloneReleaseVersions(cached.versions); } + const latestRelease = await getRelease(manager, repo, undefined, token).catch(() => null); + const latestTag = latestRelease?.tag_name || undefined; const releases: ReleaseResponse[] = []; for (let page = 1; ; page++) { const url = `${API_BASE}/repos/${repo}/releases?per_page=${RELEASES_PER_PAGE}&page=${page}`; @@ -309,17 +387,19 @@ export const fetchReleaseVersions = async (manager: Manager, repoInput: string): if (pageReleases.length < RELEASES_PER_PAGE) break; } - const versions = releases + let releaseEntries = releases .filter((release) => !release.draft) - .map((release) => ({ - version: release.tag_name || "", - prerelease: Boolean(release.prerelease), - name: release.name || undefined, - body: release.body || undefined, - publishedAt: release.published_at || undefined, - url: release.html_url || undefined, - })) - .filter((release) => release.version); + .map((release) => ({ release, version: releaseToVersion(release, latestTag) })) + .filter((entry) => entry.version.version); + + if (options.includeManifest) { + releaseEntries = await Promise.all(releaseEntries.map(async (entry) => ({ + release: entry.release, + version: await enrichReleaseVersionWithManifest(manager, repo, entry.release, entry.version, token), + }))); + } + + const versions = releaseEntries.map((entry) => entry.version); releaseVersionCache.set(cacheKey, { expiresAt: now + RELEASE_VERSION_CACHE_TTL_MS, diff --git a/src/lang/locale/en.ts b/src/lang/locale/en.ts index 6195855..87bc13d 100644 --- a/src/lang/locale/en.ts +++ b/src/lang/locale/en.ts @@ -105,6 +105,13 @@ export default { 管理器_选择版本_当前版本: 'Installed: {version}', 管理器_选择版本_版本数量: '{count} versions', 管理器_选择版本_选择版本: 'Select version {version}', + 兼容性_策略: 'Compatibility', + 兼容性_仅兼容: 'Compatible only', + 兼容性_显示全部: 'Show all', + 兼容性_GitHubLatest: 'GitHub Latest', + 兼容性_需要版本: 'Requires Obsidian {version}', + 兼容性_兼容: 'Compatible', + 兼容性_不兼容: 'Incompatible', 筛选_全部_描述: 'All', 筛选_仅启用_描述: 'Enabled only', @@ -132,6 +139,10 @@ export default { 更新检测配置_说明: 'Choose how this overview check determines updates and delay days.', 更新检测配置_方式_标题: 'Detection method', 更新检测配置_方式_描述: 'Release order works for repositories with non-monotonic tags; version works for semantic versions.', + 更新检测配置_兼容性_标题: 'Compatibility strategy', + 更新检测配置_兼容性_描述: 'Uses manifest.json minAppVersion. Compatible first recommends versions that can run on this Obsidian version; show all lets latest and compatibility branches both participate.', + 更新检测配置_兼容性_兼容优先: 'Recommend compatible only', + 更新检测配置_兼容性_显示全部: 'Show all versions', 更新检测配置_延迟_标题: 'Delay days', 更新检测配置_延迟_描述: 'Only treat releases older than this many days as updates. 0 disables the delay.', 更新检测配置_开始按钮: 'Start check', diff --git a/src/lang/locale/zh_cn.ts b/src/lang/locale/zh_cn.ts index 6df2264..b4218ef 100644 --- a/src/lang/locale/zh_cn.ts +++ b/src/lang/locale/zh_cn.ts @@ -43,6 +43,10 @@ export default { 更新检测配置_说明: '选择本次总览检测的判定方式和延迟天数。', 更新检测配置_方式_标题: '检测方式', 更新检测配置_方式_描述: '发布顺序适合 tag 不严格递增的仓库;版本号适合规范语义化版本。', + 更新检测配置_兼容性_标题: '兼容性策略', + 更新检测配置_兼容性_描述: '按 manifest.json 的 minAppVersion 判断。兼容优先会推荐当前 Obsidian 可运行的版本;显示全部则允许最新版或兼容分支都参与判断。', + 更新检测配置_兼容性_兼容优先: '只推荐兼容版本', + 更新检测配置_兼容性_显示全部: '显示全部版本', 更新检测配置_延迟_标题: '延迟天数', 更新检测配置_延迟_描述: '只把发布满指定天数的版本视为可更新;0 表示不延迟。', 更新检测配置_开始按钮: '开始检查', @@ -183,6 +187,13 @@ export default { 管理器_选择版本_当前版本: '当前安装:{version}', 管理器_选择版本_版本数量: '{count} 个版本', 管理器_选择版本_选择版本: '选择版本 {version}', + 兼容性_策略: '兼容性策略', + 兼容性_仅兼容: '只推荐兼容', + 兼容性_显示全部: '显示全部', + 兼容性_GitHubLatest: 'GitHub 最新', + 兼容性_需要版本: '需要 Obsidian {version}', + 兼容性_兼容: '兼容', + 兼容性_不兼容: '不兼容', 通知_ID已复制: 'ID已复制', diff --git a/src/main.ts b/src/main.ts index ce99fd3..b598f58 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,5 +1,5 @@ import { normalizePath, ObsidianProtocolData, Plugin, PluginManifest, Workspace } from 'obsidian'; -import { DEFAULT_SETTINGS, ManagerSettings, PluginUpdateCheckMode } from './settings/data'; +import { DEFAULT_SETTINGS, ManagerSettings, PluginUpdateCheckMode, ReleaseCompatibilityMode } from './settings/data'; import { ManagerSettingTab } from './settings'; import { Translator } from './lang/inxdex'; import { ManagerModal } from './modal/manager-modal'; @@ -36,6 +36,7 @@ interface UpdateStatus { interface PluginUpdateCheckOptions { updateCheckMode?: PluginUpdateCheckMode; + compatibilityMode?: ReleaseCompatibilityMode; updateDelayDays?: number; } @@ -509,12 +510,14 @@ export default class Manager extends Plugin { public getPluginUpdateCheckOptions(): Required { return { updateCheckMode: this.normalizePluginUpdateCheckMode(this.settings.PLUGIN_UPDATE_CHECK_MODE), + compatibilityMode: this.normalizeReleaseCompatibilityMode(this.settings.PLUGIN_UPDATE_COMPATIBILITY_MODE), updateDelayDays: this.normalizePluginUpdateDelayDays(this.settings.PLUGIN_UPDATE_DELAY_DAYS), }; } public async setPluginUpdateCheckOptions(options: PluginUpdateCheckOptions): Promise { this.settings.PLUGIN_UPDATE_CHECK_MODE = this.normalizePluginUpdateCheckMode(options.updateCheckMode); + this.settings.PLUGIN_UPDATE_COMPATIBILITY_MODE = this.normalizeReleaseCompatibilityMode(options.compatibilityMode); this.settings.PLUGIN_UPDATE_DELAY_DAYS = this.normalizePluginUpdateDelayDays(options.updateDelayDays); await this.saveSettings(); } @@ -589,7 +592,7 @@ export default class Manager extends Plugin { private async checkBetaSource(source: BetaSource): Promise { try { - const versions = await fetchReleaseVersions(this, source.repo); + const versions = await fetchReleaseVersions(this, source.repo, { includeManifest: source.type === "plugin" }); let localVersion = source.localVersion || ""; if (source.type === "plugin") { const pluginId = this.getBetaSourcePluginId(source); @@ -644,7 +647,7 @@ export default class Manager extends Plugin { ? source.latestReleasePublishedAt || source.latestPublishedAt : undefined; if (!targetVersion) { - const versions = await fetchReleaseVersions(this, source.repo); + const versions = await fetchReleaseVersions(this, source.repo, { includeManifest: source.type === "plugin" }); let localVersion = source.localVersion || ""; if (source.type === "plugin") { const pluginId = this.getBetaSourcePluginId(source); @@ -1024,6 +1027,10 @@ export default class Manager extends Plugin { return value === "version" ? "version" : "release"; } + private normalizeReleaseCompatibilityMode(value?: string | null): ReleaseCompatibilityMode { + return value === "all" ? "all" : "compatible"; + } + private normalizePluginUpdateDelayDays(value: unknown): number { const days = Math.floor(Number(value)); return Number.isFinite(days) && days > 0 ? days : 0; @@ -1032,6 +1039,7 @@ export default class Manager extends Plugin { private resolvePluginUpdateCheckOptions(options?: PluginUpdateCheckOptions): Required { return { updateCheckMode: this.normalizePluginUpdateCheckMode(options?.updateCheckMode ?? this.settings.PLUGIN_UPDATE_CHECK_MODE), + compatibilityMode: this.normalizeReleaseCompatibilityMode(options?.compatibilityMode ?? this.settings.PLUGIN_UPDATE_COMPATIBILITY_MODE), updateDelayDays: this.normalizePluginUpdateDelayDays(options?.updateDelayDays ?? this.settings.PLUGIN_UPDATE_DELAY_DAYS), }; } @@ -1060,6 +1068,7 @@ export default class Manager extends Plugin { mode: "latest", includePrerelease: false, updateCheckMode: options.updateCheckMode, + compatibilityMode: options.compatibilityMode, updateDelayDays: options.updateDelayDays || undefined, autoUpdate: false, enabled: true, @@ -1103,7 +1112,7 @@ export default class Manager extends Plugin { st.repo = null; } if (st.repo) { - st.versions = await this.fetchGithubVersions(st.repo, updateOptions.updateCheckMode === "release"); + st.versions = await this.fetchGithubVersions(st.repo, updateOptions.updateCheckMode === "release", true); } this.applyPluginUpdateStatus(st, pm, st.repo || null, st.versions || [], official, updateOptions); if (this.settings.DEBUG) console.log("[BPM] update official match", pm.id, localVersion, "->", st.remoteVersion); @@ -1120,10 +1129,8 @@ export default class Manager extends Plugin { if (repo) { st.source = 'github'; st.repo = repo; - st.versions = await this.fetchGithubVersions(repo, updateOptions.updateCheckMode === "release"); - // 选择一个默认的远端版本:优先最新稳定,否则第一个 - const pick = st.versions?.find(v => !v.prerelease) ?? st.versions?.[0] ?? null; - const remoteVersion = pick?.version ?? await this.fetchGithubManifestVersion(repo); + st.versions = await this.fetchGithubVersions(repo, updateOptions.updateCheckMode === "release", true); + const remoteVersion = st.versions.length > 0 ? null : await this.fetchGithubManifestVersion(repo); this.applyPluginUpdateStatus(st, pm, repo, st.versions || [], remoteVersion, updateOptions); if (!st.remoteVersion) st.message = this.translator.t("更新_未获取到远端版本"); if (this.settings.DEBUG) console.log("[BPM] update github match", pm.id, repo, localVersion, "->", st.remoteVersion); @@ -1157,7 +1164,7 @@ export default class Manager extends Plugin { st.source = "official"; try { st.repo = await this.repoResolver.resolveRepo(pm.id); - if (st.repo) st.versions = await this.fetchGithubVersions(st.repo, updateOptions.updateCheckMode === "release"); + if (st.repo) st.versions = await this.fetchGithubVersions(st.repo, updateOptions.updateCheckMode === "release", true); } catch { if (updateOptions.updateCheckMode === "release") throw new Error(this.translator.t("更新_未获取到远端版本")); } @@ -1174,9 +1181,8 @@ export default class Manager extends Plugin { if (repo) { st.source = "github"; st.repo = repo; - st.versions = await this.fetchGithubVersions(repo, updateOptions.updateCheckMode === "release"); - const pick = st.versions?.find(v => !v.prerelease) ?? st.versions?.[0] ?? null; - const remoteVersion = pick?.version ?? await this.fetchGithubManifestVersion(repo); + st.versions = await this.fetchGithubVersions(repo, updateOptions.updateCheckMode === "release", true); + const remoteVersion = st.versions.length > 0 ? null : await this.fetchGithubManifestVersion(repo); this.applyPluginUpdateStatus(st, pm, repo, st.versions || [], remoteVersion, updateOptions); if (!st.remoteVersion) st.message = this.translator.t("更新_未获取到远端版本"); if (this.settings.DEBUG) console.log("[BPM] single update github", pm.id, repo, localVersion, "->", st.remoteVersion); @@ -1262,9 +1268,9 @@ export default class Manager extends Plugin { return null; } - private async fetchGithubVersions(repoInput: string, throwOnError = false): Promise { + private async fetchGithubVersions(repoInput: string, throwOnError = false, includeManifest = false): Promise { try { - return await fetchReleaseVersions(this, repoInput); + return await fetchReleaseVersions(this, repoInput, { includeManifest }); } catch (e) { console.error("[BPM] fetchGithubVersions error", repoInput, e); if (throwOnError) throw e; diff --git a/src/modal/manager-modal.ts b/src/modal/manager-modal.ts index 44b9f3e..c484148 100644 --- a/src/modal/manager-modal.ts +++ b/src/modal/manager-modal.ts @@ -49,7 +49,7 @@ import { ManagerTransferTheme, parseManagerTransferPackage, } from "../import-export"; -import { markSourceInstalledRelease, sourceHasUpdate as sourceHasConfiguredUpdate, syncSourceReleaseCheck } from "../source-release"; +import { markSourceInstalledRelease, pickSourceTargetRelease, releaseIsCompatible, sourceHasUpdate as sourceHasConfiguredUpdate, syncSourceReleaseCheck } from "../source-release"; import { createSharedVaultLinks, forgetSharedVault, @@ -945,14 +945,26 @@ export class ManagerModal extends Modal { return; } - const versions = await fetchReleaseVersions(this.manager, repo); + const versions = await fetchReleaseVersions(this.manager, repo, { includeManifest: true }); if (!status) status = {}; status.repo = repo; status.versions = versions; status.error = ""; status.message = ""; - const stableVersion = versions.find((release) => !release.prerelease)?.version; - status.remoteVersion = stableVersion || versions[0]?.version || status.remoteVersion || null; + const updateOptions = this.manager.getPluginUpdateCheckOptions(); + const target = pickSourceTargetRelease({ + id: pluginId, + repo, + type: "plugin", + mode: "latest", + includePrerelease: false, + updateCheckMode: updateOptions.updateCheckMode, + compatibilityMode: updateOptions.compatibilityMode, + updateDelayDays: updateOptions.updateDelayDays || undefined, + autoUpdate: false, + enabled: true, + }, versions); + status.remoteVersion = target?.tag || status.remoteVersion || null; this.manager.updateStatus[pluginId] = { ...(this.manager.updateStatus?.[pluginId] ?? {}), ...status, @@ -4450,6 +4462,7 @@ export class ManagerModal extends Modal { autoUpdate: source.autoUpdate ?? false, mode: source.mode || "latest", updateCheckMode: source.updateCheckMode || "release", + compatibilityMode: source.compatibilityMode || "compatible", updateDelayDays: this.normalizeSourceUpdateDelayDays(source.updateDelayDays), }; if (existing) { @@ -4461,7 +4474,7 @@ export class ManagerModal extends Modal { private async checkBetaSource(source: BetaSource): Promise { try { - const versions = await fetchReleaseVersions(this.manager, source.repo); + const versions = await fetchReleaseVersions(this.manager, source.repo, { includeManifest: source.type === "plugin" }); const localVersion = this.getSourceLocalVersion(source); const releaseCheck = syncSourceReleaseCheck(source, versions, localVersion); await this.refreshSourcePackageCreatedAt(source); @@ -4725,6 +4738,8 @@ export class ManagerModal extends Modal { const card = list.createDiv("manager-source-card"); card.setAttribute("data-source-repo", source.repo); + card.toggleClass("has-update", hasUpdate); + card.toggleClass("has-error", Boolean(source.error)); const cardMain = card.createDiv("manager-source-card__main"); const cardHeader = cardMain.createDiv("manager-source-card__header"); @@ -4751,6 +4766,10 @@ export class ManagerModal extends Modal { text: source.updateCheckMode === "version" ? t("来源_检测方式_版本号") : t("来源_检测方式_发布顺序"), cls: `manager-source-item__chip ${source.updateCheckMode === "version" ? "is-latest" : "is-frozen"}`, })); + chips.appendChild(createSpan({ + text: source.compatibilityMode === "all" ? t("兼容性_显示全部") : t("兼容性_仅兼容"), + cls: `manager-source-item__chip ${source.compatibilityMode === "all" ? "is-frozen" : "is-latest"}`, + })); if (source.autoUpdate) { chips.appendChild(createSpan({ text: t("来源_自动更新"), cls: "manager-source-item__chip is-auto" })); } @@ -4781,7 +4800,9 @@ export class ManagerModal extends Modal { } const controls = card.createDiv("manager-source-card__controls"); - const strategyGroup = controls.createDiv("manager-source-card__control-group manager-source-card__control-group--strategy"); + const strategyPanel = controls.createDiv("manager-source-card__control-panel manager-source-card__control-panel--strategy"); + const updatePanel = controls.createDiv("manager-source-card__control-panel manager-source-card__control-panel--update"); + const strategyGroup = strategyPanel.createDiv("manager-source-card__control-group manager-source-card__control-group--strategy"); strategyGroup.createSpan({ cls: "manager-source-card__control-label", text: t("来源_版本策略") }); const strategyWrap = strategyGroup.createDiv("manager-source-item__strategy"); const modeDropdown = new DropdownComponent(strategyWrap); @@ -4813,6 +4834,24 @@ export class ManagerModal extends Modal { updateCheckDropdown.selectEl.addClass("manager-source-item__check-mode"); updateCheckDropdown.selectEl.setAttribute("aria-label", t("来源_检测方式")); + if (source.type === "plugin") { + const compatibilityGroup = strategyPanel.createDiv("manager-source-card__control-group manager-source-card__control-group--compatibility"); + compatibilityGroup.createSpan({ cls: "manager-source-card__control-label", text: t("兼容性_策略") }); + const compatibilityDropdown = new DropdownComponent(compatibilityGroup); + compatibilityDropdown.addOptions({ + compatible: t("兼容性_仅兼容"), + all: t("兼容性_显示全部"), + }); + compatibilityDropdown.setValue(source.compatibilityMode || "compatible"); + compatibilityDropdown.onChange(async (value) => { + source.compatibilityMode = value === "all" ? "all" : "compatible"; + await this.checkBetaSource(source); + this.renderContent(); + }); + compatibilityDropdown.selectEl.addClass("manager-source-item__mode"); + compatibilityDropdown.selectEl.setAttribute("aria-label", t("兼容性_策略")); + } + const delayWrap = strategyWrap.createDiv("manager-source-item__delay"); delayWrap.createSpan({ cls: "manager-source-item__delay-label", text: t("来源_更新延迟_标签") }); const delayInput = new TextComponent(delayWrap); @@ -4846,7 +4885,8 @@ export class ManagerModal extends Modal { }); } - const autoGroup = controls.createDiv("manager-source-card__control-group manager-source-card__control-group--auto"); + const updateToggleGrid = updatePanel.createDiv("manager-source-card__toggle-grid"); + const autoGroup = updateToggleGrid.createDiv("manager-source-card__control-group manager-source-card__control-group--auto"); autoGroup.createSpan({ cls: "manager-source-card__control-label", text: t("来源_更新") }); const autoWrap = autoGroup.createDiv("manager-source-item__toggle"); autoWrap.createSpan({ cls: "manager-source-item__toggle-label", text: t("来源_自动") }); @@ -4859,7 +4899,7 @@ export class ManagerModal extends Modal { this.renderContent(); }); - const prereleaseGroup = controls.createDiv("manager-source-card__control-group manager-source-card__control-group--pre"); + const prereleaseGroup = updateToggleGrid.createDiv("manager-source-card__control-group manager-source-card__control-group--pre"); prereleaseGroup.createSpan({ cls: "manager-source-card__control-label", text: t("安装_发布类型_预发布") }); const prereleaseWrap = prereleaseGroup.createDiv("manager-source-item__toggle"); prereleaseWrap.createSpan({ cls: "manager-source-item__toggle-label", text: t("来源_最新") }); @@ -4873,7 +4913,7 @@ export class ManagerModal extends Modal { this.renderContent(); }); - const actionGroup = controls.createDiv("manager-source-card__actions"); + const actionGroup = updatePanel.createDiv("manager-source-card__actions"); const prepareActionButton = (btn: ButtonComponent, label: string) => { btn.setTooltip(label); btn.buttonEl.setAttribute("aria-label", label); @@ -5056,6 +5096,9 @@ export class ManagerModal extends Modal { }; const getReleaseOptionLabel = (release: ReleaseVersion) => [ release.version, + release.isGithubLatest ? t("兼容性_GitHubLatest") : "", + release.minAppVersion ? t("兼容性_需要版本", { version: release.minAppVersion }) : "", + release.minAppVersion ? t(releaseIsCompatible(release) ? "兼容性_兼容" : "兼容性_不兼容") : "", formatReleaseDate(release.publishedAt), release.prerelease ? t("安装_发布类型_预发布") : "", ].filter(Boolean).join(" · "); @@ -5063,6 +5106,21 @@ export class ManagerModal extends Modal { if (this.installVersions.length === 0) return null; const selected = this.installVersion.trim(); if (selected) return this.installVersions.find((item) => item.version === selected) ?? null; + if (this.installType === "plugin") { + const updateOptions = this.manager.getPluginUpdateCheckOptions(); + const target = pickSourceTargetRelease({ + id: this.getNormalizedInstallRepo(), + repo: this.getNormalizedInstallRepo(), + type: "plugin", + mode: "latest", + includePrerelease: false, + updateCheckMode: "release", + compatibilityMode: updateOptions.compatibilityMode, + autoUpdate: false, + enabled: true, + }, this.installVersions); + if (target) return this.installVersions.find((item) => item.version === target.tag) ?? null; + } return this.installVersions.find((item) => !item.prerelease) || this.installVersions[0]; }; const updateReleaseInfo = () => { @@ -5081,6 +5139,9 @@ export class ManagerModal extends Modal { releaseEls.title.setText(release.name || release.version); const metaParts = [ release.version, + release.isGithubLatest ? t("兼容性_GitHubLatest") : "", + release.minAppVersion ? t("兼容性_需要版本", { version: release.minAppVersion }) : "", + release.minAppVersion ? t(releaseIsCompatible(release) ? "兼容性_兼容" : "兼容性_不兼容") : "", release.prerelease ? t("安装_发布类型_预发布") : t("安装_发布类型_正式版"), formatReleaseDate(release.publishedAt), ].filter(Boolean); @@ -5096,7 +5157,7 @@ export class ManagerModal extends Modal { fetchButton?.setDisabled(true); fetchButton?.setButtonText(t("管理器_安装_版本_获取中")); try { - this.installVersions = await fetchReleaseVersions(this.manager, validRepo); + this.installVersions = await fetchReleaseVersions(this.manager, validRepo, { includeManifest: this.installType === "plugin" }); this.installVersion = ""; if (this.installVersions.length === 0) new Notice(t("管理器_安装_版本_空提示")); } catch (e) { @@ -5277,10 +5338,23 @@ export class ManagerModal extends Modal { let selectedRelease = getSelectedRelease(); if (!selectedRelease) { try { - const versions = await fetchReleaseVersions(this.manager, validRepo); + const updateOptions = this.manager.getPluginUpdateCheckOptions(); + const versions = await fetchReleaseVersions(this.manager, validRepo, { includeManifest: this.installType === "plugin" }); selectedRelease = this.installVersion ? versions.find((item) => item.version === this.installVersion) ?? null - : versions.find((item) => !item.prerelease) || versions[0] || null; + : this.installType === "plugin" + ? versions.find((item) => item.version === pickSourceTargetRelease({ + id: pluginId || validRepo, + repo: validRepo, + type: "plugin", + mode: "latest", + includePrerelease: false, + updateCheckMode: "release", + compatibilityMode: updateOptions.compatibilityMode, + autoUpdate: false, + enabled: true, + }, versions)?.tag) || versions.find((item) => !item.prerelease) || versions[0] || null + : versions.find((item) => !item.prerelease) || versions[0] || null; } catch { selectedRelease = null; } @@ -5298,6 +5372,7 @@ export class ManagerModal extends Modal { frozenVersion: this.installVersion || undefined, includePrerelease: Boolean(selectedRelease?.prerelease), updateCheckMode: "release", + compatibilityMode: this.manager.getPluginUpdateCheckOptions().compatibilityMode, autoUpdate: false, enabled: true, localVersion, @@ -6825,14 +6900,14 @@ export class ManagerModal extends Modal { private applyEditingStyle() { if (!this.modalContainer) return; if (this.editorMode) { - this.modalContainer.addClass("manager-container--editing"); + this.modalContainer.addClass("manager-container--editing"); } else { - this.modalContainer.removeClass("manager-container--editing"); + this.modalContainer.removeClass("manager-container--editing"); } if (this.bulkEditMode) { - this.modalContainer.addClass("manager-container--bulk-editing"); + this.modalContainer.addClass("manager-container--bulk-editing"); } else { - this.modalContainer.removeClass("manager-container--bulk-editing"); + this.modalContainer.removeClass("manager-container--bulk-editing"); } if (this.desktopActionWrapper) this.syncPageChrome(); } diff --git a/src/modal/update-check-modal.ts b/src/modal/update-check-modal.ts index 58651b9..21a51df 100644 --- a/src/modal/update-check-modal.ts +++ b/src/modal/update-check-modal.ts @@ -9,10 +9,11 @@ import { } from "obsidian"; import Manager from "main"; -import type { PluginUpdateCheckMode } from "../settings/data"; +import type { PluginUpdateCheckMode, ReleaseCompatibilityMode } from "../settings/data"; export interface PluginUpdateCheckConfig { updateCheckMode: PluginUpdateCheckMode; + compatibilityMode: ReleaseCompatibilityMode; updateDelayDays: number; } @@ -24,6 +25,9 @@ export const normalizePluginUpdateDelayDays = (value: unknown): number => { export const normalizePluginUpdateCheckMode = (value: unknown): PluginUpdateCheckMode => value === "version" ? "version" : "release"; +export const normalizeReleaseCompatibilityMode = (value: unknown): ReleaseCompatibilityMode => + value === "all" ? "all" : "compatible"; + export const openPluginUpdateCheckModal = ( app: App, manager: Manager, @@ -46,6 +50,7 @@ class PluginUpdateCheckModal extends Modal { super(app); this.config = { updateCheckMode: normalizePluginUpdateCheckMode(initial.updateCheckMode), + compatibilityMode: normalizeReleaseCompatibilityMode(initial.compatibilityMode), updateDelayDays: normalizePluginUpdateDelayDays(initial.updateDelayDays), }; } @@ -83,6 +88,21 @@ class PluginUpdateCheckModal extends Modal { }); }); + new Setting(body) + .setName(t("更新检测配置_兼容性_标题")) + .setDesc(t("更新检测配置_兼容性_描述")) + .addDropdown((dropdown: DropdownComponent) => { + dropdown.addOptions({ + compatible: t("更新检测配置_兼容性_兼容优先"), + all: t("更新检测配置_兼容性_显示全部"), + }); + dropdown.setValue(this.config.compatibilityMode); + dropdown.selectEl.setAttribute("aria-label", t("更新检测配置_兼容性_标题")); + dropdown.onChange((value) => { + this.config.compatibilityMode = normalizeReleaseCompatibilityMode(value); + }); + }); + new Setting(body) .setName(t("更新检测配置_延迟_标题")) .setDesc(t("更新检测配置_延迟_描述")) @@ -127,6 +147,7 @@ class PluginUpdateCheckModal extends Modal { if (this.resolved) return; this.resolved = true; this.config.updateDelayDays = normalizePluginUpdateDelayDays(this.config.updateDelayDays); + this.config.compatibilityMode = normalizeReleaseCompatibilityMode(this.config.compatibilityMode); this.resolve({ ...this.config }); this.close(); } diff --git a/src/modal/update-modal.ts b/src/modal/update-modal.ts index 1f7aa5f..38ec223 100644 --- a/src/modal/update-modal.ts +++ b/src/modal/update-modal.ts @@ -2,6 +2,7 @@ import { App, ExtraButtonComponent, Modal, Notice, setIcon, Setting } from "obsi import Manager from "main"; import { ReleaseVersion, fetchReleaseVersions } from "src/github-install"; import { getExtraButtonElement } from "src/obsidian-internals"; +import { pickSourceTargetRelease, releaseIsCompatible } from "src/source-release"; export class UpdateModal extends Modal { private manager: Manager; @@ -75,7 +76,7 @@ export class UpdateModal extends Modal { const loading = this.versionListEl.createDiv("manager-version-picker__empty"); loading.setText(t("管理器_选择版本_获取中")); try { - versionList = await fetchReleaseVersions(this.manager, this.repo); + versionList = await fetchReleaseVersions(this.manager, this.repo, { includeManifest: true }); } catch (e) { console.error("fetch versions in modal failed", e); new Notice(t("管理器_选择版本_获取失败提示"), 4000); @@ -87,11 +88,22 @@ export class UpdateModal extends Modal { const hasDefaultVersion = Boolean(this.defaultVersion && versionList.some((release) => release.version === this.defaultVersion)); const hasLocalVersion = Boolean(manifest?.version && versionList.some((release) => release.version === manifest.version)); + const updateOptions = this.manager.getPluginUpdateCheckOptions(); + const target = this.repo ? pickSourceTargetRelease({ + id: this.pluginId, + repo: this.repo, + type: "plugin", + mode: "latest", + includePrerelease: false, + updateCheckMode: updateOptions.updateCheckMode, + compatibilityMode: updateOptions.compatibilityMode, + updateDelayDays: updateOptions.updateDelayDays || undefined, + autoUpdate: false, + enabled: true, + }, versionList) : null; this.selectedVersion = hasDefaultVersion ? this.defaultVersion! - : hasLocalVersion - ? manifest.version - : (versionList[0]?.version ?? ""); + : target?.tag || (hasLocalVersion ? manifest.version : (versionList[0]?.version ?? "")); summaryStats.empty(); const releaseCount = summaryStats.createSpan({ cls: "manager-version-picker__summary-stat" }); setIcon(releaseCount.createSpan({ cls: "manager-version-picker__summary-stat-icon" }), "tags"); @@ -154,13 +166,23 @@ export class UpdateModal extends Modal { if (release.version === localVersion) { title.createSpan({ cls: "manager-version-picker__badge", text: t("来源_当前") }); } + if (release.isGithubLatest) { + title.createSpan({ cls: "manager-version-picker__badge is-latest", text: t("兼容性_GitHubLatest") }); + } title.createSpan({ cls: `manager-version-picker__badge ${release.prerelease ? "is-prerelease" : "is-stable"}`, text: t(release.prerelease ? "安装_发布类型_预发布" : "安装_发布类型_正式版"), }); + if (release.minAppVersion) { + title.createSpan({ + cls: `manager-version-picker__badge ${releaseIsCompatible(release) ? "is-compatible" : "is-incompatible"}`, + text: t(releaseIsCompatible(release) ? "兼容性_兼容" : "兼容性_不兼容"), + }); + } const meta = main.createDiv("manager-version-picker__item-meta"); if (release.name && release.name !== release.version) meta.createSpan({ text: release.name }); + if (release.minAppVersion) meta.createSpan({ text: t("兼容性_需要版本", { version: release.minAppVersion }) }); if (release.publishedAt) meta.createSpan({ text: this.formatDate(release.publishedAt) }); const check = item.createSpan({ cls: "manager-version-picker__item-check" }); diff --git a/src/settings/data.ts b/src/settings/data.ts index fb0f7fb..e83da16 100644 --- a/src/settings/data.ts +++ b/src/settings/data.ts @@ -25,6 +25,7 @@ export type FilterOperator = "contains" | "not-contains"; export type TagFilterOperator = FilterOperator; export type PluginOverviewLayout = "list" | "two-column"; export type PluginUpdateCheckMode = NonNullable; +export type ReleaseCompatibilityMode = NonNullable; export const DEFAULT_MAIN_PAGE_ACTION_PLACEMENT: Record = { checkUpdate: "menu", @@ -83,6 +84,8 @@ export interface ManagerSettings { SOURCE_AUTO_UPDATE: boolean; /** 插件总览更新检测的判定方式。release 按 GitHub 发布顺序,version 按版本号比较。 */ PLUGIN_UPDATE_CHECK_MODE: PluginUpdateCheckMode; + /** 插件总览更新检测是否按 manifest.minAppVersion 过滤不兼容版本。 */ + PLUGIN_UPDATE_COMPATIBILITY_MODE: ReleaseCompatibilityMode; /** 插件总览更新检测延迟天数;只把发布满指定天数的版本视为可更新。 */ PLUGIN_UPDATE_DELAY_DAYS: number; /** GitHub 请求中转站;留空时直连官方 GitHub。支持前缀或 {url}/{encodedUrl} 模板。 */ @@ -188,6 +191,7 @@ export const DEFAULT_SETTINGS: ManagerSettings = { SOURCE_STARTUP_CHECK_UPDATES: false, SOURCE_AUTO_UPDATE: true, PLUGIN_UPDATE_CHECK_MODE: "release", + PLUGIN_UPDATE_COMPATIBILITY_MODE: "compatible", PLUGIN_UPDATE_DELAY_DAYS: 0, GITHUB_PROXY: "", GITHUB_TOKEN: "", diff --git a/src/source-release.ts b/src/source-release.ts index 3a9b4c1..5c9c393 100644 --- a/src/source-release.ts +++ b/src/source-release.ts @@ -1,3 +1,4 @@ +import { requireApiVersion } from "obsidian"; import type { BetaSource } from "./data/types"; import type { ReleaseVersion } from "./github-install"; @@ -6,6 +7,7 @@ export interface SourceReleaseRef { publishedAt?: string; prerelease?: boolean; index?: number; + isGithubLatest?: boolean; } export interface SourceReleaseCheck { @@ -35,6 +37,7 @@ const releaseToRef = (release: ReleaseVersion, index: number): SourceReleaseRef publishedAt: release.publishedAt, prerelease: release.prerelease, index, + isGithubLatest: release.isGithubLatest, }); const DAY_MS = 24 * 60 * 60 * 1000; @@ -61,6 +64,16 @@ const releaseIsMatureEnough = (source: BetaSource, release: ReleaseVersion, now return publishedAt <= now - delayDays * DAY_MS; }; +export const releaseIsCompatible = (release: ReleaseVersion): boolean => { + const minAppVersion = release.minAppVersion?.trim(); + if (!minAppVersion) return true; + try { + return requireApiVersion(minAppVersion); + } catch { + return true; + } +}; + const sourceTargetIsMatureEnough = (source: BetaSource): boolean => { const delayDays = normalizeUpdateDelayDays(source); if (delayDays <= 0) return true; @@ -91,8 +104,16 @@ export const pickSourceTargetRelease = (source: BetaSource, versions: ReleaseVer }; } - const release = (source.includePrerelease ? versions : versions.filter((item) => !item.prerelease)) - .find((item) => releaseIsMatureEnough(source, item)); + const compatibleVersions = source.compatibilityMode === "all" + ? versions + : versions.filter((item) => releaseIsCompatible(item)); + const candidates = source.includePrerelease + ? compatibleVersions + : compatibleVersions.filter((item) => !item.prerelease); + const githubLatest = candidates.find((item) => item.isGithubLatest); + const release = githubLatest && releaseIsMatureEnough(source, githubLatest) + ? githubLatest + : candidates.find((item) => releaseIsMatureEnough(source, item)); if (!release) return null; return releaseToRef(release, versions.indexOf(release)); }; @@ -136,11 +157,13 @@ export const syncSourceReleaseCheck = ( source.localVersion = localVersion || source.localVersion || ""; source.latestReleaseTag = target?.tag || ""; source.latestReleasePublishedAt = target?.publishedAt; + source.latestReleaseIsGithubLatest = target?.isGithubLatest; source.latestVersion = target?.tag || ""; source.latestPublishedAt = target?.publishedAt; if (installed?.tag) source.installedReleaseTag = installed.tag; if (installed?.publishedAt) source.installedReleasePublishedAt = installed.publishedAt; + source.installedReleaseIsGithubLatest = installed?.isGithubLatest; return { target, installed }; }; @@ -154,6 +177,9 @@ export const markSourceInstalledRelease = ( const tag = cleanReleaseTag(releaseTag); if (tag) source.installedReleaseTag = tag; source.installedReleasePublishedAt = publishedAt || source.installedReleasePublishedAt; + if (releaseTagsMatch(tag, source.latestReleaseTag)) { + source.installedReleaseIsGithubLatest = source.latestReleaseIsGithubLatest; + } source.localVersion = localVersion || source.localVersion || tag; }; @@ -197,6 +223,7 @@ export const sourceHasReleaseUpdate = (source: BetaSource): boolean => { if (!targetTag || !installedTag) return false; if (releaseTagsMatch(targetTag, installedTag)) return false; if (!sourceTargetIsMatureEnough(source)) return false; + if (source.compatibilityMode !== "all" && source.installedReleaseIsGithubLatest && !source.latestReleaseIsGithubLatest) return false; if (source.mode === "frozen") return true; diff --git a/styles.css b/styles.css index 19f27cd..8785a29 100644 --- a/styles.css +++ b/styles.css @@ -9480,6 +9480,19 @@ body.theme-dark { color: var(--color-green); } +.manager-version-picker__badge.is-compatible, +.manager-version-picker__badge.is-latest { + border-color: color-mix(in srgb, var(--interactive-accent) 42%, var(--bpm-border)); + background: color-mix(in srgb, var(--interactive-accent) 10%, var(--bpm-card)); + color: var(--interactive-accent); +} + +.manager-version-picker__badge.is-incompatible { + border-color: color-mix(in srgb, var(--text-error) 48%, var(--bpm-border)); + background: color-mix(in srgb, var(--text-error) 10%, var(--bpm-card)); + color: var(--text-error); +} + .manager-version-picker__item-meta { display: flex; align-items: center; @@ -9905,11 +9918,11 @@ body.theme-dark { } } -@media (max-width: 520px) { - .manager-header { - height: var(--bpm-ui-header-height); - min-height: var(--bpm-ui-header-height); - padding-right: 12px; +@media (max-width: 520px) { + .manager-header { + height: var(--bpm-ui-header-height); + min-height: var(--bpm-ui-header-height); + padding-right: 12px; padding-left: 12px; } @@ -9921,6 +9934,277 @@ body.theme-dark { } } +/* Source subscription cards: separated identity, status, configuration, and actions. */ +.manager-source-card { + position: relative; + gap: 12px; + overflow: hidden; + border: 1px solid color-mix(in srgb, var(--bpm-border) 70%, transparent); + background: color-mix(in srgb, var(--bpm-card) 94%, var(--bpm-surface)); + box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); +} + +.manager-source-card::before { + content: ""; + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 2px; + background: transparent; + pointer-events: none; +} + +.manager-source-card:hover { + border-color: color-mix(in srgb, var(--text-muted) 28%, var(--bpm-border)); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.07); +} + +.manager-source-card.has-update { + border-color: color-mix(in srgb, var(--color-green) 24%, var(--bpm-border)); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--color-green) 5%, var(--bpm-card)) 0%, + var(--bpm-card) 22% + ); +} + +.manager-source-card.has-update::before { + background: color-mix(in srgb, var(--color-green) 72%, var(--bpm-border)); +} + +.manager-source-card.has-error { + border-color: color-mix(in srgb, var(--text-error) 28%, var(--bpm-border)); + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--text-error) 5%, var(--bpm-card)) 0%, + var(--bpm-card) 22% + ); +} + +.manager-source-card.has-error::before { + background: color-mix(in srgb, var(--text-error) 74%, var(--bpm-border)); +} + +.manager-source-card__header { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(180px, auto); + align-items: start; + gap: 12px; +} + +.manager-source-card__title-block, +.manager-source-card__repo-line { + min-width: 0; +} + +.manager-source-card__repo { + font-size: 14px; + line-height: 1.25; +} + +.manager-source-card__chips { + justify-content: flex-end; + align-content: flex-start; + max-width: 430px; +} + +.manager-source-card__meta-grid { + grid-template-columns: repeat(3, minmax(150px, 1fr)); +} + +.manager-source-card__meta { + grid-template-columns: 16px minmax(0, 1fr); + grid-template-areas: + "icon label" + "icon value"; + align-items: center; + gap: 3px 7px; + min-height: 42px; + padding: 7px 8px; + border-color: color-mix(in srgb, var(--bpm-border) 46%, transparent); + background: color-mix(in srgb, var(--bpm-muted) 34%, transparent); +} + +.manager-source-card__meta-icon { + grid-area: icon; + align-self: center; +} + +.manager-source-card__meta-label { + grid-area: label; + line-height: 1.1; +} + +.manager-source-card__meta-value { + grid-area: value; + line-height: 1.25; +} + +.manager-source-card__controls { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(230px, 0.65fr); + align-items: stretch; + gap: 10px; + padding-top: 10px; +} + +.manager-source-card__control-panel { + display: flex; + flex-direction: column; + gap: 8px; + min-width: 0; + padding: 9px; + border: 0; + border-radius: var(--bpm-radius-sm); + background: color-mix(in srgb, var(--bpm-muted) 34%, transparent); + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--bpm-border) 42%, transparent); +} + +.manager-source-card__control-panel--update { + justify-content: space-between; +} + +.manager-source-card__control-group, +.manager-source-card__control-group--strategy, +.manager-source-card__control-group--compatibility, +.manager-source-card__control-group--auto { + display: flex; + align-items: center; + flex: 0 1 auto; + flex-wrap: wrap; + gap: 7px; + width: 100%; + min-width: 0; +} + +.manager-source-card__control-group--strategy, +.manager-source-card__control-group--compatibility { + align-items: stretch; + flex-direction: column; +} + +.manager-source-card__control-group--compatibility .manager-source-item__mode { + flex: 1 1 auto; + width: 100%; +} + +.manager-source-card__control-label { + flex: 0 0 auto; + white-space: normal; + line-height: 1.2; +} + +.manager-source-card__toggle-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; + min-width: 0; +} + +.manager-source-card__control-group--auto, +.manager-source-card__control-group--pre { + align-items: stretch; + flex-direction: column; +} + +.manager-source-item__strategy { + align-items: center; + flex: 1 1 auto; + gap: 7px; + width: 100%; +} + +.manager-source-item__mode, +.manager-source-item__version { + flex: 1 1 124px; + width: auto; + min-width: 118px; +} + +.manager-source-item__check-mode { + min-width: 132px; +} + +.manager-source-item__delay { + flex: 1 1 132px; + justify-content: space-between; + min-width: 128px; +} + +.manager-source-item__delay-input { + flex: 0 0 58px; + width: 58px; +} + +.manager-source-item__toggle { + justify-content: space-between; + width: 100%; +} + +.manager-source-card__actions { + align-self: stretch; + display: grid; + grid-template-columns: repeat(5, minmax(34px, 1fr)); + gap: 6px; + justify-content: stretch; + padding-top: 8px; + border-top: 1px solid color-mix(in srgb, var(--bpm-border) 44%, transparent); +} + +.manager-source-card__actions button { + width: 100%; + min-width: 0; +} + +.manager-source-card__actions button:disabled { + opacity: 0.45; +} + +@media (max-width: 760px) { + .manager-source-card__header, + .manager-source-card__controls { + display: grid; + grid-template-columns: 1fr; + } + + .manager-source-card__chips { + justify-content: flex-start; + max-width: none; + } + + .manager-source-card__meta-grid { + grid-template-columns: 1fr; + } + + .manager-source-card__toggle-grid { + grid-template-columns: 1fr; + } + + .manager-source-card__actions { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(44px, 1fr)); + justify-content: stretch; + } + + .manager-source-card__actions button { + width: 100%; + } + + .manager-source-item__mode, + .manager-source-item__version, + .manager-source-item__delay { + flex: 1 1 148px; + width: auto; + min-width: 0; + } + + .manager-source-item__delay-input { + flex: 0 0 58px; + width: 58px; + } +} + /* Final uninstall modal overrides after legacy delete rules. */ .manager-delete__title { display: flex;