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",
|
"enable_metadata_archive_db",
|
||||||
"language",
|
"language",
|
||||||
"use_portable_settings",
|
"use_portable_settings",
|
||||||
|
"onboarding_completed",
|
||||||
|
"dismissed_banners",
|
||||||
"proxy_enabled",
|
"proxy_enabled",
|
||||||
"proxy_type",
|
"proxy_type",
|
||||||
"proxy_host",
|
"proxy_host",
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ DEFAULT_SETTINGS: Dict[str, Any] = {
|
|||||||
"hash_chunk_size_mb": DEFAULT_HASH_CHUNK_SIZE_MB,
|
"hash_chunk_size_mb": DEFAULT_HASH_CHUNK_SIZE_MB,
|
||||||
"language": "en",
|
"language": "en",
|
||||||
"show_only_sfw": False,
|
"show_only_sfw": False,
|
||||||
|
"onboarding_completed": False,
|
||||||
|
"dismissed_banners": [],
|
||||||
"enable_metadata_archive_db": False,
|
"enable_metadata_archive_db": False,
|
||||||
"proxy_enabled": False,
|
"proxy_enabled": False,
|
||||||
"proxy_host": "",
|
"proxy_host": "",
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export class AppCore {
|
|||||||
state.loadingManager = new LoadingManager();
|
state.loadingManager = new LoadingManager();
|
||||||
modalManager.initialize();
|
modalManager.initialize();
|
||||||
updateService.initialize();
|
updateService.initialize();
|
||||||
bannerService.initialize();
|
await bannerService.initialize();
|
||||||
window.modalManager = modalManager;
|
window.modalManager = modalManager;
|
||||||
window.settingsManager = settingsManager;
|
window.settingsManager = settingsManager;
|
||||||
const exampleImagesManager = new ExampleImagesManager();
|
const exampleImagesManager = new ExampleImagesManager();
|
||||||
@@ -81,8 +81,8 @@ export class AppCore {
|
|||||||
this.initialized = true;
|
this.initialized = true;
|
||||||
|
|
||||||
// Start onboarding if needed (after everything is initialized)
|
// Start onboarding if needed (after everything is initialized)
|
||||||
setTimeout(() => {
|
setTimeout(async () => {
|
||||||
onboardingManager.start();
|
await onboardingManager.start();
|
||||||
}, 1000); // Small delay to ensure all elements are rendered
|
}, 1000); // Small delay to ensure all elements are rendered
|
||||||
|
|
||||||
// Return the core instance for chaining
|
// Return the core instance for chaining
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class BannerService {
|
|||||||
/**
|
/**
|
||||||
* Initialize the banner service
|
* Initialize the banner service
|
||||||
*/
|
*/
|
||||||
initialize() {
|
async initialize() {
|
||||||
if (this.initialized) return;
|
if (this.initialized) return;
|
||||||
|
|
||||||
this.container = document.getElementById('banner-container');
|
this.container = document.getElementById('banner-container');
|
||||||
@@ -45,6 +45,9 @@ class BannerService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load dismissed banners from backend first (for persistence across browser modes)
|
||||||
|
await this.loadDismissedBannersFromBackend();
|
||||||
|
|
||||||
// Register default banners
|
// Register default banners
|
||||||
this.registerBanner('civitai-extension', {
|
this.registerBanner('civitai-extension', {
|
||||||
id: 'civitai-extension',
|
id: 'civitai-extension',
|
||||||
@@ -76,10 +79,36 @@ class BannerService {
|
|||||||
|
|
||||||
this.prepareCommunitySupportBanner();
|
this.prepareCommunitySupportBanner();
|
||||||
|
|
||||||
this.showActiveBanners();
|
await this.showActiveBanners();
|
||||||
this.initialized = true;
|
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
|
* Register a new banner
|
||||||
* @param {string} id - Unique banner ID
|
* @param {string} id - Unique banner ID
|
||||||
@@ -101,6 +130,7 @@ class BannerService {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
isBannerDismissed(bannerId) {
|
isBannerDismissed(bannerId) {
|
||||||
|
// Check localStorage (which is synced with backend on load)
|
||||||
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||||
return dismissedBanners.includes(bannerId);
|
return dismissedBanners.includes(bannerId);
|
||||||
}
|
}
|
||||||
@@ -109,13 +139,16 @@ class BannerService {
|
|||||||
* Dismiss a banner
|
* Dismiss a banner
|
||||||
* @param {string} bannerId - Banner ID
|
* @param {string} bannerId - Banner ID
|
||||||
*/
|
*/
|
||||||
dismissBanner(bannerId) {
|
async dismissBanner(bannerId) {
|
||||||
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
const dismissedBanners = getStorageItem('dismissed_banners', []);
|
||||||
let bannerAlreadyDismissed = dismissedBanners.includes(bannerId);
|
let bannerAlreadyDismissed = dismissedBanners.includes(bannerId);
|
||||||
|
|
||||||
if (!bannerAlreadyDismissed) {
|
if (!bannerAlreadyDismissed) {
|
||||||
dismissedBanners.push(bannerId);
|
dismissedBanners.push(bannerId);
|
||||||
setStorageItem('dismissed_banners', dismissedBanners);
|
setStorageItem('dismissed_banners', dismissedBanners);
|
||||||
|
|
||||||
|
// Save to backend for persistence (survives incognito/private mode)
|
||||||
|
await this.saveDismissedBannersToBackend(dismissedBanners);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove banner from DOM
|
// 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
|
* Show all active (non-dismissed) banners
|
||||||
*/
|
*/
|
||||||
showActiveBanners() {
|
async showActiveBanners() {
|
||||||
if (!this.container) return;
|
if (!this.container) return;
|
||||||
|
|
||||||
const activeBanners = Array.from(this.banners.values())
|
const activeBanners = Array.from(this.banners.values())
|
||||||
@@ -177,7 +226,7 @@ class BannerService {
|
|||||||
}).join('') : '';
|
}).join('') : '';
|
||||||
|
|
||||||
const dismissButtonHtml = banner.dismissible ?
|
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>
|
<i class="fas fa-times"></i>
|
||||||
</button>` : '';
|
</button>` : '';
|
||||||
|
|
||||||
@@ -227,8 +276,20 @@ class BannerService {
|
|||||||
/**
|
/**
|
||||||
* Clear all dismissed banners (for testing/admin purposes)
|
* Clear all dismissed banners (for testing/admin purposes)
|
||||||
*/
|
*/
|
||||||
clearDismissedBanners() {
|
async clearDismissedBanners() {
|
||||||
setStorageItem('dismissed_banners', []);
|
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();
|
location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,7 +82,23 @@ export class OnboardingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if user should see onboarding
|
// 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 completed = getStorageItem('onboarding_completed');
|
||||||
const skipped = getStorageItem('onboarding_skipped');
|
const skipped = getStorageItem('onboarding_skipped');
|
||||||
return !completed && !skipped;
|
return !completed && !skipped;
|
||||||
@@ -90,7 +106,8 @@ export class OnboardingManager {
|
|||||||
|
|
||||||
// Start the onboarding process
|
// Start the onboarding process
|
||||||
async start() {
|
async start() {
|
||||||
if (!this.shouldShowOnboarding()) {
|
const shouldShow = await this.shouldShowOnboarding();
|
||||||
|
if (!shouldShow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,9 +176,9 @@ export class OnboardingManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Handle skip button - skip entire tutorial
|
// Handle skip button - skip entire tutorial
|
||||||
document.getElementById('skipLanguageBtn').addEventListener('click', () => {
|
document.getElementById('skipLanguageBtn').addEventListener('click', async () => {
|
||||||
document.body.removeChild(modal);
|
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();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -205,11 +222,11 @@ export class OnboardingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Start the tutorial steps
|
// Start the tutorial steps
|
||||||
startTutorial() {
|
async startTutorial() {
|
||||||
this.isActive = true;
|
this.isActive = true;
|
||||||
this.currentStep = 0;
|
this.currentStep = 0;
|
||||||
this.createOverlay();
|
this.createOverlay();
|
||||||
this.showStep(0);
|
await this.showStep(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create overlay elements
|
// Create overlay elements
|
||||||
@@ -231,9 +248,9 @@ export class OnboardingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Show specific step
|
// Show specific step
|
||||||
showStep(stepIndex) {
|
async showStep(stepIndex) {
|
||||||
if (stepIndex >= this.steps.length) {
|
if (stepIndex >= this.steps.length) {
|
||||||
this.complete();
|
await this.complete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -242,7 +259,7 @@ export class OnboardingManager {
|
|||||||
|
|
||||||
if (!target && step.target !== 'body') {
|
if (!target && step.target !== 'body') {
|
||||||
// Skip this step if target not found
|
// Skip this step if target not found
|
||||||
this.showStep(stepIndex + 1);
|
await this.showStep(stepIndex + 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -426,25 +443,48 @@ export class OnboardingManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to next step
|
// Navigate to next step
|
||||||
nextStep() {
|
async nextStep() {
|
||||||
this.showStep(this.currentStep + 1);
|
await this.showStep(this.currentStep + 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Navigate to previous step
|
// Navigate to previous step
|
||||||
previousStep() {
|
async previousStep() {
|
||||||
if (this.currentStep > 0) {
|
if (this.currentStep > 0) {
|
||||||
this.showStep(this.currentStep - 1);
|
await this.showStep(this.currentStep - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Skip the tutorial
|
// 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_skipped', true);
|
||||||
|
setStorageItem('onboarding_completed', true);
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Complete the tutorial
|
// 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);
|
setStorageItem('onboarding_completed', true);
|
||||||
this.cleanup();
|
this.cleanup();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user