mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(onboarding): implement onboarding tutorial with language selection and step guidance
This commit is contained in:
227
static/css/onboarding.css
Normal file
227
static/css/onboarding.css
Normal file
@@ -0,0 +1,227 @@
|
||||
/* Onboarding Tutorial Styles */
|
||||
.onboarding-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.8);
|
||||
z-index: var(--z-overlay);
|
||||
display: none;
|
||||
}
|
||||
|
||||
.onboarding-overlay.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.onboarding-spotlight {
|
||||
position: absolute;
|
||||
background: transparent;
|
||||
border: 3px solid var(--lora-accent);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.8);
|
||||
z-index: calc(var(--z-overlay) + 1);
|
||||
pointer-events: none;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.onboarding-popup {
|
||||
position: absolute;
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: var(--space-3);
|
||||
min-width: 320px;
|
||||
max-width: 400px;
|
||||
z-index: calc(var(--z-overlay) + 2);
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.onboarding-popup h3 {
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
color: var(--lora-accent);
|
||||
font-size: 1.2em;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.onboarding-popup p {
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
color: var(--text-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.onboarding-controls {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.onboarding-progress {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
font-size: 0.85em;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.onboarding-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.onboarding-btn {
|
||||
padding: var(--space-1) var(--space-2);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--card-bg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.onboarding-btn:hover {
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.onboarding-btn.primary {
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
border-color: var(--lora-accent);
|
||||
}
|
||||
|
||||
.onboarding-btn.primary:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
/* Language Selection Modal */
|
||||
.language-selection-modal {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.9);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: calc(var(--z-overlay) + 10);
|
||||
}
|
||||
|
||||
.language-selection-content {
|
||||
background: var(--lora-surface);
|
||||
border: 1px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-base);
|
||||
padding: var(--space-3);
|
||||
min-width: 400px;
|
||||
text-align: center;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.language-selection-content h2 {
|
||||
margin: 0 0 var(--space-2) 0;
|
||||
color: var(--lora-accent);
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.language-selection-content p {
|
||||
margin: 0 0 var(--space-3) 0;
|
||||
color: var(--text-color);
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.language-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-3);
|
||||
}
|
||||
|
||||
.language-option {
|
||||
padding: var(--space-2);
|
||||
border: 2px solid var(--lora-border);
|
||||
border-radius: var(--border-radius-sm);
|
||||
background: var(--card-bg);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
|
||||
.language-option:hover {
|
||||
border-color: var(--lora-accent);
|
||||
background: var(--lora-surface);
|
||||
}
|
||||
|
||||
.language-option.selected {
|
||||
border-color: var(--lora-accent);
|
||||
background: var(--lora-accent);
|
||||
color: var(--lora-text);
|
||||
}
|
||||
|
||||
.language-flag {
|
||||
font-size: 1.5em;
|
||||
}
|
||||
|
||||
.language-name {
|
||||
font-size: 0.9em;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.language-actions {
|
||||
display: flex;
|
||||
gap: var(--space-2);
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Shortcut Key Highlighting */
|
||||
.onboarding-shortcut {
|
||||
display: inline-block;
|
||||
background: var(--shortcut-bg);
|
||||
border: 1px solid var(--shortcut-border);
|
||||
border-radius: var(--border-radius-xs);
|
||||
padding: 2px 6px;
|
||||
font-size: 0.8em;
|
||||
font-weight: 600;
|
||||
color: var(--shortcut-text);
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
/* Animation for highlighting elements */
|
||||
.onboarding-highlight {
|
||||
animation: onboarding-pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes onboarding-pulse {
|
||||
0%, 100% {
|
||||
box-shadow: 0 0 0 0 var(--lora-accent);
|
||||
}
|
||||
50% {
|
||||
box-shadow: 0 0 0 8px transparent;
|
||||
}
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.onboarding-popup {
|
||||
min-width: 280px;
|
||||
max-width: calc(100vw - 40px);
|
||||
padding: var(--space-2);
|
||||
}
|
||||
|
||||
.language-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.language-selection-content {
|
||||
min-width: calc(100vw - 40px);
|
||||
max-width: 400px;
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { initTheme, initBackToTop } from './utils/uiHelpers.js';
|
||||
import { initializeInfiniteScroll } from './utils/infiniteScroll.js';
|
||||
import { migrateStorageItems } from './utils/storageHelpers.js';
|
||||
import { i18n } from './i18n/index.js';
|
||||
import { onboardingManager } from './managers/OnboardingManager.js';
|
||||
|
||||
// Core application class
|
||||
export class AppCore {
|
||||
@@ -65,6 +66,11 @@ export class AppCore {
|
||||
// Mark as initialized
|
||||
this.initialized = true;
|
||||
|
||||
// Start onboarding if needed (after everything is initialized)
|
||||
setTimeout(() => {
|
||||
onboardingManager.start();
|
||||
}, 1000); // Small delay to ensure all elements are rendered
|
||||
|
||||
// Return the core instance for chaining
|
||||
return this;
|
||||
}
|
||||
|
||||
393
static/js/managers/OnboardingManager.js
Normal file
393
static/js/managers/OnboardingManager.js
Normal file
@@ -0,0 +1,393 @@
|
||||
import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js';
|
||||
import { state } from '../state/index.js';
|
||||
|
||||
export class OnboardingManager {
|
||||
constructor() {
|
||||
this.isActive = false;
|
||||
this.currentStep = 0;
|
||||
this.selectedLanguage = 'en';
|
||||
this.overlay = null;
|
||||
this.spotlight = null;
|
||||
this.popup = null;
|
||||
|
||||
// Available languages with flag emojis
|
||||
this.languages = [
|
||||
{ code: 'en', name: 'English', flag: '🇺🇸' },
|
||||
{ code: 'zh-cn', name: '简体中文', flag: '🇨🇳' },
|
||||
{ code: 'zh-tw', name: '繁體中文', flag: '🇹🇼' },
|
||||
{ code: 'ja', name: '日本語', flag: '🇯🇵' },
|
||||
{ code: 'ko', name: '한국어', flag: '🇰🇷' },
|
||||
{ code: 'es', name: 'Español', flag: '🇪🇸' },
|
||||
{ code: 'fr', name: 'Français', flag: '🇫🇷' },
|
||||
{ code: 'de', name: 'Deutsch', flag: '🇩🇪' },
|
||||
{ code: 'ru', name: 'Русский', flag: '🇷🇺' }
|
||||
];
|
||||
|
||||
// Tutorial steps configuration
|
||||
this.steps = [
|
||||
{
|
||||
target: '.controls .action-buttons [data-action="fetch"]',
|
||||
title: 'Fetch Models Metadata',
|
||||
content: 'Click the <strong>Fetch</strong> button to download model metadata and preview images from Civitai. This enriches your local models with detailed information.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '.controls .action-buttons [data-action="download"]',
|
||||
title: 'Download New Models',
|
||||
content: 'Use the <strong>Download</strong> button to download models directly from Civitai URLs. Simply paste a model URL and choose your download location.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '.controls .action-buttons [data-action="bulk"]',
|
||||
title: 'Bulk Operations',
|
||||
content: 'Enter bulk mode by clicking this button or pressing <span class="onboarding-shortcut">B</span>. Select multiple models and perform batch operations. Use <span class="onboarding-shortcut">Ctrl+A</span> to select all visible models.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '#searchInput',
|
||||
title: 'Search Your Models',
|
||||
content: 'Use the search bar to quickly find models by filename, model name, tags, or creator. Type your search terms here.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '#searchOptionsToggle',
|
||||
title: 'Search Options',
|
||||
content: 'Click this button to configure what fields to search in: filename, model name, tags, or creator name. Customize your search scope.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '#filterButton',
|
||||
title: 'Filter Models',
|
||||
content: 'Use filters to narrow down models by base model type (SD1.5, SDXL, Flux, etc.) or by specific tags. Great for organizing large collections.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: 'body',
|
||||
title: 'Folder Navigation',
|
||||
content: 'Move your mouse to the <strong>left edge</strong> of the window to reveal the folder sidebar. You can pin it for permanent access and navigate through your model directories.',
|
||||
position: 'center',
|
||||
customPosition: { top: '20%', left: '50%' }
|
||||
},
|
||||
{
|
||||
target: '#breadcrumbContainer',
|
||||
title: 'Breadcrumb Navigation',
|
||||
content: 'The breadcrumb navigation shows your current path and allows quick navigation between folders. Click any folder name to jump directly there.',
|
||||
position: 'bottom'
|
||||
},
|
||||
{
|
||||
target: '.card-grid',
|
||||
title: 'Model Cards',
|
||||
content: '<strong>Single-click</strong> a model card to view detailed information and edit metadata. Look for the pencil icon when hovering over editable fields.',
|
||||
position: 'top',
|
||||
customPosition: { top: '10%', left: '50%' }
|
||||
},
|
||||
{
|
||||
target: '.card-grid',
|
||||
title: 'Context Menu & Quick Actions',
|
||||
content: '<strong>Right-click</strong> any model card for a context menu with additional actions. Click the <strong>airplane icon</strong> to send to ComfyUI workflow (hold <span class="onboarding-shortcut">Shift</span> to replace existing content).',
|
||||
position: 'top',
|
||||
customPosition: { top: '10%', left: '50%' }
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
// Check if user should see onboarding
|
||||
shouldShowOnboarding() {
|
||||
const completed = getStorageItem('onboarding_completed');
|
||||
const skipped = getStorageItem('onboarding_skipped');
|
||||
return !completed && !skipped;
|
||||
}
|
||||
|
||||
// Start the onboarding process
|
||||
async start() {
|
||||
if (!this.shouldShowOnboarding()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Show language selection first
|
||||
await this.showLanguageSelection();
|
||||
}
|
||||
|
||||
// Show language selection modal
|
||||
showLanguageSelection() {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'language-selection-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="language-selection-content">
|
||||
<h2>Welcome to LoRA Manager</h2>
|
||||
<p>Choose your preferred language to get started, or continue with English.</p>
|
||||
<div class="language-grid">
|
||||
${this.languages.map(lang => `
|
||||
<div class="language-option" data-language="${lang.code}">
|
||||
<span class="language-flag">${lang.flag}</span>
|
||||
<span class="language-name">${lang.name}</span>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="language-actions">
|
||||
<button class="onboarding-btn" id="skipLanguageBtn">Skip</button>
|
||||
<button class="onboarding-btn primary" id="continueLanguageBtn">Continue</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.body.appendChild(modal);
|
||||
|
||||
// Handle language selection
|
||||
modal.querySelectorAll('.language-option').forEach(option => {
|
||||
option.addEventListener('click', () => {
|
||||
modal.querySelectorAll('.language-option').forEach(opt => opt.classList.remove('selected'));
|
||||
option.classList.add('selected');
|
||||
this.selectedLanguage = option.dataset.language;
|
||||
});
|
||||
});
|
||||
|
||||
// Handle continue button
|
||||
document.getElementById('continueLanguageBtn').addEventListener('click', async () => {
|
||||
if (this.selectedLanguage !== 'en') {
|
||||
// Save language and reload page
|
||||
await this.changeLanguage(this.selectedLanguage);
|
||||
}
|
||||
document.body.removeChild(modal);
|
||||
this.startTutorial();
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Handle skip button
|
||||
document.getElementById('skipLanguageBtn').addEventListener('click', () => {
|
||||
document.body.removeChild(modal);
|
||||
this.startTutorial();
|
||||
resolve();
|
||||
});
|
||||
|
||||
// Select English by default
|
||||
modal.querySelector('[data-language="en"]').classList.add('selected');
|
||||
});
|
||||
}
|
||||
|
||||
// Change language using existing settings manager
|
||||
async changeLanguage(languageCode) {
|
||||
try {
|
||||
// Update state
|
||||
state.global.settings.language = languageCode;
|
||||
|
||||
// Save to localStorage
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
// Save to backend
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
language: languageCode
|
||||
})
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
// Mark onboarding as started before reload
|
||||
setStorageItem('onboarding_language_set', true);
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to change language:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Start the tutorial steps
|
||||
startTutorial() {
|
||||
this.isActive = true;
|
||||
this.currentStep = 0;
|
||||
this.createOverlay();
|
||||
this.showStep(0);
|
||||
}
|
||||
|
||||
// Create overlay elements
|
||||
createOverlay() {
|
||||
// Create overlay
|
||||
this.overlay = document.createElement('div');
|
||||
this.overlay.className = 'onboarding-overlay active';
|
||||
document.body.appendChild(this.overlay);
|
||||
|
||||
// Create spotlight
|
||||
this.spotlight = document.createElement('div');
|
||||
this.spotlight.className = 'onboarding-spotlight';
|
||||
document.body.appendChild(this.spotlight);
|
||||
|
||||
// Create popup
|
||||
this.popup = document.createElement('div');
|
||||
this.popup.className = 'onboarding-popup';
|
||||
document.body.appendChild(this.popup);
|
||||
|
||||
// Handle clicks outside popup
|
||||
this.overlay.addEventListener('click', (e) => {
|
||||
if (e.target === this.overlay) {
|
||||
this.skip();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Show specific step
|
||||
showStep(stepIndex) {
|
||||
if (stepIndex >= this.steps.length) {
|
||||
this.complete();
|
||||
return;
|
||||
}
|
||||
|
||||
const step = this.steps[stepIndex];
|
||||
const target = document.querySelector(step.target);
|
||||
|
||||
if (!target && step.target !== 'body') {
|
||||
// Skip this step if target not found
|
||||
this.showStep(stepIndex + 1);
|
||||
return;
|
||||
}
|
||||
|
||||
// Position spotlight
|
||||
if (target && step.target !== 'body') {
|
||||
const rect = target.getBoundingClientRect();
|
||||
this.spotlight.style.left = `${rect.left - 5}px`;
|
||||
this.spotlight.style.top = `${rect.top - 5}px`;
|
||||
this.spotlight.style.width = `${rect.width + 10}px`;
|
||||
this.spotlight.style.height = `${rect.height + 10}px`;
|
||||
this.spotlight.style.display = 'block';
|
||||
} else {
|
||||
this.spotlight.style.display = 'none';
|
||||
}
|
||||
|
||||
// Update popup content
|
||||
this.popup.innerHTML = `
|
||||
<h3>${step.title}</h3>
|
||||
<p>${step.content}</p>
|
||||
<div class="onboarding-controls">
|
||||
<div class="onboarding-progress">
|
||||
<span>${stepIndex + 1} / ${this.steps.length}</span>
|
||||
</div>
|
||||
<div class="onboarding-actions">
|
||||
<button class="onboarding-btn" onclick="onboardingManager.skip()">Skip Tutorial</button>
|
||||
${stepIndex > 0 ? '<button class="onboarding-btn" onclick="onboardingManager.previousStep()">Back</button>' : ''}
|
||||
<button class="onboarding-btn primary" onclick="onboardingManager.nextStep()">
|
||||
${stepIndex === this.steps.length - 1 ? 'Finish' : 'Next'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Position popup
|
||||
this.positionPopup(step, target);
|
||||
|
||||
this.currentStep = stepIndex;
|
||||
}
|
||||
|
||||
// Position popup relative to target
|
||||
positionPopup(step, target) {
|
||||
const popup = this.popup;
|
||||
const windowWidth = window.innerWidth;
|
||||
const windowHeight = window.innerHeight;
|
||||
|
||||
if (step.customPosition) {
|
||||
popup.style.left = step.customPosition.left;
|
||||
popup.style.top = step.customPosition.top;
|
||||
popup.style.transform = 'translate(-50%, 0)';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!target || step.target === 'body') {
|
||||
popup.style.left = '50%';
|
||||
popup.style.top = '50%';
|
||||
popup.style.transform = 'translate(-50%, -50%)';
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = target.getBoundingClientRect();
|
||||
const popupRect = popup.getBoundingClientRect();
|
||||
|
||||
let left, top;
|
||||
|
||||
switch (step.position) {
|
||||
case 'bottom':
|
||||
left = rect.left + (rect.width / 2) - (popupRect.width / 2);
|
||||
top = rect.bottom + 20;
|
||||
break;
|
||||
case 'top':
|
||||
left = rect.left + (rect.width / 2) - (popupRect.width / 2);
|
||||
top = rect.top - popupRect.height - 20;
|
||||
break;
|
||||
case 'right':
|
||||
left = rect.right + 20;
|
||||
top = rect.top + (rect.height / 2) - (popupRect.height / 2);
|
||||
break;
|
||||
case 'left':
|
||||
left = rect.left - popupRect.width - 20;
|
||||
top = rect.top + (rect.height / 2) - (popupRect.height / 2);
|
||||
break;
|
||||
default:
|
||||
left = rect.left + (rect.width / 2) - (popupRect.width / 2);
|
||||
top = rect.bottom + 20;
|
||||
}
|
||||
|
||||
// Ensure popup stays within viewport
|
||||
left = Math.max(20, Math.min(left, windowWidth - popupRect.width - 20));
|
||||
top = Math.max(20, Math.min(top, windowHeight - popupRect.height - 20));
|
||||
|
||||
popup.style.left = `${left}px`;
|
||||
popup.style.top = `${top}px`;
|
||||
popup.style.transform = 'none';
|
||||
}
|
||||
|
||||
// Navigate to next step
|
||||
nextStep() {
|
||||
this.showStep(this.currentStep + 1);
|
||||
}
|
||||
|
||||
// Navigate to previous step
|
||||
previousStep() {
|
||||
if (this.currentStep > 0) {
|
||||
this.showStep(this.currentStep - 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the tutorial
|
||||
skip() {
|
||||
setStorageItem('onboarding_skipped', true);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
// Complete the tutorial
|
||||
complete() {
|
||||
setStorageItem('onboarding_completed', true);
|
||||
this.cleanup();
|
||||
}
|
||||
|
||||
// Clean up overlay elements
|
||||
cleanup() {
|
||||
if (this.overlay) {
|
||||
document.body.removeChild(this.overlay);
|
||||
this.overlay = null;
|
||||
}
|
||||
if (this.spotlight) {
|
||||
document.body.removeChild(this.spotlight);
|
||||
this.spotlight = null;
|
||||
}
|
||||
if (this.popup) {
|
||||
document.body.removeChild(this.popup);
|
||||
this.popup = null;
|
||||
}
|
||||
this.isActive = false;
|
||||
}
|
||||
|
||||
// Reset onboarding status (for testing)
|
||||
reset() {
|
||||
localStorage.removeItem('lora_manager_onboarding_completed');
|
||||
localStorage.removeItem('lora_manager_onboarding_skipped');
|
||||
localStorage.removeItem('lora_manager_onboarding_language_set');
|
||||
}
|
||||
}
|
||||
|
||||
// Create singleton instance
|
||||
export const onboardingManager = new OnboardingManager();
|
||||
|
||||
// Make it globally available for button handlers
|
||||
window.onboardingManager = onboardingManager;
|
||||
@@ -971,7 +971,7 @@ export class SettingsManager {
|
||||
// Save to localStorage
|
||||
setStorageItem('settings', state.global.settings);
|
||||
|
||||
// 保存到后端
|
||||
// Save to backend
|
||||
const response = await fetch('/api/settings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
<title>{% block title %}{{ t('header.appTitle') }}{% endblock %}</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="stylesheet" href="/loras_static/css/style.css">
|
||||
<link rel="stylesheet" href="/loras_static/css/onboarding.css">
|
||||
{% block page_css %}{% endblock %}
|
||||
<link rel="stylesheet" href="/loras_static/vendor/font-awesome/css/all.min.css"
|
||||
crossorigin="anonymous" referrerpolicy="no-referrer">
|
||||
|
||||
Reference in New Issue
Block a user