mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-10-06 01:45:31 -03:00
refactor(update)!: make obtainability a property of updates, not a surface
The owner could not tell from the UI what "Buzz Price Tracking" enabled, what the "Price alert threshold" number meant, or what "Price Alerts" was alerting about. That was not a copy problem: the implementation exposed our mechanism (a page scrape) and our SQL predicates as the user's concepts. Two concrete defects came from the same root: - the alert population included versions the user already owns (neither the event generator nor the query filtered on is_in_library; in the owner's library 28 of 52 gated versions were already downloaded, so most "alerts" were about files already on disk, which cannot become cheaper *for them*); - a threshold-filtered state list lived in a notification surface, so an empty panel had three indistinguishable causes and read as a broken feature. The information model is now the version plus ownership: cost is shown only where a decision exists. Owned -> nothing. Not owned and free -> nothing. Not owned and gated -> the price when it is known, `Paid` without a number when it is not, and early access keeps its countdown because "free on <date>" decides between waiting and paying. The numeric threshold has no place in that model: every decision is categorical (wait / pay / skip), so the setting, the comparison and the whole alert-state machine are gone. - both alert-state columns are removed from the schema rather than left dead; a database created by an unreleased build has them dropped on open (native ALTER TABLE ... DROP COLUMN, guarded), which is a no-op for everyone else - gate events are emitted only for versions the user does not have, and the price-drop event goes with the threshold it belonged to - both alert endpoints, PriceAlertsHandler and the service-registry adapter field it needed are removed: events already reach the UI through the refresh response - the bell tab, panel, CSS, both entry points, the unread watermark and their locales are removed; the setting keeps only the enable flag and the refresh interval and is framed as plumbing - "Price unavailable" is replaced by `Paid`: the gate is certain from the public API, only the number is best-effort, and that is our plumbing, not the user's problem Verified against a copy of the owner's real database: 52 gated versions -> 28 owned (now silent) + 24 the feature is actually about; the drop migration ran and both removed endpoints 404.
This commit is contained in:
@@ -4,7 +4,6 @@ import { translate } from '../../utils/i18nHelpers.js';
|
||||
import { state } from '../../state/index.js';
|
||||
import { getCompleteApiConfig, getCurrentModelType } from '../../api/apiConfig.js';
|
||||
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
||||
import { openPriceAlertsPanel, updateService } from '../../managers/UpdateService.js';
|
||||
import { rematchModalManager } from '../../managers/RematchModalManager.js';
|
||||
import { showRematchSummary } from '../RematchSummaryModal.js';
|
||||
|
||||
@@ -28,7 +27,6 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
const excludedModelsItem = this.menu.querySelector('[data-action="manage-excluded-models"]');
|
||||
const rematchRecipesItem = this.menu.querySelector('[data-action="rematch-recipes"]');
|
||||
const groupByModelItem = this.menu.querySelector('[data-action="toggle-group-by-model"]');
|
||||
const priceAlertsItem = this.menu.querySelector('[data-action="price-alerts"]');
|
||||
const groupByModelCheck = groupByModelItem?.querySelector('.check-indicator');
|
||||
|
||||
// Update check indicator for group-by-model
|
||||
@@ -37,16 +35,6 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
groupByModelCheck.style.display = isEnabled ? 'block' : 'none';
|
||||
}
|
||||
|
||||
// Append the unread count when it is already known (loaded once on init,
|
||||
// and only when price tracking is on), so the label stays a plain label
|
||||
// for everyone else.
|
||||
const priceAlertsLabel = priceAlertsItem?.querySelector('#globalPriceAlertsLabel');
|
||||
if (priceAlertsLabel) {
|
||||
const unread = updateService?.getUnreadPriceAlertCount?.() || 0;
|
||||
const base = translate('globalContextMenu.priceAlerts.label', {}, 'Price alerts');
|
||||
priceAlertsLabel.textContent = unread > 0 ? `${base} (${unread})` : base;
|
||||
}
|
||||
|
||||
if (isRecipesPage) {
|
||||
modelUpdateItem?.classList.add('hidden');
|
||||
licenseRefreshItem?.classList.add('hidden');
|
||||
@@ -54,7 +42,6 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
cleanupExamplesItem?.classList.add('hidden');
|
||||
excludedModelsItem?.classList.add('hidden');
|
||||
groupByModelItem?.classList.add('hidden');
|
||||
priceAlertsItem?.classList.add('hidden');
|
||||
rematchRecipesItem?.classList.remove('hidden');
|
||||
} else {
|
||||
modelUpdateItem?.classList.remove('hidden');
|
||||
@@ -63,7 +50,6 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
cleanupExamplesItem?.classList.remove('hidden');
|
||||
excludedModelsItem?.classList.remove('hidden');
|
||||
groupByModelItem?.classList.remove('hidden');
|
||||
priceAlertsItem?.classList.remove('hidden');
|
||||
rematchRecipesItem?.classList.add('hidden');
|
||||
}
|
||||
|
||||
@@ -103,9 +89,6 @@ export class GlobalContextMenu extends BaseContextMenu {
|
||||
console.error('Failed to check model updates:', error);
|
||||
});
|
||||
break;
|
||||
case 'price-alerts':
|
||||
openPriceAlertsPanel();
|
||||
break;
|
||||
case 'fetch-missing-licenses':
|
||||
this.fetchMissingLicenses(menuItem).catch((error) => {
|
||||
console.error('Failed to refresh missing license metadata:', error);
|
||||
|
||||
@@ -4,7 +4,6 @@ import { getStorageItem, setStorageItem, removeStorageItem, getSessionItem, setS
|
||||
import { showToast, openCivitaiByMetadata, isTypingContext } from '../../utils/uiHelpers.js';
|
||||
import { eventManager } from '../../utils/EventManager.js';
|
||||
import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js';
|
||||
import { openPriceAlertsPanel } from '../../managers/UpdateService.js';
|
||||
import { sidebarManager } from '../SidebarManager.js';
|
||||
import { initSortDropdown, applySortToSelect, randomizeSortValue } from './SortDropdown.js';
|
||||
|
||||
@@ -257,17 +256,6 @@ export class PageControls {
|
||||
});
|
||||
}
|
||||
|
||||
// Opens the notification bell on the price alerts tab (same action as the
|
||||
// global context menu entry).
|
||||
const priceAlertsOption = document.getElementById('priceAlertsMenuItem');
|
||||
if (priceAlertsOption) {
|
||||
priceAlertsOption.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelector('.dropdown-group.active')?.classList.remove('active');
|
||||
openPriceAlertsPanel();
|
||||
});
|
||||
}
|
||||
|
||||
// Close dropdowns when clicking outside
|
||||
document.addEventListener('click', (e) => {
|
||||
if (!e.target.closest('.dropdown-group')) {
|
||||
|
||||
@@ -569,65 +569,58 @@ function renderRow(version, options) {
|
||||
}));
|
||||
}
|
||||
|
||||
if (isEarlyAccess) {
|
||||
// Obtainability, only where there is a decision to make: a version already in
|
||||
// the library cannot become cheaper *for this user*, so it gets no cost badge.
|
||||
// The download price is the only price axis that matters here — the generation
|
||||
// fee is what CivitAI charges to generate on its own site, which a local model
|
||||
// never incurs.
|
||||
const priceLabel = formatBuzzPrice(version.priceBuzz);
|
||||
if (!version.isInLibrary && isEarlyAccess) {
|
||||
// Early access is the one state with a real countdown: "free on <date>"
|
||||
// decides between waiting and paying.
|
||||
const eaTime = formatEarlyAccessTime(version.earlyAccessEndsAt);
|
||||
badges.push(buildBadge(earlyAccessBadgeLabel, 'early-access', {
|
||||
title: buildPriceTooltip(
|
||||
version,
|
||||
translate(
|
||||
'modals.model.versions.badges.earlyAccessTooltip',
|
||||
{},
|
||||
'This version currently requires Civitai early access'
|
||||
)
|
||||
eaTime
|
||||
? translate(
|
||||
'modals.model.versions.badges.earlyAccessTooltipUntil',
|
||||
{ date: eaTime },
|
||||
`Early access until ${eaTime}`
|
||||
)
|
||||
: translate(
|
||||
'modals.model.versions.badges.earlyAccessTooltip',
|
||||
{},
|
||||
'This version currently requires Civitai early access'
|
||||
)
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
if (isPaidPermanent(version)) {
|
||||
badges.push(buildBadge(paidBadgeLabel, 'paid', {
|
||||
title: buildPriceTooltip(
|
||||
version,
|
||||
translate(
|
||||
if (!version.isInLibrary && isGated(version)) {
|
||||
if (priceLabel) {
|
||||
// The number already says "paid", so a separate Paid badge would be
|
||||
// noise next to it.
|
||||
badges.push(buildBadge(priceLabel, isEarlyAccess ? 'early-access' : 'paid', {
|
||||
title: buildPriceTooltip(version, paidBadgeLabel),
|
||||
}));
|
||||
} else if (!isEarlyAccess) {
|
||||
// Paid, but no readable price: the gate is certain (it comes from the
|
||||
// public API), only the number is best-effort. Saying nothing here
|
||||
// would read as "free", which is the one thing this version is not.
|
||||
badges.push(buildBadge(paidBadgeLabel, 'paid', {
|
||||
title: translate(
|
||||
'modals.model.versions.badges.paidTooltip',
|
||||
{},
|
||||
'This version requires payment to download'
|
||||
)
|
||||
),
|
||||
}));
|
||||
}
|
||||
|
||||
// Known price for a gated version. The price is only captured when the
|
||||
// optional price tracking is enabled, so this stays hidden otherwise.
|
||||
const priceLabel = isGated(version) ? formatBuzzPrice(version.priceBuzz) : '';
|
||||
if (priceLabel) {
|
||||
const alertsEnabled = !!version.priceAlert;
|
||||
badges.push(buildBadge(priceLabel, alertsEnabled ? 'price-alert' : 'paid', {
|
||||
title: alertsEnabled
|
||||
? translate(
|
||||
'modals.model.versions.badges.priceAlertTooltip',
|
||||
{ price: priceLabel },
|
||||
`${priceLabel} - below your price alert threshold`
|
||||
)
|
||||
: buildPriceTooltip(version, paidBadgeLabel),
|
||||
}));
|
||||
} else if (isGated(version) && version.priceAttemptedAt) {
|
||||
// We looked and could not read a price. Staying silent would read as
|
||||
// "free", which is the one thing this version is not.
|
||||
badges.push(buildBadge(
|
||||
translate('modals.model.versions.badges.priceUnavailable', {}, 'Price unavailable'),
|
||||
'muted',
|
||||
{
|
||||
title: translate(
|
||||
'modals.model.versions.badges.priceUnavailableTooltip',
|
||||
{},
|
||||
'CivitAI does not publish this price in its public API, and mature model pages can only be read in a browser'
|
||||
),
|
||||
}
|
||||
));
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// A version that used to be gated and no longer is. `gateLapsedAt` is
|
||||
// persisted, so the marker survives long after the refresh that saw it.
|
||||
if (!isGated(version) && version.gateLapsedAt) {
|
||||
if (!version.isInLibrary && !isGated(version) && version.gateLapsedAt) {
|
||||
const freeBadgeLabel = translate('modals.model.versions.badges.freeNow', {}, 'Free Now');
|
||||
badges.push(buildBadge(freeBadgeLabel, 'success', {
|
||||
title: translate(
|
||||
|
||||
Reference in New Issue
Block a user