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/header.css';
|
||||
@import 'components/banner.css';
|
||||
@import 'components/card.css';
|
||||
@import 'components/modal/_base.css';
|
||||
@import 'components/modal/delete-modal.css';
|
||||
|
||||
@@ -7,6 +7,7 @@ import { HeaderManager } from './components/Header.js';
|
||||
import { settingsManager } from './managers/SettingsManager.js';
|
||||
import { exampleImagesManager } from './managers/ExampleImagesManager.js';
|
||||
import { helpManager } from './managers/HelpManager.js';
|
||||
import { bannerService } from './managers/BannerService.js';
|
||||
import { showToast, initTheme, initBackToTop } from './utils/uiHelpers.js';
|
||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||
@@ -27,6 +28,7 @@ export class AppCore {
|
||||
state.loadingManager = new LoadingManager();
|
||||
modalManager.initialize();
|
||||
updateService.initialize();
|
||||
bannerService.initialize();
|
||||
window.modalManager = modalManager;
|
||||
window.settingsManager = settingsManager;
|
||||
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',
|
||||
'checkpoints_search_prefs',
|
||||
'show_update_notifications',
|
||||
'last_update_check'
|
||||
'last_update_check',
|
||||
'dismissed_banners'
|
||||
];
|
||||
|
||||
// Migrate each known key
|
||||
|
||||
@@ -82,6 +82,11 @@
|
||||
</button>
|
||||
|
||||
<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 %}
|
||||
<!-- Show initialization component when initializing -->
|
||||
{% include 'components/initialization.html' %}
|
||||
|
||||
Reference in New Issue
Block a user