From fe57a8e1562ad4e0714cd3f7ffa43334ade454f7 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Sun, 27 Jul 2025 18:07:43 +0800 Subject: [PATCH] feat: implement banner service for managing notification banners, including UI integration and storage handling --- static/css/components/banner.css | 245 ++++++++++++++++++++++++++++ static/css/style.css | 1 + static/js/core.js | 2 + static/js/managers/BannerService.js | 176 ++++++++++++++++++++ static/js/utils/storageHelpers.js | 3 +- templates/base.html | 5 + 6 files changed, 431 insertions(+), 1 deletion(-) create mode 100644 static/css/components/banner.css create mode 100644 static/js/managers/BannerService.js diff --git a/static/css/components/banner.css b/static/css/components/banner.css new file mode 100644 index 00000000..b19a597f --- /dev/null +++ b/static/css/components/banner.css @@ -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; +} diff --git a/static/css/style.css b/static/css/style.css index 91644220..938fcf30 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -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'; diff --git a/static/js/core.js b/static/js/core.js index 9a268217..f2ff2dfa 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -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; diff --git a/static/js/managers/BannerService.js b/static/js/managers/BannerService.js new file mode 100644 index 00000000..3711585f --- /dev/null +++ b/static/js/managers/BannerService.js @@ -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 => + ` + + ${action.text} + ` + ).join('') : ''; + + const dismissButtonHtml = banner.dismissible ? + `` : ''; + + bannerElement.innerHTML = ` +
+ ${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; diff --git a/static/js/utils/storageHelpers.js b/static/js/utils/storageHelpers.js index 050d16ee..1320d7bd 100644 --- a/static/js/utils/storageHelpers.js +++ b/static/js/utils/storageHelpers.js @@ -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 diff --git a/templates/base.html b/templates/base.html index 257ce245..7464ea1b 100644 --- a/templates/base.html +++ b/templates/base.html @@ -82,6 +82,11 @@