mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(early-access): implement EA filtering and UI improvements
Add Early Access version support with filtering and improved UI: Backend: - Add is_early_access and early_access_ends_at fields to ModelVersionRecord - Implement two-phase EA detection (bulk API + single API enrichment) - Add hide_early_access_updates setting to filter EA updates - Update has_update() and has_updates_bulk() to respect EA filter setting - Add _enrich_early_access_details() for precise EA time fetching - Fix setting propagation through base_model_service and model_update_service Frontend: - Add smart relative time display for EA (in Xh, in Xd, or date) - Replace EA label with clock icon in metadata (fa-clock) - Show Download button with bolt icon for EA versions (fa-bolt) - Change EA badge color to #F59F00 (CivitAI Buzz theme) - Fix toggle UI for hide_early_access_updates setting - Add translation keys for EA time formatting Tests: - Update all tests to pass with new EA functionality - Add test coverage for EA filtering logic Closes #815
This commit is contained in:
@@ -387,3 +387,51 @@
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Early Access styles - Buzz theme color (#F59F00) */
|
||||
.version-badge-early-access {
|
||||
background: color-mix(in oklch, #F59F00 25%, transparent);
|
||||
color: #E67700;
|
||||
border-color: color-mix(in oklch, #F59F00 55%, transparent);
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-badge-early-access {
|
||||
background: color-mix(in oklch, #F59F00 20%, transparent);
|
||||
color: #F59F00;
|
||||
border-color: color-mix(in oklch, #F59F00 45%, transparent);
|
||||
}
|
||||
|
||||
.version-meta-ea {
|
||||
color: #E67700;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-meta-ea {
|
||||
color: #F59F00;
|
||||
}
|
||||
|
||||
/* Early Access row - gray out effect */
|
||||
.model-version-row.is-early-access {
|
||||
opacity: 0.85;
|
||||
filter: grayscale(40%);
|
||||
transition: opacity 0.2s ease, filter 0.2s ease;
|
||||
}
|
||||
|
||||
.model-version-row.is-early-access:hover {
|
||||
opacity: 0.95;
|
||||
filter: grayscale(25%);
|
||||
}
|
||||
|
||||
/* Early Access download button - Buzz theme color (#F59F00) */
|
||||
.version-action-early-access {
|
||||
background: color-mix(in oklch, #F59F00 15%, transparent);
|
||||
color: #E67700;
|
||||
border-color: color-mix(in oklch, #F59F00 50%, transparent);
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
[data-theme="dark"] .version-action-early-access {
|
||||
background: color-mix(in oklch, #F59F00 12%, transparent);
|
||||
color: #F59F00;
|
||||
border-color: color-mix(in oklch, #F59F00 40%, transparent);
|
||||
}
|
||||
|
||||
@@ -123,7 +123,70 @@ function formatDateLabel(value) {
|
||||
});
|
||||
}
|
||||
|
||||
function buildMetaMarkup(version) {
|
||||
/**
|
||||
* Format EA end time as smart relative time
|
||||
* - < 1 day: "in Xh" (hours)
|
||||
* - 1-7 days: "in Xd" (days)
|
||||
* - > 7 days: "Jan 15" (short date)
|
||||
*/
|
||||
function formatEarlyAccessTime(endsAt) {
|
||||
if (!endsAt) {
|
||||
return null;
|
||||
}
|
||||
const endDate = new Date(endsAt);
|
||||
if (Number.isNaN(endDate.getTime())) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const diffMs = endDate.getTime() - now.getTime();
|
||||
const diffHours = diffMs / (1000 * 60 * 60);
|
||||
const diffDays = diffHours / 24;
|
||||
|
||||
if (diffHours < 1) {
|
||||
return translate('modals.model.versions.eaTime.endingSoon', {}, 'ending soon');
|
||||
}
|
||||
if (diffHours < 24) {
|
||||
const hours = Math.ceil(diffHours);
|
||||
return translate(
|
||||
'modals.model.versions.eaTime.hours',
|
||||
{ count: hours },
|
||||
`in ${hours}h`
|
||||
);
|
||||
}
|
||||
if (diffDays <= 7) {
|
||||
const days = Math.ceil(diffDays);
|
||||
return translate(
|
||||
'modals.model.versions.eaTime.days',
|
||||
{ count: days },
|
||||
`in ${days}d`
|
||||
);
|
||||
}
|
||||
// More than 7 days: show short date
|
||||
return endDate.toLocaleDateString(undefined, {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
function isEarlyAccessActive(version) {
|
||||
// Two-phase detection:
|
||||
// 1. Use pre-computed isEarlyAccess flag if available (from backend)
|
||||
// 2. Otherwise check exact end time if available
|
||||
if (typeof version.isEarlyAccess === 'boolean') {
|
||||
return version.isEarlyAccess;
|
||||
}
|
||||
if (!version.earlyAccessEndsAt) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return new Date(version.earlyAccessEndsAt) > new Date();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function buildMetaMarkup(version, options = {}) {
|
||||
const segments = [];
|
||||
if (version.baseModel) {
|
||||
segments.push(
|
||||
@@ -138,6 +201,14 @@ function buildMetaMarkup(version) {
|
||||
segments.push(escapeHtml(formatFileSize(version.sizeBytes)));
|
||||
}
|
||||
|
||||
// Add early access info if applicable
|
||||
if (options.showEarlyAccess && isEarlyAccessActive(version)) {
|
||||
const eaTime = formatEarlyAccessTime(version.earlyAccessEndsAt);
|
||||
if (eaTime) {
|
||||
segments.push(`<span class="version-meta-ea"><i class="fas fa-clock"></i> ${escapeHtml(eaTime)}</span>`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!segments.length) {
|
||||
return escapeHtml(
|
||||
translate('modals.model.versions.labels.noDetails', {}, 'No additional details')
|
||||
@@ -235,6 +306,7 @@ function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
||||
|
||||
const strategy = state?.global?.settings?.update_flag_strategy;
|
||||
const sameBaseMode = strategy === DISPLAY_FILTER_MODES.SAME_BASE;
|
||||
const hideEarlyAccess = state?.global?.settings?.hide_early_access_updates;
|
||||
|
||||
if (!sameBaseMode) {
|
||||
return Boolean(record?.hasUpdate);
|
||||
@@ -278,6 +350,9 @@ function resolveUpdateAvailability(record, baseModel, currentVersionId) {
|
||||
if (version.isInLibrary || version.shouldIgnore) {
|
||||
return false;
|
||||
}
|
||||
if (hideEarlyAccess && isEarlyAccessActive(version)) {
|
||||
return false;
|
||||
}
|
||||
const versionBase = normalizeBaseModelName(version.baseModel);
|
||||
if (versionBase !== normalizedBase) {
|
||||
return false;
|
||||
@@ -349,6 +424,7 @@ function renderRow(version, options) {
|
||||
const isNewer =
|
||||
typeof latestLibraryVersionId === 'number' &&
|
||||
version.versionId > latestLibraryVersionId;
|
||||
const isEarlyAccess = isEarlyAccessActive(version);
|
||||
const badges = [];
|
||||
|
||||
if (isCurrent) {
|
||||
@@ -361,6 +437,10 @@ function renderRow(version, options) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.newer', {}, 'Newer Version'), 'info'));
|
||||
}
|
||||
|
||||
if (isEarlyAccess) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.earlyAccess', {}, 'Early Access'), 'early-access'));
|
||||
}
|
||||
|
||||
if (version.shouldIgnore) {
|
||||
badges.push(buildBadge(translate('modals.model.versions.badges.ignored', {}, 'Ignored'), 'muted'));
|
||||
}
|
||||
@@ -377,8 +457,10 @@ function renderRow(version, options) {
|
||||
|
||||
const actions = [];
|
||||
if (!version.isInLibrary) {
|
||||
// Download button with optional EA bolt icon
|
||||
const downloadIcon = isEarlyAccess ? '<i class="fas fa-bolt"></i> ' : '';
|
||||
actions.push(
|
||||
`<button class="version-action version-action-primary" data-version-action="download">${escapeHtml(downloadLabel)}</button>`
|
||||
`<button class="version-action version-action-primary" data-version-action="download">${downloadIcon}${escapeHtml(downloadLabel)}</button>`
|
||||
);
|
||||
} else if (version.filePath) {
|
||||
actions.push(
|
||||
@@ -402,7 +484,7 @@ function renderRow(version, options) {
|
||||
);
|
||||
|
||||
const rowAttributes = [
|
||||
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}"`,
|
||||
`class="model-version-row${isCurrent ? ' is-current' : ''}${linkTarget ? ' is-clickable' : ''}${isEarlyAccess ? ' is-early-access' : ''}"`,
|
||||
`data-version-id="${escapeHtml(version.versionId)}"`,
|
||||
];
|
||||
if (linkTarget) {
|
||||
@@ -419,7 +501,7 @@ function renderRow(version, options) {
|
||||
</div>
|
||||
<div class="version-badges">${badges.join('')}</div>
|
||||
<div class="version-meta">
|
||||
${buildMetaMarkup(version)}
|
||||
${buildMetaMarkup(version, { showEarlyAccess: true })}
|
||||
</div>
|
||||
</div>
|
||||
<div class="version-actions">
|
||||
|
||||
@@ -475,6 +475,12 @@ export class SettingsManager {
|
||||
updateFlagStrategySelect.value = state.global.settings.update_flag_strategy || 'same_base';
|
||||
}
|
||||
|
||||
// Set hide early access updates setting
|
||||
const hideEarlyAccessUpdatesCheckbox = document.getElementById('hideEarlyAccessUpdates');
|
||||
if (hideEarlyAccessUpdatesCheckbox) {
|
||||
hideEarlyAccessUpdatesCheckbox.checked = state.global.settings.hide_early_access_updates || false;
|
||||
}
|
||||
|
||||
// Set optimize example images setting
|
||||
const optimizeExampleImagesCheckbox = document.getElementById('optimizeExampleImages');
|
||||
if (optimizeExampleImagesCheckbox) {
|
||||
|
||||
@@ -34,6 +34,7 @@ const DEFAULT_SETTINGS_BASE = Object.freeze({
|
||||
compact_mode: false,
|
||||
priority_tags: { ...DEFAULT_PRIORITY_TAG_CONFIG },
|
||||
update_flag_strategy: 'same_base',
|
||||
hide_early_access_updates: false,
|
||||
auto_organize_exclusions: [],
|
||||
metadata_refresh_skip_paths: [],
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user