mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat: implement banner service for managing notification banners, including UI integration and storage handling
This commit is contained in:
245
static/css/components/banner.css
Normal file
245
static/css/components/banner.css
Normal file
@@ -0,0 +1,245 @@
|
|||||||
|
/* Banner Container */
|
||||||
|
.banner-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
z-index: calc(var(--z-header) - 1);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
background: var(--card-bg);
|
||||||
|
margin-bottom: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Individual Banner */
|
||||||
|
.banner-item {
|
||||||
|
position: relative;
|
||||||
|
padding: var(--space-2) var(--space-3);
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.05),
|
||||||
|
oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.02)
|
||||||
|
);
|
||||||
|
border-left: 4px solid var(--lora-accent);
|
||||||
|
animation: banner-slide-down 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner Content Layout */
|
||||||
|
.banner-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--space-3);
|
||||||
|
max-width: 1400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner Text Section */
|
||||||
|
.banner-text {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
margin: 0 0 4px 0;
|
||||||
|
font-size: 1.1em;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-color);
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--text-muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Banner Actions */
|
||||||
|
.banner-actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--space-1);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: var(--border-radius-xs);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 0.85em;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action i {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Primary Action Button */
|
||||||
|
.banner-action-primary {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action-primary:hover {
|
||||||
|
background: oklch(calc(var(--lora-accent-l) - 5%) var(--lora-accent-c) var(--lora-accent-h));
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 3px 6px oklch(var(--lora-accent) / 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Secondary Action Button */
|
||||||
|
.banner-action-secondary {
|
||||||
|
background: var(--card-bg);
|
||||||
|
color: var(--text-color);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action-secondary:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Tertiary Action Button */
|
||||||
|
.banner-action-tertiary {
|
||||||
|
background: transparent;
|
||||||
|
color: var(--lora-accent);
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action-tertiary:hover {
|
||||||
|
background: var(--lora-accent);
|
||||||
|
color: white;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dismiss Button */
|
||||||
|
.banner-dismiss {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-size: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-dismiss:hover {
|
||||||
|
background: oklch(var(--lora-accent) / 0.1);
|
||||||
|
color: var(--lora-accent);
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes banner-slide-down {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes banner-slide-up {
|
||||||
|
from {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
max-height: 200px;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-20px);
|
||||||
|
max-height: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.banner-content {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-actions {
|
||||||
|
width: 100%;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-dismiss {
|
||||||
|
top: 6px;
|
||||||
|
right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-item {
|
||||||
|
padding: var(--space-2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-title {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-description {
|
||||||
|
font-size: 0.85em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.banner-actions {
|
||||||
|
flex-direction: column;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-action {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.banner-content {
|
||||||
|
gap: var(--space-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme adjustments */
|
||||||
|
[data-theme="dark"] .banner-item {
|
||||||
|
background: linear-gradient(135deg,
|
||||||
|
oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.08),
|
||||||
|
oklch(var(--lora-accent-l) var(--lora-accent-c) var(--lora-accent-h) / 0.03)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Prevent text selection */
|
||||||
|
.banner-item,
|
||||||
|
.banner-title,
|
||||||
|
.banner-description,
|
||||||
|
.banner-action,
|
||||||
|
.banner-dismiss {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
/* Import Components */
|
/* Import Components */
|
||||||
@import 'components/header.css';
|
@import 'components/header.css';
|
||||||
|
@import 'components/banner.css';
|
||||||
@import 'components/card.css';
|
@import 'components/card.css';
|
||||||
@import 'components/modal/_base.css';
|
@import 'components/modal/_base.css';
|
||||||
@import 'components/modal/delete-modal.css';
|
@import 'components/modal/delete-modal.css';
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { HeaderManager } from './components/Header.js';
|
|||||||
import { settingsManager } from './managers/SettingsManager.js';
|
import { settingsManager } from './managers/SettingsManager.js';
|
||||||
import { exampleImagesManager } from './managers/ExampleImagesManager.js';
|
import { exampleImagesManager } from './managers/ExampleImagesManager.js';
|
||||||
import { helpManager } from './managers/HelpManager.js';
|
import { helpManager } from './managers/HelpManager.js';
|
||||||
|
import { bannerService } from './managers/BannerService.js';
|
||||||
import { showToast, initTheme, initBackToTop } from './utils/uiHelpers.js';
|
import { showToast, initTheme, initBackToTop } from './utils/uiHelpers.js';
|
||||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||||
@@ -27,6 +28,7 @@ export class AppCore {
|
|||||||
state.loadingManager = new LoadingManager();
|
state.loadingManager = new LoadingManager();
|
||||||
modalManager.initialize();
|
modalManager.initialize();
|
||||||
updateService.initialize();
|
updateService.initialize();
|
||||||
|
bannerService.initialize();
|
||||||
window.modalManager = modalManager;
|
window.modalManager = modalManager;
|
||||||
window.settingsManager = settingsManager;
|
window.settingsManager = settingsManager;
|
||||||
window.exampleImagesManager = exampleImagesManager;
|
window.exampleImagesManager = exampleImagesManager;
|
||||||
|
|||||||
176
static/js/managers/BannerService.js
Normal file
176
static/js/managers/BannerService.js
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Banner Service for managing notification banners
|
||||||
|
*/
|
||||||
|
class BannerService {
|
||||||
|
constructor() {
|
||||||
|
this.banners = new Map();
|
||||||
|
this.container = null;
|
||||||
|
this.initialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the banner service
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
if (this.initialized) return;
|
||||||
|
|
||||||
|
this.container = document.getElementById('banner-container');
|
||||||
|
if (!this.container) {
|
||||||
|
console.warn('Banner container not found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register default banners
|
||||||
|
this.registerBanner('civitai-extension', {
|
||||||
|
id: 'civitai-extension',
|
||||||
|
title: 'New Tool Available: LM Civitai Extension!',
|
||||||
|
content: 'LM Civitai Extension is a browser extension designed to work seamlessly with LoRA Manager to significantly enhance your Civitai browsing experience! See which models you already have, download new ones with a single click, and manage your downloads efficiently.',
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
text: 'Chrome Web Store',
|
||||||
|
icon: 'fab fa-chrome',
|
||||||
|
url: 'https://chromewebstore.google.com/detail/capigligggeijgmocnaflanlbghnamgm?utm_source=item-share-cb',
|
||||||
|
type: 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Firefox Extension',
|
||||||
|
icon: 'fab fa-firefox-browser',
|
||||||
|
url: 'https://github.com/willmiao/lm-civitai-extension-firefox/releases/latest/download/extension.xpi',
|
||||||
|
type: 'secondary'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: 'Read more...',
|
||||||
|
icon: 'fas fa-book',
|
||||||
|
url: 'https://github.com/willmiao/ComfyUI-Lora-Manager/wiki/LoRA-Manager-Civitai-Extension-(Chrome-Extension)',
|
||||||
|
type: 'tertiary'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
dismissible: true,
|
||||||
|
priority: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
this.showActiveBanners();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a new banner
|
||||||
|
* @param {string} id - Unique banner ID
|
||||||
|
* @param {Object} bannerConfig - Banner configuration
|
||||||
|
*/
|
||||||
|
registerBanner(id, bannerConfig) {
|
||||||
|
this.banners.set(id, bannerConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a banner has been dismissed
|
||||||
|
* @param {string} bannerId - Banner ID
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isBannerDismissed(bannerId) {
|
||||||
|
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||||
|
return dismissedBanners.includes(bannerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dismiss a banner
|
||||||
|
* @param {string} bannerId - Banner ID
|
||||||
|
*/
|
||||||
|
dismissBanner(bannerId) {
|
||||||
|
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||||
|
if (!dismissedBanners.includes(bannerId)) {
|
||||||
|
dismissedBanners.push(bannerId);
|
||||||
|
setStorageItem('dismissed_banners', dismissedBanners);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove banner from DOM
|
||||||
|
const bannerElement = document.querySelector(`[data-banner-id="${bannerId}"]`);
|
||||||
|
if (bannerElement) {
|
||||||
|
bannerElement.style.animation = 'banner-slide-up 0.3s ease-in-out forwards';
|
||||||
|
setTimeout(() => {
|
||||||
|
bannerElement.remove();
|
||||||
|
this.updateContainerVisibility();
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show all active (non-dismissed) banners
|
||||||
|
*/
|
||||||
|
showActiveBanners() {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const activeBanners = Array.from(this.banners.values())
|
||||||
|
.filter(banner => !this.isBannerDismissed(banner.id))
|
||||||
|
.sort((a, b) => (b.priority || 0) - (a.priority || 0));
|
||||||
|
|
||||||
|
activeBanners.forEach(banner => {
|
||||||
|
this.renderBanner(banner);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.updateContainerVisibility();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render a banner to the DOM
|
||||||
|
* @param {Object} banner - Banner configuration
|
||||||
|
*/
|
||||||
|
renderBanner(banner) {
|
||||||
|
const bannerElement = document.createElement('div');
|
||||||
|
bannerElement.className = 'banner-item';
|
||||||
|
bannerElement.setAttribute('data-banner-id', banner.id);
|
||||||
|
|
||||||
|
const actionsHtml = banner.actions ? banner.actions.map(action =>
|
||||||
|
`<a href="${action.url}" target="_blank" class="banner-action banner-action-${action.type}" rel="noopener noreferrer">
|
||||||
|
<i class="${action.icon}"></i>
|
||||||
|
<span>${action.text}</span>
|
||||||
|
</a>`
|
||||||
|
).join('') : '';
|
||||||
|
|
||||||
|
const dismissButtonHtml = banner.dismissible ?
|
||||||
|
`<button class="banner-dismiss" onclick="bannerService.dismissBanner('${banner.id}')" title="Dismiss">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>` : '';
|
||||||
|
|
||||||
|
bannerElement.innerHTML = `
|
||||||
|
<div class="banner-content">
|
||||||
|
<div class="banner-text">
|
||||||
|
<h4 class="banner-title">${banner.title}</h4>
|
||||||
|
<p class="banner-description">${banner.content}</p>
|
||||||
|
</div>
|
||||||
|
<div class="banner-actions">
|
||||||
|
${actionsHtml}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
${dismissButtonHtml}
|
||||||
|
`;
|
||||||
|
|
||||||
|
this.container.appendChild(bannerElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update container visibility based on active banners
|
||||||
|
*/
|
||||||
|
updateContainerVisibility() {
|
||||||
|
if (!this.container) return;
|
||||||
|
|
||||||
|
const hasActiveBanners = this.container.children.length > 0;
|
||||||
|
this.container.style.display = hasActiveBanners ? 'block' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all dismissed banners (for testing/admin purposes)
|
||||||
|
*/
|
||||||
|
clearDismissedBanners() {
|
||||||
|
setStorageItem('dismissed_banners', []);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create and export singleton instance
|
||||||
|
export const bannerService = new BannerService();
|
||||||
|
|
||||||
|
// Make it globally available
|
||||||
|
window.bannerService = bannerService;
|
||||||
@@ -141,7 +141,8 @@ export function migrateStorageItems() {
|
|||||||
'recipes_search_prefs',
|
'recipes_search_prefs',
|
||||||
'checkpoints_search_prefs',
|
'checkpoints_search_prefs',
|
||||||
'show_update_notifications',
|
'show_update_notifications',
|
||||||
'last_update_check'
|
'last_update_check',
|
||||||
|
'dismissed_banners'
|
||||||
];
|
];
|
||||||
|
|
||||||
// Migrate each known key
|
// Migrate each known key
|
||||||
|
|||||||
@@ -82,6 +82,11 @@
|
|||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
<!-- Banner component -->
|
||||||
|
<div id="banner-container" class="banner-container" style="display: none;">
|
||||||
|
<!-- Banners will be dynamically inserted here -->
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if is_initializing %}
|
{% if is_initializing %}
|
||||||
<!-- Show initialization component when initializing -->
|
<!-- Show initialization component when initializing -->
|
||||||
{% include 'components/initialization.html' %}
|
{% include 'components/initialization.html' %}
|
||||||
|
|||||||
Reference in New Issue
Block a user