diff --git a/static/css/onboarding.css b/static/css/onboarding.css new file mode 100644 index 00000000..6dc17fbc --- /dev/null +++ b/static/css/onboarding.css @@ -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; + } +} diff --git a/static/js/core.js b/static/js/core.js index eba47200..36376d89 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -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; } diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js new file mode 100644 index 00000000..8f3eca5b --- /dev/null +++ b/static/js/managers/OnboardingManager.js @@ -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 Fetch 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 Download 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 B. Select multiple models and perform batch operations. Use Ctrl+A 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 left edge 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: 'Single-click 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: 'Right-click any model card for a context menu with additional actions. Click the airplane icon to send to ComfyUI workflow (hold Shift 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 = ` +
Choose your preferred language to get started, or continue with English.
+${step.content}
+