Add Model Resolver version status feature
This commit is contained in:
+90
-1
@@ -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):
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user