mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
fix: persist onboarding and banner dismiss state to backend
Moves onboarding_completed and dismissed_banners from localStorage to backend settings (settings.json) to survive incognito/private browser modes. Fixes #786
This commit is contained in:
@@ -231,6 +231,8 @@ class SettingsHandler:
|
||||
"enable_metadata_archive_db",
|
||||
"language",
|
||||
"use_portable_settings",
|
||||
"onboarding_completed",
|
||||
"dismissed_banners",
|
||||
"proxy_enabled",
|
||||
"proxy_type",
|
||||
"proxy_host",
|
||||
|
||||
@@ -35,6 +35,8 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
||||
"hash_chunk_size_mb": DEFAULT_HASH_CHUNK_SIZE_MB,
|
||||
"language": "en",
|
||||
"show_only_sfw": False,
|
||||
"onboarding_completed": False,
|
||||
"dismissed_banners": [],
|
||||
"enable_metadata_archive_db": False,
|
||||
"proxy_enabled": False,
|
||||
"proxy_host": "",
|
||||
|
||||
@@ -46,7 +46,7 @@ export class AppCore {
|
||||
state.loadingManager = new LoadingManager();
|
||||
modalManager.initialize();
|
||||
updateService.initialize();
|
||||
bannerService.initialize();
|
||||
await bannerService.initialize();
|
||||
window.modalManager = modalManager;
|
||||
window.settingsManager = settingsManager;
|
||||
const exampleImagesManager = new ExampleImagesManager();
|
||||
@@ -81,8 +81,8 @@ export class AppCore {
|
||||
this.initialized = true;
|
||||
|
||||
// Start onboarding if needed (after everything is initialized)
|
||||
setTimeout(() => {
|
||||
onboardingManager.start();
|
||||
setTimeout(async () => {
|
||||
await onboardingManager.start();
|
||||
}, 1000); // Small delay to ensure all elements are rendered
|
||||
|
||||
// Return the core instance for chaining
|
||||
|
||||
@@ -36,7 +36,7 @@ class BannerService {
|
||||
/**
|
||||
* Initialize the banner service
|
||||
*/
|
||||
initialize() {
|
||||
async initialize() {
|
||||
if (this.initialized) return;
|
||||
|
||||
this.container = document.getElementById('banner-container');
|
||||
@@ -45,6 +45,9 @@ class BannerService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Load dismissed banners from backend first (for persistence across browser modes)
|
||||
await this.loadDismissedBannersFromBackend();
|
||||
|
||||
// Register default banners
|
||||
this.registerBanner('civitai-extension', {
|
||||
id: 'civitai-extension',
|
||||
@@ -76,10 +79,36 @@ class BannerService {
|
||||
|
||||
this.prepareCommunitySupportBanner();
|
||||
|
||||
this.showActiveBanners();
|
||||
await this.showActiveBanners();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load dismissed banners from backend settings
|
||||
* Falls back to localStorage if backend is unavailable
|
||||
*/
|
||||
async loadDismissedBannersFromBackend() {
|
||||
try {
|
||||
const response = await fetch('/api/lm/settings');
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings && Array.isArray(data.settings.dismissed_banners)) {
|
||||
// Merge backend dismissed banners with localStorage
|
||||
const backendDismissed = data.settings.dismissed_banners;
|
||||
const localDismissed = getStorageItem('dismissed_banners', []);
|
||||
|
||||
// Use Set to get unique banner IDs
|
||||
const mergedDismissed = [...new Set([...backendDismissed, ...localDismissed])];
|
||||
|
||||
// Save merged list to localStorage as cache
|
||||
if (mergedDismissed.length > 0) {
|
||||
setStorageItem('dismissed_banners', mergedDismissed);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.debug('Failed to fetch dismissed banners from backend, using localStorage');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a new banner
|
||||
* @param {string} id - Unique banner ID
|
||||
@@ -101,6 +130,7 @@ class BannerService {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
isBannerDismissed(bannerId) {
|
||||
// Check localStorage (which is synced with backend on load)
|
||||
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||
return dismissedBanners.includes(bannerId);
|
||||
}
|
||||
@@ -109,13 +139,16 @@ class BannerService {
|
||||
* Dismiss a banner
|
||||
* @param {string} bannerId - Banner ID
|
||||
*/
|
||||
dismissBanner(bannerId) {
|
||||
async dismissBanner(bannerId) {
|
||||
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||
let bannerAlreadyDismissed = dismissedBanners.includes(bannerId);
|
||||
|
||||
if (!bannerAlreadyDismissed) {
|
||||
dismissedBanners.push(bannerId);
|
||||
setStorageItem('dismissed_banners', dismissedBanners);
|
||||
|
||||
// Save to backend for persistence (survives incognito/private mode)
|
||||
await this.saveDismissedBannersToBackend(dismissedBanners);
|
||||
}
|
||||
|
||||
// Remove banner from DOM
|
||||
@@ -139,10 +172,26 @@ class BannerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save dismissed banners to backend settings
|
||||
* @param {string[]} dismissedBanners - Array of dismissed banner IDs
|
||||
*/
|
||||
async saveDismissedBannersToBackend(dismissedBanners) {
|
||||
try {
|
||||
await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dismissed_banners: dismissedBanners })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to save dismissed banners to backend:', e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show all active (non-dismissed) banners
|
||||
*/
|
||||
showActiveBanners() {
|
||||
async showActiveBanners() {
|
||||
if (!this.container) return;
|
||||
|
||||
const activeBanners = Array.from(this.banners.values())
|
||||
@@ -177,7 +226,7 @@ class BannerService {
|
||||
}).join('') : '';
|
||||
|
||||
const dismissButtonHtml = banner.dismissible ?
|
||||
`<button class="banner-dismiss" onclick="bannerService.dismissBanner('${banner.id}')" title="Dismiss">
|
||||
`<button class="banner-dismiss" onclick="bannerService.dismissBanner('${banner.id}').catch(console.error)" title="Dismiss">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>` : '';
|
||||
|
||||
@@ -227,8 +276,20 @@ class BannerService {
|
||||
/**
|
||||
* Clear all dismissed banners (for testing/admin purposes)
|
||||
*/
|
||||
clearDismissedBanners() {
|
||||
async clearDismissedBanners() {
|
||||
setStorageItem('dismissed_banners', []);
|
||||
|
||||
// Also clear on backend
|
||||
try {
|
||||
await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ dismissed_banners: [] })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to clear dismissed banners on backend:', e);
|
||||
}
|
||||
|
||||
location.reload();
|
||||
}
|
||||
|
||||
|
||||
@@ -82,7 +82,23 @@ export class OnboardingManager {
|
||||
}
|
||||
|
||||
// Check if user should see onboarding
|
||||
shouldShowOnboarding() {
|
||||
// First checks backend settings (persistent), falls back to localStorage
|
||||
async shouldShowOnboarding() {
|
||||
// Try to get state from backend first (persistent across browser modes)
|
||||
try {
|
||||
const response = await fetch('/api/lm/settings');
|
||||
const data = await response.json();
|
||||
if (data.success && data.settings && data.settings.onboarding_completed === true) {
|
||||
// Sync to localStorage as cache
|
||||
setStorageItem('onboarding_completed', true);
|
||||
return false;
|
||||
}
|
||||
} catch (e) {
|
||||
// Backend unavailable, fall back to localStorage
|
||||
console.debug('Failed to fetch onboarding state from backend, using localStorage');
|
||||
}
|
||||
|
||||
// Fallback to localStorage (for backward compatibility)
|
||||
const completed = getStorageItem('onboarding_completed');
|
||||
const skipped = getStorageItem('onboarding_skipped');
|
||||
return !completed && !skipped;
|
||||
@@ -90,7 +106,8 @@ export class OnboardingManager {
|
||||
|
||||
// Start the onboarding process
|
||||
async start() {
|
||||
if (!this.shouldShowOnboarding()) {
|
||||
const shouldShow = await this.shouldShowOnboarding();
|
||||
if (!shouldShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -159,9 +176,9 @@ export class OnboardingManager {
|
||||
});
|
||||
|
||||
// Handle skip button - skip entire tutorial
|
||||
document.getElementById('skipLanguageBtn').addEventListener('click', () => {
|
||||
document.getElementById('skipLanguageBtn').addEventListener('click', async () => {
|
||||
document.body.removeChild(modal);
|
||||
this.skip(); // Skip entire tutorial instead of just language selection
|
||||
await this.skip(); // Skip entire tutorial instead of just language selection
|
||||
resolve();
|
||||
});
|
||||
|
||||
@@ -205,11 +222,11 @@ export class OnboardingManager {
|
||||
}
|
||||
|
||||
// Start the tutorial steps
|
||||
startTutorial() {
|
||||
async startTutorial() {
|
||||
this.isActive = true;
|
||||
this.currentStep = 0;
|
||||
this.createOverlay();
|
||||
this.showStep(0);
|
||||
await this.showStep(0);
|
||||
}
|
||||
|
||||
// Create overlay elements
|
||||
@@ -231,9 +248,9 @@ export class OnboardingManager {
|
||||
}
|
||||
|
||||
// Show specific step
|
||||
showStep(stepIndex) {
|
||||
async showStep(stepIndex) {
|
||||
if (stepIndex >= this.steps.length) {
|
||||
this.complete();
|
||||
await this.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -242,7 +259,7 @@ export class OnboardingManager {
|
||||
|
||||
if (!target && step.target !== 'body') {
|
||||
// Skip this step if target not found
|
||||
this.showStep(stepIndex + 1);
|
||||
await this.showStep(stepIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -426,25 +443,48 @@ export class OnboardingManager {
|
||||
}
|
||||
|
||||
// Navigate to next step
|
||||
nextStep() {
|
||||
this.showStep(this.currentStep + 1);
|
||||
async nextStep() {
|
||||
await this.showStep(this.currentStep + 1);
|
||||
}
|
||||
|
||||
// Navigate to previous step
|
||||
previousStep() {
|
||||
async previousStep() {
|
||||
if (this.currentStep > 0) {
|
||||
this.showStep(this.currentStep - 1);
|
||||
await this.showStep(this.currentStep - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the tutorial
|
||||
skip() {
|
||||
async skip() {
|
||||
// Save to backend for persistence (survives incognito/private mode)
|
||||
try {
|
||||
await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ onboarding_completed: true })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to save onboarding state to backend:', e);
|
||||
}
|
||||
// Also save to localStorage as cache and for backward compatibility
|
||||
setStorageItem('onboarding_skipped', true);
|
||||
setStorageItem('onboarding_completed', true);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
// Complete the tutorial
|
||||
complete() {
|
||||
async complete() {
|
||||
// Save to backend for persistence (survives incognito/private mode)
|
||||
try {
|
||||
await fetch('/api/lm/settings', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ onboarding_completed: true })
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to save onboarding state to backend:', e);
|
||||
}
|
||||
// Also save to localStorage as cache and for backward compatibility
|
||||
setStorageItem('onboarding_completed', true);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user