Add Model Resolver version status feature

This commit is contained in:
Dariusz L
2026-08-01 20:56:02 +02:00
parent 201936b79a
commit e3affba329
4 changed files with 249 additions and 12 deletions
+90 -1
View File
@@ -1,12 +1,13 @@
"""
@author: Azornes
@title: ComfyUI Model Resolver
@version: 1.0.0
@version: 1.1.0
@description: Extension for resolving missing models and downloading from HuggingFace/CivitAI
"""
import asyncio
import os
import re
import sys
import threading
import time
@@ -47,6 +48,88 @@ from .core.path_utils import (
# Web directory for JavaScript interface
WEB_DIRECTORY = "./web"
PROJECT_VERSION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "pyproject.toml")
PROJECT_GITHUB_URL = "https://github.com/Azornes/Comfyui-Model-Resolver"
PROJECT_GITHUB_PYPROJECT_URL = (
"https://raw.githubusercontent.com/Azornes/Comfyui-Model-Resolver/main/pyproject.toml"
)
PROJECT_VERSION_CACHE_TTL_SECONDS = 6 * 60 * 60
_project_version_cache = {"checked_at": 0.0, "latest_version": None}
_project_version_cache_lock = threading.Lock()
def _extract_project_version(text: str) -> str:
"""Extract the package version from a pyproject.toml document."""
match = re.search(r"(?m)^\s*version\s*=\s*[\"']([^\"']+)[\"']\s*$", str(text or ""))
return match.group(1).strip() if match else ""
def _get_local_project_version() -> str:
try:
with open(PROJECT_VERSION_FILE, encoding="utf-8") as version_file:
return _extract_project_version(version_file.read()) or "unknown"
except (OSError, UnicodeError):
return "unknown"
def _version_sort_key(version: str) -> tuple[int, ...]:
return tuple(int(part) for part in re.findall(r"\d+", str(version or "")))
def _get_project_version_info() -> dict[str, Any]:
"""Return the installed version and the version currently published on GitHub."""
current_version = _get_local_project_version()
now = time.monotonic()
with _project_version_cache_lock:
cached_at = float(_project_version_cache.get("checked_at") or 0.0)
latest_version = _project_version_cache.get("latest_version")
cache_is_fresh = now - cached_at < PROJECT_VERSION_CACHE_TTL_SECONDS
if not cache_is_fresh:
latest_version = None
try:
from .core.network_utils import request_source_response
response = request_source_response(
PROJECT_GITHUB_PYPROJECT_URL,
timeout=5,
max_attempts=1,
log_name="GitHub version check",
)
try:
if response is not None and response.status_code == 200:
latest_version = _extract_project_version(response.text)
finally:
if response is not None:
response.close()
except Exception:
latest_version = None
with _project_version_cache_lock:
_project_version_cache.update(
{
"checked_at": now,
"latest_version": latest_version or None,
}
)
current_key = _version_sort_key(current_version)
latest_key = _version_sort_key(latest_version or "")
if not latest_version or not latest_key:
status = "unavailable"
elif current_key and latest_key > current_key:
status = "update_available"
else:
status = "current"
return {
"current_version": current_version,
"latest_version": latest_version,
"status": status,
"github_url": PROJECT_GITHUB_URL,
}
MODEL_RESOLVER_DEPENDENCY_NODE_TYPE = "ModelResolverDependency"
MODEL_RESOLVER_DEPENDENCY_NODE_DISPLAY_NAME = "Model Resolver Opener"
MODEL_RESOLVER_DEPENDENCY_NODE_CATEGORY = "Model Resolver/Workflow"
@@ -5018,6 +5101,12 @@ class ModelResolverExtension:
}
)
@routes.get("/model_resolver/version")
@json_api_endpoint("version")
async def get_project_version(request):
"""Return the installed version and the latest GitHub version."""
return web.json_response(await asyncio.to_thread(_get_project_version_info))
@routes.get("/model_resolver/subfolders/{category}")
@json_api_endpoint("subfolders")
async def get_subfolders(request):
+53 -1
View File
@@ -3321,7 +3321,7 @@
.mr-footer {
display: flex;
flex-direction: row;
justify-content: center;
justify-content: flex-start;
align-items: center;
gap: var(--mr-spacing-10);
flex-wrap: wrap;
@@ -3349,6 +3349,58 @@
display: none;
}
.mr-footer:not(.mr-footer-is-missing) .mr-footer-star-link {
margin-right: auto;
}
.mr-footer-version {
display: inline-flex;
align-items: center;
gap: var(--mr-spacing-7);
flex: 0 0 auto;
order: -1;
margin-left: 0;
color: var(--mr-color-gray-400);
font-size: var(--mr-font-sm);
line-height: 1.2;
white-space: nowrap;
}
.mr-footer-version-current {
color: var(--mr-color-gray-300);
}
.mr-footer-version-latest {
color: var(--mr-color-gray-500);
text-decoration: none;
}
.mr-footer-version-latest:hover,
.mr-footer-version-latest:focus-visible {
color: inherit;
text-decoration: underline;
}
.mr-footer-version-latest:focus-visible {
outline: none;
}
.mr-footer-version-latest.mr-footer-version-update {
font-weight: 400;
}
.mr-footer-version.mr-footer-version-up-to-date .mr-footer-version-current {
color: #72d38a;
}
.mr-footer-version.mr-footer-version-outdated .mr-footer-version-current {
color: #f56f6f;
}
.mr-footer-version.mr-footer-version-hidden {
display: none;
}
.mr-footer-star-link {
display: flex;
align-items: center;
+105 -10
View File
@@ -3,6 +3,8 @@ import { api } from "../../../../../scripts/api.js";
import { $el } from "../../../../../scripts/ui.js";
import { getSvgIcon } from "../../utils/icon_utils.js";
import { startSplitterDrag } from "../utils/splitter_drag.js";
const FOOTER_VERSION_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
export const queueMethods = {
createContent() {
// Wrap the body in a two-column layout: left = items, right = queued panel
@@ -2830,8 +2832,7 @@ export const queueMethods = {
this.footerElement.classList.toggle('mr-footer-is-missing', this.activeTab === 'missing');
this.footerStarLink.classList.remove('mr-footer-star-hidden');
if (this.activeTab !== 'missing') return;
this.footerVersionElement?.classList.remove('mr-footer-version-hidden');
const footerStyle = getComputedStyle(this.footerElement);
const footerWidth = (this.footerElement.clientWidth || 0)
@@ -2839,11 +2840,13 @@ export const queueMethods = {
- (parseFloat(footerStyle.paddingRight) || 0);
if (footerWidth <= 0) return;
const actionItems = Array.from(this.footerActions.children || []).filter((element) => {
const style = getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
});
if (!actionItems.length) return;
const actionsVisible = getComputedStyle(this.footerActions).display !== 'none';
const actionItems = actionsVisible
? Array.from(this.footerActions.children || []).filter((element) => {
const style = getComputedStyle(element);
return style.display !== 'none' && style.visibility !== 'hidden';
})
: [];
const gap = parseFloat(footerStyle.columnGap || footerStyle.gap || '0') || 0;
const actionsStyle = getComputedStyle(this.footerActions);
@@ -2851,8 +2854,24 @@ export const queueMethods = {
const actionsWidth = actionItems.reduce((total, element) => {
return total + element.getBoundingClientRect().width;
}, 0) + (Math.max(0, actionItems.length - 1) * actionGap);
const requiredWidth = this.footerStarLink.scrollWidth + actionsWidth + gap;
this.footerStarLink.classList.toggle('mr-footer-star-hidden', requiredWidth > footerWidth);
const starWidth = this.footerStarLink.scrollWidth;
const versionWidth = this.footerVersionElement?.scrollWidth || 0;
const getRequiredWidth = ({ includeStar = true, includeVersion = true } = {}) => {
const itemWidths = [
includeStar ? starWidth : 0,
actionsWidth,
includeVersion ? versionWidth : 0
];
const visibleWidths = itemWidths.filter((width) => width > 0);
return visibleWidths.reduce((total, width) => total + width, 0)
+ (Math.max(0, visibleWidths.length - 1) * gap);
};
const shouldHideStar = getRequiredWidth() > footerWidth;
const shouldHideVersion = versionWidth > 0
&& getRequiredWidth({ includeStar: !shouldHideStar }) > footerWidth;
this.footerVersionElement?.classList.toggle('mr-footer-version-hidden', shouldHideVersion);
this.footerStarLink.classList.toggle('mr-footer-star-hidden', shouldHideStar);
},
bindFooterStarLayoutObserver() {
@@ -2860,6 +2879,65 @@ export const queueMethods = {
this._footerStarResizeObserver = new ResizeObserver(() => this.updateFooterStarLayout());
this._footerStarResizeObserver.observe(this.footerElement);
this._footerStarResizeObserver.observe(this.footerActions);
if (this.footerVersionElement) this._footerStarResizeObserver.observe(this.footerVersionElement);
},
async loadFooterVersion() {
if (!this.footerVersionCurrent || !this.footerVersionLatest) return;
const now = Date.now();
if (
this._footerVersionLastCheckedAt
&& now - this._footerVersionLastCheckedAt < FOOTER_VERSION_REFRESH_INTERVAL_MS
) {
return;
}
this._footerVersionLastCheckedAt = now;
try {
const data = await this.fetchJson(
'/model_resolver/version',
{ silent: true },
'Get Model Resolver version'
);
const formatVersion = (value) => {
const version = String(value || '').trim().replace(/^v/i, '');
return version ? `v${version}` : '';
};
const currentVersion = formatVersion(data?.current_version);
const latestVersion = formatVersion(data?.latest_version);
this.footerVersionCurrent.textContent = currentVersion || 'v—';
this.footerVersionLatest.textContent = latestVersion
? `(latest: ${latestVersion})`
: '(latest: unavailable)';
const isUpToDate = data?.status === 'current';
const isOutdated = data?.status === 'update_available';
this.footerVersionElement?.classList.toggle('mr-footer-version-up-to-date', isUpToDate);
this.footerVersionElement?.classList.toggle('mr-footer-version-outdated', isOutdated);
this.footerVersionLatest.classList.toggle(
'mr-footer-version-update',
isOutdated
);
this.footerVersionLatest.title = data?.status === 'update_available'
? 'A newer Model Resolver version is available on GitHub.'
: 'Model Resolver version status';
this.footerVersionElement?.setAttribute(
'aria-label',
latestVersion
? `Model Resolver ${currentVersion}; latest GitHub version ${latestVersion}`
: `Model Resolver ${currentVersion}; latest GitHub version unavailable`
);
this.updateFooterStarLayout();
} catch (_) {
this.footerVersionLatest.textContent = '(latest: unavailable)';
this.footerVersionElement?.classList.remove(
'mr-footer-version-up-to-date',
'mr-footer-version-outdated'
);
this.footerVersionLatest.classList.remove('mr-footer-version-update');
this.updateFooterStarLayout();
}
},
createFooter() {
@@ -3007,7 +3085,23 @@ export const queueMethods = {
})
]);
const versionCurrent = $el("span.mr-footer-version-current", {
textContent: "v—"
});
const versionLatest = $el("a.mr-footer-version-latest", {
href: "https://github.com/Azornes/Comfyui-Model-Resolver",
target: "_blank",
rel: "noopener noreferrer",
textContent: "(latest: checking…)"
});
const versionElement = $el("span.mr-footer-version", {
title: "Model Resolver version"
}, [versionCurrent, versionLatest]);
this.footerStarLink = starLink;
this.footerVersionElement = versionElement;
this.footerVersionCurrent = versionCurrent;
this.footerVersionLatest = versionLatest;
this.footerActions = $el("div.mr-footer-actions", {}, [
selectMenu,
searchMenu,
@@ -3016,7 +3110,8 @@ export const queueMethods = {
]);
this.footerElement = $el("div.mr-footer", {}, [
starLink,
this.footerActions
this.footerActions,
versionElement
]);
this.bindFooterStarLayoutObserver();
queueMicrotask(() => this.updateFooterStarLayout());
@@ -18,6 +18,7 @@ export const lifecycleGraphMethods = {
async showContent(workflow = null, { restoreFullscreen = true } = {}) {
this.backdrop.style.display = "none";
this.element.style.display = "flex";
void this.loadFooterVersion();
if (!this.docked) {
this.scheduleModalViewportClamp();
}