Release v0.4.1 (#250)
* Release v0.4.0: update footer notice * Release v0.4.1
This commit is contained in:
@@ -40,6 +40,7 @@ jobs:
|
||||
run: |
|
||||
node tests/js/test_ai_pipeline_preview.mjs
|
||||
node tests/js/test_draft_guard.mjs
|
||||
node tests/js/test_update_check.mjs
|
||||
|
||||
security-gate:
|
||||
name: Security Gate
|
||||
|
||||
@@ -6,7 +6,14 @@ The format is inspired by Keep a Changelog and SemVer.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.4.1] - 2026-06-18
|
||||
|
||||
### Added
|
||||
- **Sidebar update notice.** The Kforge Labs sidebar now checks the installed
|
||||
pack version against GitHub's latest release and shows a discreet
|
||||
footer-slot "Update Available" notice only when a newer release exists. The
|
||||
notice links to the release notes and stays hidden when the check fails or
|
||||
the install is already current, so offline ComfyUI sessions remain quiet.
|
||||
- **Easy Image Batch — review hardening.** `keyframe_batch` is kept as a
|
||||
**deprecated alias** of `keyframes_insert` (pre-rename workflows keep loading;
|
||||
an explicit `keyframes_insert` wins if both are wired; removal deferred to a
|
||||
|
||||
@@ -7,6 +7,7 @@ before they land in the runtime sidebar.
|
||||
|---|---|
|
||||
| [`sidebar-icon-proposals.html`](sidebar-icon-proposals.html) | Side-by-side icon direction mockup for the Kforge Labs sidebar action rows |
|
||||
| [`sidebar-icon-proposals.png`](sidebar-icon-proposals.png) | Rendered preview of the current icon proposal state |
|
||||
| [`update-banner.html`](update-banner.html) | Footer banner layout proposal for a discreet "Update Available" release-notes link |
|
||||
| [`snapshot-dialogs-redesign.md`](snapshot-dialogs-redesign.md) | Scope sketch + Mermaid flow for consolidating Settings cog into Save / Load dialogs (+ autosave-newest fix) |
|
||||
| [`snapshot-dialogs.html`](snapshot-dialogs.html) | Side-by-side mockups: current vs. proposed Snapshot toolbar, Save dialog, Load dialog, and autosave YES/NO modal |
|
||||
|
||||
|
||||
@@ -0,0 +1,276 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Update Available Banner - Sidebar Mockup</title>
|
||||
<style>
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
--bg: #111316;
|
||||
--panel: #1b1d22;
|
||||
--panel-soft: #20242a;
|
||||
--line: rgba(255,255,255,0.1);
|
||||
--text: #e6e8ec;
|
||||
--muted: #9299a4;
|
||||
--blue: #6db4ff;
|
||||
--blue-soft: rgba(109,180,255,0.12);
|
||||
--green: #79c17d;
|
||||
--input: rgba(0,0,0,0.22);
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font: 13px/1.4 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
}
|
||||
|
||||
main {
|
||||
width: min(1040px, calc(100vw - 32px));
|
||||
margin: 28px auto 40px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 24px;
|
||||
font-weight: 750;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.intro {
|
||||
max-width: 720px;
|
||||
margin: 0 0 22px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.proposal {
|
||||
width: min(520px, 100%);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.025);
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.proposal h2 {
|
||||
margin: 0 0 5px;
|
||||
font-size: 15px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
|
||||
.proposal-copy {
|
||||
margin: 0 0 14px;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
width: 296px;
|
||||
height: 620px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(255,255,255,0.13);
|
||||
border-radius: 8px;
|
||||
background: var(--panel);
|
||||
box-shadow: 0 18px 44px rgba(0,0,0,0.32);
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.actions-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
padding: 2px 6px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
color: rgba(230,232,236,0.58);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.status {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
font-size: 11px;
|
||||
color: rgba(230,232,236,0.86);
|
||||
}
|
||||
|
||||
.dot {
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
margin-right: 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--green);
|
||||
box-shadow: 0 0 0 1px rgba(0,0,0,0.35);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.state {
|
||||
margin-left: 4px;
|
||||
color: rgba(230,232,236,0.55);
|
||||
font-size: 10px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.btn {
|
||||
width: 28px;
|
||||
height: 17px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 3px;
|
||||
background: var(--input);
|
||||
color: rgba(230,232,236,0.8);
|
||||
font: 700 11px/1 system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.btn.green {
|
||||
background: rgba(120,165,100,0.25);
|
||||
border-color: rgba(120,165,100,0.5);
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 8px 8px;
|
||||
border-top: 1px solid rgba(255,255,255,0.08);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.search {
|
||||
margin: 6px;
|
||||
padding: 5px 8px 5px 26px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 4px;
|
||||
background: var(--input);
|
||||
color: rgba(230,232,236,0.55);
|
||||
font-size: 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tree {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
padding: 0 4px 8px;
|
||||
}
|
||||
|
||||
.tree-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 25px;
|
||||
padding: 3px 6px;
|
||||
border-radius: 3px;
|
||||
color: rgba(230,232,236,0.88);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tree-row.dim { color: rgba(230,232,236,0.58); }
|
||||
.tree-row.indent { padding-left: 20px; }
|
||||
.chev { width: 10px; color: rgba(230,232,236,0.62); text-align: center; }
|
||||
.count {
|
||||
margin-left: auto;
|
||||
padding: 1px 7px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,0.08);
|
||||
color: rgba(230,232,236,0.72);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.inline-update {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px 12px;
|
||||
color: rgba(230,232,236,0.62);
|
||||
font-size: 11px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.inline-update a {
|
||||
color: #b9d8ff;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.notes {
|
||||
margin: 12px 2px 0;
|
||||
color: var(--muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.sidebar { width: min(296px, 100%); }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Sidebar Update Available Banner</h1>
|
||||
<p class="intro">Chosen quiet footer treatment for a future release-check feature. It points users to release notes without interrupting ComfyUI work or stealing space from the main sidebar lists. The notice uses the normal user-facing empty footer slot; the dev build line is omitted because normal users will not see it.</p>
|
||||
|
||||
<section class="proposal">
|
||||
<h2>Footer Slot Text</h2>
|
||||
<p class="proposal-copy">Discreet release notice at the bottom of the sidebar, using the footer slot instead of adding another row.</p>
|
||||
<div class="sidebar">
|
||||
<div class="sidebar-inner">
|
||||
<div class="actions-row">
|
||||
<div class="status"><span class="dot"></span><span class="name">Koolook_v03</span><span class="state">saved</span></div>
|
||||
<button class="btn">L</button><button class="btn">S</button><button class="btn">A/B</button>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="actions-row"><span class="label">Tools</span><button class="btn">I</button><button class="btn">H</button><button class="btn">P</button></div>
|
||||
<div class="divider"></div>
|
||||
<div class="search">Search nodes and workflows...</div>
|
||||
<div class="divider"></div>
|
||||
<div class="actions-row"><span class="label">Nodes</span><button class="btn">R</button><button class="btn">C</button><button class="btn green">+</button></div>
|
||||
<div class="divider"></div>
|
||||
<div class="actions-row"><span class="label">Workflows</span><button class="btn">S</button><button class="btn">M</button></div>
|
||||
<div class="divider"></div>
|
||||
<div class="tree">
|
||||
<div class="tree-row"><span class="chev">v</span>Nodes (favorites)<span class="count">11</span></div>
|
||||
<div class="tree-row indent dim"><span class="chev">></span>image<span class="count">1</span></div>
|
||||
<div class="tree-row indent dim"><span class="chev">></span>Koolook<span class="count">7</span></div>
|
||||
<div class="divider"></div>
|
||||
<div class="tree-row"><span class="chev">v</span>Workflows<span class="count">6</span></div>
|
||||
<div class="tree-row indent dim"><span class="chev">></span>Kforge Labs Workflows<span class="count">2</span></div>
|
||||
<div class="tree-row indent dim"><span class="chev">></span>_Modules<span class="count">3</span></div>
|
||||
</div>
|
||||
<div class="inline-update">
|
||||
<span>Update Available</span>
|
||||
<a href="https://github.com/malkuthro/ComfyUI-Koolook/releases" target="_blank" rel="noopener noreferrer">Release notes</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="notes">Lowest visual weight. This is the closest match if we want the notice to live where the hidden dev footer would otherwise be.</p>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -90,6 +90,26 @@ _DIRNAME_RE = re.compile(r"^[A-Za-z0-9 _.()\-]+$")
|
||||
# filter; can be removed in a future major if we choose to migrate.
|
||||
_HIDDEN_LIST_PREFIXES = ("_autosave_",)
|
||||
_WEB_DIR = Path(__file__).resolve().parent / "web"
|
||||
_REPO_ROOT = Path(__file__).resolve().parent
|
||||
_GITHUB_RELEASES_URL = "https://github.com/malkuthro/ComfyUI-Koolook/releases"
|
||||
_GITHUB_LATEST_RELEASE_API_URL = (
|
||||
"https://api.github.com/repos/malkuthro/ComfyUI-Koolook/releases/latest"
|
||||
)
|
||||
|
||||
|
||||
def _read_pack_version() -> str:
|
||||
"""Read the installed Koolook pack version from ``pyproject.toml``.
|
||||
|
||||
Kept local and dependency-free so the ComfyUI runtime does not need package
|
||||
metadata installed. Returns ``"unknown"`` if the file is unavailable or
|
||||
malformed; the frontend treats that as "do not show update notice".
|
||||
"""
|
||||
try:
|
||||
text = (_REPO_ROOT / "pyproject.toml").read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return "unknown"
|
||||
match = re.search(r'(?m)^\s*version\s*=\s*["\']([^"\']+)["\']', text)
|
||||
return match.group(1).strip() if match else "unknown"
|
||||
|
||||
|
||||
def _latest_autosave_mtime(autosave_dir: Path, named_mtime: float) -> float | None:
|
||||
@@ -564,6 +584,16 @@ def register_routes(routes, setup_registry_factory=None, setup_runner_factory=No
|
||||
async def setup_runner_simulator_js(_request):
|
||||
return _web_asset_response("setup_runner_simulator.js", "application/javascript")
|
||||
|
||||
@routes.get("/koolook/api/version")
|
||||
async def get_koolook_version(_request):
|
||||
return web.json_response(
|
||||
{
|
||||
"version": _read_pack_version(),
|
||||
"releasesUrl": _GITHUB_RELEASES_URL,
|
||||
"latestReleaseApiUrl": _GITHUB_LATEST_RELEASE_API_URL,
|
||||
}
|
||||
)
|
||||
|
||||
@routes.get("/koolook/api/setups")
|
||||
async def list_published_setups(_request):
|
||||
registry: PublishedSetupRegistry = setup_registry_factory()
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "koolook"
|
||||
description = "This repository provides custom nodes for ComfyUI tailored for VFX and AI image/video generation workflows"
|
||||
version = "0.3.7"
|
||||
version = "0.4.1"
|
||||
license = {file = "LICENSE"}
|
||||
# classifiers = [
|
||||
# # For OS-independent nodes (works on all operating systems)
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// Behavior tests for the sidebar update-available check.
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = path.resolve(import.meta.dirname, "../..");
|
||||
const updateCheck = await import(
|
||||
`file:///${path.join(repoRoot, "web", "sidebar", "update_check.js").replaceAll("\\", "/")}`
|
||||
);
|
||||
|
||||
function jsonResponse(body, ok = true) {
|
||||
return {
|
||||
ok,
|
||||
async json() {
|
||||
return body;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
assert.equal(updateCheck.compareSemver("0.4.1", "0.4.0"), 1);
|
||||
assert.equal(updateCheck.compareSemver("v0.4.0", "0.4.0"), 0);
|
||||
assert.equal(updateCheck.compareSemver("0.3.9", "0.4.0"), -1);
|
||||
assert.equal(updateCheck.compareSemver("0.4.0", "0.4.0-rc.1"), 1);
|
||||
assert.equal(updateCheck.compareSemver("not-a-version", "0.4.0"), 0);
|
||||
|
||||
{
|
||||
const calls = [];
|
||||
const update = await updateCheck.checkForUpdate(async (url) => {
|
||||
calls.push(String(url));
|
||||
if (calls.length === 1) {
|
||||
assert.ok(calls[0].startsWith("/koolook/api/version?_="));
|
||||
return jsonResponse({
|
||||
version: "0.4.0",
|
||||
latestReleaseApiUrl: "https://api.example.test/latest",
|
||||
});
|
||||
}
|
||||
assert.equal(url, "https://api.example.test/latest");
|
||||
return jsonResponse({
|
||||
tag_name: "v0.4.1",
|
||||
html_url: "https://github.com/malkuthro/ComfyUI-Koolook/releases/tag/v0.4.1",
|
||||
});
|
||||
});
|
||||
assert.deepEqual(update, {
|
||||
current: "0.4.0",
|
||||
latest: "0.4.1",
|
||||
url: "https://github.com/malkuthro/ComfyUI-Koolook/releases/tag/v0.4.1",
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
let calls = 0;
|
||||
const update = await updateCheck.checkForUpdate(async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return jsonResponse({ version: "0.4.0" });
|
||||
return jsonResponse({
|
||||
tag_name: "v0.4.1",
|
||||
html_url: "https://github.example.test/releases/v0.4.1",
|
||||
});
|
||||
});
|
||||
assert.deepEqual(update, {
|
||||
current: "0.4.0",
|
||||
latest: "0.4.1",
|
||||
url: "https://github.com/malkuthro/ComfyUI-Koolook/releases/tag/v0.4.1",
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const update = await updateCheck.checkForUpdate(async () => jsonResponse({ version: "0.4.0" }));
|
||||
assert.equal(update, null);
|
||||
}
|
||||
|
||||
{
|
||||
let calls = 0;
|
||||
const update = await updateCheck.checkForUpdate(async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return jsonResponse({ version: "0.4.0" });
|
||||
return jsonResponse({ tag_name: "v0.4.0", html_url: "https://example.test/current" });
|
||||
});
|
||||
assert.equal(update, null);
|
||||
}
|
||||
|
||||
{
|
||||
const update = await updateCheck.checkForUpdate(async () => {
|
||||
throw new Error("offline");
|
||||
});
|
||||
assert.equal(update, null);
|
||||
}
|
||||
|
||||
console.log("test_update_check.mjs: all scenarios passed");
|
||||
@@ -53,6 +53,21 @@ def test_setup_runner_simulator_routes_serve_html_and_js() -> None:
|
||||
asyncio.run(exercise())
|
||||
|
||||
|
||||
def test_version_route_exposes_installed_version_and_release_urls() -> None:
|
||||
async def exercise() -> None:
|
||||
app = _app_with_registry(PublishedSetupRegistry(StaticSetupStorage([])))
|
||||
|
||||
response = await _handle(app, "GET", "/koolook/api/version")
|
||||
|
||||
assert response.status == 200
|
||||
body = _json_body(response)
|
||||
assert body["version"] == "0.4.1"
|
||||
assert body["releasesUrl"] == "https://github.com/malkuthro/ComfyUI-Koolook/releases"
|
||||
assert body["latestReleaseApiUrl"].endswith("/malkuthro/ComfyUI-Koolook/releases/latest")
|
||||
|
||||
asyncio.run(exercise())
|
||||
|
||||
|
||||
def test_catalog_list_omits_invalid_setups() -> None:
|
||||
async def exercise() -> None:
|
||||
invalid = deepcopy(_valid_setup())
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
TREE_JS = REPO_ROOT / "web" / "sidebar" / "tree.js"
|
||||
|
||||
|
||||
def test_compare_panels_disable_update_footer() -> None:
|
||||
text = TREE_JS.read_text(encoding="utf-8")
|
||||
|
||||
assert "showUpdateFooter: false" in text
|
||||
assert "if (showUpdateFooter) {" in text
|
||||
@@ -320,7 +320,11 @@ export function ensureStyle() {
|
||||
.koolook-preview-widget-name { text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-widget-value { text-align: right; font-size: 11px; opacity: 0.85; font-family: monospace; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.koolook-preview-desc { margin: 10px 9px 0; padding: 7px 9px; background: rgba(0,0,0,0.18); border: 1px solid rgba(255,255,255,0.05); border-radius: 6px; font-style: italic; font-weight: 500; font-size: 11px; line-height: 1.45; word-break: break-word; opacity: 0.86; max-height: 120px; overflow: hidden; }
|
||||
.koolook-build-tag { flex-shrink: 0; padding: 6px 10px 14px; font-size: 10px; opacity: 0.5; text-align: center; letter-spacing: 0.04em; font-family: monospace; color: var(--input-text, inherit); line-height: 1.45; }
|
||||
.koolook-update-footer { flex-shrink: 0; display: none; justify-content: center; gap: 6px; padding: 4px 10px 12px; color: var(--input-text, inherit); opacity: 0.62; font-size: 11px; line-height: 1.45; text-align: center; }
|
||||
.koolook-update-footer-visible { display: flex; }
|
||||
.koolook-update-footer a { color: #b9d8ff; font-weight: 700; text-decoration: none; opacity: 1; }
|
||||
.koolook-update-footer a:hover { color: #ffffff; }
|
||||
.koolook-build-tag { flex-shrink: 0; padding: 0 10px 12px; font-size: 10px; opacity: 0.5; text-align: center; letter-spacing: 0.04em; font-family: monospace; color: var(--input-text, inherit); line-height: 1.45; }
|
||||
.koolook-build-sha { font-size: 13px; letter-spacing: 0.06em; }
|
||||
.koolook-build-scope { display: block; font-size: 12px; margin-top: 3px; letter-spacing: 0.02em; font-family: var(--font-family, sans-serif); font-style: italic; }
|
||||
/* Folder-browse picker (issue #137, mockup section 6). Navigate-into
|
||||
|
||||
+11
-1
@@ -74,6 +74,7 @@ import {
|
||||
clearWorkflowsRenderSource,
|
||||
} from "./workflows_store.js";
|
||||
import { diffPicks, diffWorkflows, getWorkflowEntryFromStore } from "./snapshot_diff.js";
|
||||
import { checkForUpdate, renderUpdateFooter } from "./update_check.js";
|
||||
import {
|
||||
serializeFullCanvas,
|
||||
serializeSelection,
|
||||
@@ -2544,6 +2545,7 @@ export function renderSidebar(container) {
|
||||
signal,
|
||||
readOnly: !liveIsActive,
|
||||
pullIn: liveIsActive ? null : pullIn,
|
||||
showUpdateFooter: false,
|
||||
});
|
||||
renderPanel(rightCol.host, {
|
||||
compare: true,
|
||||
@@ -2553,6 +2555,7 @@ export function renderSidebar(container) {
|
||||
compareName: snapName,
|
||||
readOnly: true,
|
||||
pullIn: liveIsActive ? pullIn : null,
|
||||
showUpdateFooter: false,
|
||||
});
|
||||
// Diff/tint/filter follow the SOURCE (the read-only side you copy FROM):
|
||||
// "new" = in the source, not the target — the copy candidates — so the
|
||||
@@ -3105,7 +3108,7 @@ function applyCompareTint(panelEl, targetPicks, targetStore, sourcePicks, source
|
||||
}
|
||||
|
||||
export function renderPanel(container, options = {}) {
|
||||
const { compare = false, snapshot = null, onToggleCompare = null, signal = null, compareName = "", readOnly = compare, pullIn = null } = options;
|
||||
const { compare = false, snapshot = null, onToggleCompare = null, signal = null, compareName = "", readOnly = compare, pullIn = null, showUpdateFooter = true } = options;
|
||||
// Compare mode (#181): when `compare` is set, every tree (re-)render in
|
||||
// this panel reads from the loaded `snapshot` via the read-only store
|
||||
// overrides, and the live-change listeners at the bottom are skipped so
|
||||
@@ -3828,6 +3831,13 @@ export function renderPanel(container, options = {}) {
|
||||
|
||||
withSource(() => renderTree({ treeEl: tree, query: "" }));
|
||||
|
||||
const updateFooter = document.createElement("div");
|
||||
updateFooter.className = "koolook-update-footer";
|
||||
container.appendChild(updateFooter);
|
||||
if (showUpdateFooter) {
|
||||
checkForUpdate().then((update) => renderUpdateFooter(updateFooter, update));
|
||||
}
|
||||
|
||||
// ---- Build tag (dev-sync verification) ----
|
||||
// `scripts/sync_to_dev.py` writes `web/_dev_build.json` post-sync so the
|
||||
// maintainer can eyeball whether the live ComfyUI is running the latest
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
|
||||
import { noStoreUrl } from "./constants.js";
|
||||
|
||||
const LOCAL_VERSION_ROUTE = "/koolook/api/version";
|
||||
const FALLBACK_LATEST_RELEASE_API =
|
||||
"https://api.github.com/repos/malkuthro/ComfyUI-Koolook/releases/latest";
|
||||
const RELEASES_URL = "https://github.com/malkuthro/ComfyUI-Koolook/releases";
|
||||
|
||||
function releaseNotesUrl(version, rawUrl) {
|
||||
const expected = `${RELEASES_URL}/tag/v${version}`;
|
||||
if (typeof rawUrl !== "string") return expected;
|
||||
try {
|
||||
const parsed = new URL(rawUrl);
|
||||
if (
|
||||
parsed.protocol === "https:" &&
|
||||
parsed.hostname === "github.com" &&
|
||||
parsed.pathname === `/malkuthro/ComfyUI-Koolook/releases/tag/v${version}`
|
||||
) {
|
||||
return parsed.href;
|
||||
}
|
||||
} catch (_err) {
|
||||
// Fall through to the canonical release URL.
|
||||
}
|
||||
return expected;
|
||||
}
|
||||
|
||||
function parseVersion(value) {
|
||||
if (typeof value !== "string") return null;
|
||||
const match = value.trim().match(/^v?(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+.*)?$/);
|
||||
if (!match) return null;
|
||||
return {
|
||||
parts: match.slice(1, 4).map((part) => Number(part)),
|
||||
prerelease: match[4] || "",
|
||||
};
|
||||
}
|
||||
|
||||
export function parseSemver(value) {
|
||||
const parsed = parseVersion(value);
|
||||
return parsed ? parsed.parts : null;
|
||||
}
|
||||
|
||||
export function compareSemver(a, b) {
|
||||
const av = parseVersion(a);
|
||||
const bv = parseVersion(b);
|
||||
if (!av || !bv) return 0;
|
||||
for (let i = 0; i < 3; i += 1) {
|
||||
if (av.parts[i] > bv.parts[i]) return 1;
|
||||
if (av.parts[i] < bv.parts[i]) return -1;
|
||||
}
|
||||
if (av.prerelease && !bv.prerelease) return -1;
|
||||
if (!av.prerelease && bv.prerelease) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
function normalizeLatestRelease(payload) {
|
||||
if (!payload || typeof payload !== "object") return null;
|
||||
const version = typeof payload.tag_name === "string"
|
||||
? payload.tag_name.replace(/^v/i, "")
|
||||
: "";
|
||||
if (!parseSemver(version)) return null;
|
||||
return {
|
||||
version,
|
||||
url: releaseNotesUrl(version, payload.html_url),
|
||||
};
|
||||
}
|
||||
|
||||
export async function checkForUpdate(fetchImpl = fetch) {
|
||||
try {
|
||||
const localResponse = await fetchImpl(noStoreUrl(LOCAL_VERSION_ROUTE), {
|
||||
cache: "no-store",
|
||||
});
|
||||
if (!localResponse || !localResponse.ok) return null;
|
||||
const local = await localResponse.json();
|
||||
const current = typeof local.version === "string" ? local.version : "";
|
||||
if (!parseSemver(current)) return null;
|
||||
|
||||
const latestUrl = typeof local.latestReleaseApiUrl === "string" && local.latestReleaseApiUrl
|
||||
? local.latestReleaseApiUrl
|
||||
: FALLBACK_LATEST_RELEASE_API;
|
||||
const latestResponse = await fetchImpl(latestUrl, { cache: "no-store" });
|
||||
if (!latestResponse || !latestResponse.ok) return null;
|
||||
const latest = normalizeLatestRelease(await latestResponse.json());
|
||||
if (!latest || compareSemver(latest.version, current) <= 0) return null;
|
||||
|
||||
return {
|
||||
current,
|
||||
latest: latest.version,
|
||||
url: latest.url,
|
||||
};
|
||||
} catch (_err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderUpdateFooter(footerEl, update) {
|
||||
footerEl.textContent = "";
|
||||
footerEl.classList.remove("koolook-update-footer-visible");
|
||||
if (!update) return;
|
||||
|
||||
const label = document.createElement("span");
|
||||
label.textContent = "Update Available";
|
||||
const link = document.createElement("a");
|
||||
link.href = update.url;
|
||||
link.target = "_blank";
|
||||
link.rel = "noopener noreferrer";
|
||||
link.textContent = "Release notes";
|
||||
link.title = `Kforge Labs ${update.latest} release notes`;
|
||||
footerEl.append(label, link);
|
||||
footerEl.classList.add("koolook-update-footer-visible");
|
||||
}
|
||||
Reference in New Issue
Block a user