From 2bcf341f047390301bbeef1697f7498f4f847e1b Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 15:42:36 +0800 Subject: [PATCH 01/10] feat(onboarding): implement onboarding tutorial with language selection and step guidance --- static/css/onboarding.css | 227 ++++++++++++++ static/js/core.js | 6 + static/js/managers/OnboardingManager.js | 393 ++++++++++++++++++++++++ static/js/managers/SettingsManager.js | 2 +- templates/base.html | 1 + 5 files changed, 628 insertions(+), 1 deletion(-) create mode 100644 static/css/onboarding.css create mode 100644 static/js/managers/OnboardingManager.js 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 = ` +
+

Welcome to LoRA Manager

+

Choose your preferred language to get started, or continue with English.

+
+ ${this.languages.map(lang => ` +
+ ${lang.flag} + ${lang.name} +
+ `).join('')} +
+
+ + +
+
+ `; + + 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 = ` +

${step.title}

+

${step.content}

+
+
+ ${stepIndex + 1} / ${this.steps.length} +
+
+ + ${stepIndex > 0 ? '' : ''} + +
+
+ `; + + // 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; diff --git a/static/js/managers/SettingsManager.js b/static/js/managers/SettingsManager.js index 033cfd7e..b31613e2 100644 --- a/static/js/managers/SettingsManager.js +++ b/static/js/managers/SettingsManager.js @@ -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: { diff --git a/templates/base.html b/templates/base.html index bf8600d4..ece094a8 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,6 +5,7 @@ {% block title %}{{ t('header.appTitle') }}{% endblock %} + {% block page_css %}{% endblock %} From 31b032429dfe09a9d00c3989ec6dd2f6ce8e861d Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 15:46:33 +0800 Subject: [PATCH 02/10] fix(sidebar): change default pinned state to true for sidebar restoration --- static/js/components/SidebarManager.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/js/components/SidebarManager.js b/static/js/components/SidebarManager.js index 637959ab..674fe825 100644 --- a/static/js/components/SidebarManager.js +++ b/static/js/components/SidebarManager.js @@ -173,7 +173,7 @@ export class SidebarManager { if (!sidebar || !hoverArea) return; // Get stored pin state - const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false); + const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, true); this.isPinned = isPinned; // Sidebar starts hidden by default (CSS handles this) @@ -922,7 +922,7 @@ export class SidebarManager { } restoreSidebarState() { - const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, false); + const isPinned = getStorageItem(`${this.pageType}_sidebarPinned`, true); const expandedPaths = getStorageItem(`${this.pageType}_expandedNodes`, []); const displayMode = getStorageItem(`${this.pageType}_displayMode`, 'tree'); // 'tree' or 'list', default to 'tree' From 903a8050b3d89647850eda5244d296c1cf28b48f Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 18:19:34 +0800 Subject: [PATCH 03/10] Add SVG flags for France, Hong Kong, Japan, South Korea, Russia, and the United States - Added France flag (fr.svg) with three vertical stripes: blue, white, and red. - Added Hong Kong flag (hk.svg) featuring a red background with a white flower emblem. - Added Japan flag (jp.svg) with a white field and a red circle in the center. - Added South Korea flag (kr.svg) showcasing a white background with a central yin-yang symbol and four black trigrams. - Added Russia flag (ru.svg) with three horizontal stripes: white, blue, and red. - Added United States flag (us.svg) with red and white stripes and a blue canton featuring stars. --- static/js/managers/OnboardingManager.js | 51 +- static/vendor/flag-icons/flag-icons.min.css | 1 + static/vendor/flags/4x3/cn.svg | 11 + static/vendor/flags/4x3/de.svg | 5 + static/vendor/flags/4x3/es.svg | 544 ++++++++++++++++++++ static/vendor/flags/4x3/fr.svg | 5 + static/vendor/flags/4x3/hk.svg | 8 + static/vendor/flags/4x3/jp.svg | 11 + static/vendor/flags/4x3/kr.svg | 24 + static/vendor/flags/4x3/ru.svg | 5 + static/vendor/flags/4x3/us.svg | 9 + templates/base.html | 1 + 12 files changed, 644 insertions(+), 31 deletions(-) create mode 100644 static/vendor/flag-icons/flag-icons.min.css create mode 100644 static/vendor/flags/4x3/cn.svg create mode 100644 static/vendor/flags/4x3/de.svg create mode 100644 static/vendor/flags/4x3/es.svg create mode 100644 static/vendor/flags/4x3/fr.svg create mode 100644 static/vendor/flags/4x3/hk.svg create mode 100644 static/vendor/flags/4x3/jp.svg create mode 100644 static/vendor/flags/4x3/kr.svg create mode 100644 static/vendor/flags/4x3/ru.svg create mode 100644 static/vendor/flags/4x3/us.svg diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index 8f3eca5b..49d230e8 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -10,17 +10,17 @@ export class OnboardingManager { this.spotlight = null; this.popup = null; - // Available languages with flag emojis + // Available languages with SVG flags (using flag-icons) 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: '🇷🇺' } + { code: 'en', name: 'English', flag: 'us' }, + { code: 'zh-cn', name: '简体中文', flag: 'cn' }, + { code: 'zh-tw', name: '繁體中文', flag: 'hk' }, + { code: 'ja', name: '日本語', flag: 'jp' }, + { code: 'ko', name: '한국어', flag: 'kr' }, + { code: 'es', name: 'Español', flag: 'es' }, + { code: 'fr', name: 'Français', flag: 'fr' }, + { code: 'de', name: 'Deutsch', flag: 'de' }, + { code: 'ru', name: 'Русский', flag: 'ru' } ]; // Tutorial steps configuration @@ -28,13 +28,13 @@ export class OnboardingManager { { 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.', + content: 'Click the Fetch button to download model metadata and preview images from Civitai.', 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.', + content: 'Use the Download button to download models directly from Civitai URLs.', position: 'bottom' }, { @@ -43,12 +43,6 @@ export class OnboardingManager { 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', @@ -58,16 +52,9 @@ export class OnboardingManager { { 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.', + content: 'Use filters to narrow down models by base model type (SD1.5, SDXL, Flux, etc.) or by specific tags.', 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', @@ -79,14 +66,14 @@ export class OnboardingManager { 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%' } + customPosition: { top: '20%', 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).', + title: 'Context Menu', + content: 'Right-click any model card for a context menu with additional actions.', position: 'top', - customPosition: { top: '10%', left: '50%' } + customPosition: { top: '20%', left: '50%' } } ]; } @@ -120,7 +107,9 @@ export class OnboardingManager {
${this.languages.map(lang => `
- ${lang.flag} + + + ${lang.name}
`).join('')} diff --git a/static/vendor/flag-icons/flag-icons.min.css b/static/vendor/flag-icons/flag-icons.min.css new file mode 100644 index 00000000..253a649d --- /dev/null +++ b/static/vendor/flag-icons/flag-icons.min.css @@ -0,0 +1 @@ +.fib{background-size:contain;background-position:50%;background-repeat:no-repeat}.fi{background-size:contain;background-position:50%;background-repeat:no-repeat;position:relative;display:inline-block;width:1.33333333em;line-height:1em}.fi:before{content:'\00a0'}.fi.fis{width:1em}.fi-xx{background-image:url(../flags/4x3/xx.svg)}.fi-xx.fis{background-image:url(../flags/1x1/xx.svg)}.fi-ad{background-image:url(../flags/4x3/ad.svg)}.fi-ad.fis{background-image:url(../flags/1x1/ad.svg)}.fi-ae{background-image:url(../flags/4x3/ae.svg)}.fi-ae.fis{background-image:url(../flags/1x1/ae.svg)}.fi-af{background-image:url(../flags/4x3/af.svg)}.fi-af.fis{background-image:url(../flags/1x1/af.svg)}.fi-ag{background-image:url(../flags/4x3/ag.svg)}.fi-ag.fis{background-image:url(../flags/1x1/ag.svg)}.fi-ai{background-image:url(../flags/4x3/ai.svg)}.fi-ai.fis{background-image:url(../flags/1x1/ai.svg)}.fi-al{background-image:url(../flags/4x3/al.svg)}.fi-al.fis{background-image:url(../flags/1x1/al.svg)}.fi-am{background-image:url(../flags/4x3/am.svg)}.fi-am.fis{background-image:url(../flags/1x1/am.svg)}.fi-ao{background-image:url(../flags/4x3/ao.svg)}.fi-ao.fis{background-image:url(../flags/1x1/ao.svg)}.fi-aq{background-image:url(../flags/4x3/aq.svg)}.fi-aq.fis{background-image:url(../flags/1x1/aq.svg)}.fi-ar{background-image:url(../flags/4x3/ar.svg)}.fi-ar.fis{background-image:url(../flags/1x1/ar.svg)}.fi-as{background-image:url(../flags/4x3/as.svg)}.fi-as.fis{background-image:url(../flags/1x1/as.svg)}.fi-at{background-image:url(../flags/4x3/at.svg)}.fi-at.fis{background-image:url(../flags/1x1/at.svg)}.fi-au{background-image:url(../flags/4x3/au.svg)}.fi-au.fis{background-image:url(../flags/1x1/au.svg)}.fi-aw{background-image:url(../flags/4x3/aw.svg)}.fi-aw.fis{background-image:url(../flags/1x1/aw.svg)}.fi-ax{background-image:url(../flags/4x3/ax.svg)}.fi-ax.fis{background-image:url(../flags/1x1/ax.svg)}.fi-az{background-image:url(../flags/4x3/az.svg)}.fi-az.fis{background-image:url(../flags/1x1/az.svg)}.fi-ba{background-image:url(../flags/4x3/ba.svg)}.fi-ba.fis{background-image:url(../flags/1x1/ba.svg)}.fi-bb{background-image:url(../flags/4x3/bb.svg)}.fi-bb.fis{background-image:url(../flags/1x1/bb.svg)}.fi-bd{background-image:url(../flags/4x3/bd.svg)}.fi-bd.fis{background-image:url(../flags/1x1/bd.svg)}.fi-be{background-image:url(../flags/4x3/be.svg)}.fi-be.fis{background-image:url(../flags/1x1/be.svg)}.fi-bf{background-image:url(../flags/4x3/bf.svg)}.fi-bf.fis{background-image:url(../flags/1x1/bf.svg)}.fi-bg{background-image:url(../flags/4x3/bg.svg)}.fi-bg.fis{background-image:url(../flags/1x1/bg.svg)}.fi-bh{background-image:url(../flags/4x3/bh.svg)}.fi-bh.fis{background-image:url(../flags/1x1/bh.svg)}.fi-bi{background-image:url(../flags/4x3/bi.svg)}.fi-bi.fis{background-image:url(../flags/1x1/bi.svg)}.fi-bj{background-image:url(../flags/4x3/bj.svg)}.fi-bj.fis{background-image:url(../flags/1x1/bj.svg)}.fi-bl{background-image:url(../flags/4x3/bl.svg)}.fi-bl.fis{background-image:url(../flags/1x1/bl.svg)}.fi-bm{background-image:url(../flags/4x3/bm.svg)}.fi-bm.fis{background-image:url(../flags/1x1/bm.svg)}.fi-bn{background-image:url(../flags/4x3/bn.svg)}.fi-bn.fis{background-image:url(../flags/1x1/bn.svg)}.fi-bo{background-image:url(../flags/4x3/bo.svg)}.fi-bo.fis{background-image:url(../flags/1x1/bo.svg)}.fi-bq{background-image:url(../flags/4x3/bq.svg)}.fi-bq.fis{background-image:url(../flags/1x1/bq.svg)}.fi-br{background-image:url(../flags/4x3/br.svg)}.fi-br.fis{background-image:url(../flags/1x1/br.svg)}.fi-bs{background-image:url(../flags/4x3/bs.svg)}.fi-bs.fis{background-image:url(../flags/1x1/bs.svg)}.fi-bt{background-image:url(../flags/4x3/bt.svg)}.fi-bt.fis{background-image:url(../flags/1x1/bt.svg)}.fi-bv{background-image:url(../flags/4x3/bv.svg)}.fi-bv.fis{background-image:url(../flags/1x1/bv.svg)}.fi-bw{background-image:url(../flags/4x3/bw.svg)}.fi-bw.fis{background-image:url(../flags/1x1/bw.svg)}.fi-by{background-image:url(../flags/4x3/by.svg)}.fi-by.fis{background-image:url(../flags/1x1/by.svg)}.fi-bz{background-image:url(../flags/4x3/bz.svg)}.fi-bz.fis{background-image:url(../flags/1x1/bz.svg)}.fi-ca{background-image:url(../flags/4x3/ca.svg)}.fi-ca.fis{background-image:url(../flags/1x1/ca.svg)}.fi-cc{background-image:url(../flags/4x3/cc.svg)}.fi-cc.fis{background-image:url(../flags/1x1/cc.svg)}.fi-cd{background-image:url(../flags/4x3/cd.svg)}.fi-cd.fis{background-image:url(../flags/1x1/cd.svg)}.fi-cf{background-image:url(../flags/4x3/cf.svg)}.fi-cf.fis{background-image:url(../flags/1x1/cf.svg)}.fi-cg{background-image:url(../flags/4x3/cg.svg)}.fi-cg.fis{background-image:url(../flags/1x1/cg.svg)}.fi-ch{background-image:url(../flags/4x3/ch.svg)}.fi-ch.fis{background-image:url(../flags/1x1/ch.svg)}.fi-ci{background-image:url(../flags/4x3/ci.svg)}.fi-ci.fis{background-image:url(../flags/1x1/ci.svg)}.fi-ck{background-image:url(../flags/4x3/ck.svg)}.fi-ck.fis{background-image:url(../flags/1x1/ck.svg)}.fi-cl{background-image:url(../flags/4x3/cl.svg)}.fi-cl.fis{background-image:url(../flags/1x1/cl.svg)}.fi-cm{background-image:url(../flags/4x3/cm.svg)}.fi-cm.fis{background-image:url(../flags/1x1/cm.svg)}.fi-cn{background-image:url(../flags/4x3/cn.svg)}.fi-cn.fis{background-image:url(../flags/1x1/cn.svg)}.fi-co{background-image:url(../flags/4x3/co.svg)}.fi-co.fis{background-image:url(../flags/1x1/co.svg)}.fi-cr{background-image:url(../flags/4x3/cr.svg)}.fi-cr.fis{background-image:url(../flags/1x1/cr.svg)}.fi-cu{background-image:url(../flags/4x3/cu.svg)}.fi-cu.fis{background-image:url(../flags/1x1/cu.svg)}.fi-cv{background-image:url(../flags/4x3/cv.svg)}.fi-cv.fis{background-image:url(../flags/1x1/cv.svg)}.fi-cw{background-image:url(../flags/4x3/cw.svg)}.fi-cw.fis{background-image:url(../flags/1x1/cw.svg)}.fi-cx{background-image:url(../flags/4x3/cx.svg)}.fi-cx.fis{background-image:url(../flags/1x1/cx.svg)}.fi-cy{background-image:url(../flags/4x3/cy.svg)}.fi-cy.fis{background-image:url(../flags/1x1/cy.svg)}.fi-cz{background-image:url(../flags/4x3/cz.svg)}.fi-cz.fis{background-image:url(../flags/1x1/cz.svg)}.fi-de{background-image:url(../flags/4x3/de.svg)}.fi-de.fis{background-image:url(../flags/1x1/de.svg)}.fi-dj{background-image:url(../flags/4x3/dj.svg)}.fi-dj.fis{background-image:url(../flags/1x1/dj.svg)}.fi-dk{background-image:url(../flags/4x3/dk.svg)}.fi-dk.fis{background-image:url(../flags/1x1/dk.svg)}.fi-dm{background-image:url(../flags/4x3/dm.svg)}.fi-dm.fis{background-image:url(../flags/1x1/dm.svg)}.fi-do{background-image:url(../flags/4x3/do.svg)}.fi-do.fis{background-image:url(../flags/1x1/do.svg)}.fi-dz{background-image:url(../flags/4x3/dz.svg)}.fi-dz.fis{background-image:url(../flags/1x1/dz.svg)}.fi-ec{background-image:url(../flags/4x3/ec.svg)}.fi-ec.fis{background-image:url(../flags/1x1/ec.svg)}.fi-ee{background-image:url(../flags/4x3/ee.svg)}.fi-ee.fis{background-image:url(../flags/1x1/ee.svg)}.fi-eg{background-image:url(../flags/4x3/eg.svg)}.fi-eg.fis{background-image:url(../flags/1x1/eg.svg)}.fi-eh{background-image:url(../flags/4x3/eh.svg)}.fi-eh.fis{background-image:url(../flags/1x1/eh.svg)}.fi-er{background-image:url(../flags/4x3/er.svg)}.fi-er.fis{background-image:url(../flags/1x1/er.svg)}.fi-es{background-image:url(../flags/4x3/es.svg)}.fi-es.fis{background-image:url(../flags/1x1/es.svg)}.fi-et{background-image:url(../flags/4x3/et.svg)}.fi-et.fis{background-image:url(../flags/1x1/et.svg)}.fi-fi{background-image:url(../flags/4x3/fi.svg)}.fi-fi.fis{background-image:url(../flags/1x1/fi.svg)}.fi-fj{background-image:url(../flags/4x3/fj.svg)}.fi-fj.fis{background-image:url(../flags/1x1/fj.svg)}.fi-fk{background-image:url(../flags/4x3/fk.svg)}.fi-fk.fis{background-image:url(../flags/1x1/fk.svg)}.fi-fm{background-image:url(../flags/4x3/fm.svg)}.fi-fm.fis{background-image:url(../flags/1x1/fm.svg)}.fi-fo{background-image:url(../flags/4x3/fo.svg)}.fi-fo.fis{background-image:url(../flags/1x1/fo.svg)}.fi-fr{background-image:url(../flags/4x3/fr.svg)}.fi-fr.fis{background-image:url(../flags/1x1/fr.svg)}.fi-ga{background-image:url(../flags/4x3/ga.svg)}.fi-ga.fis{background-image:url(../flags/1x1/ga.svg)}.fi-gb{background-image:url(../flags/4x3/gb.svg)}.fi-gb.fis{background-image:url(../flags/1x1/gb.svg)}.fi-gd{background-image:url(../flags/4x3/gd.svg)}.fi-gd.fis{background-image:url(../flags/1x1/gd.svg)}.fi-ge{background-image:url(../flags/4x3/ge.svg)}.fi-ge.fis{background-image:url(../flags/1x1/ge.svg)}.fi-gf{background-image:url(../flags/4x3/gf.svg)}.fi-gf.fis{background-image:url(../flags/1x1/gf.svg)}.fi-gg{background-image:url(../flags/4x3/gg.svg)}.fi-gg.fis{background-image:url(../flags/1x1/gg.svg)}.fi-gh{background-image:url(../flags/4x3/gh.svg)}.fi-gh.fis{background-image:url(../flags/1x1/gh.svg)}.fi-gi{background-image:url(../flags/4x3/gi.svg)}.fi-gi.fis{background-image:url(../flags/1x1/gi.svg)}.fi-gl{background-image:url(../flags/4x3/gl.svg)}.fi-gl.fis{background-image:url(../flags/1x1/gl.svg)}.fi-gm{background-image:url(../flags/4x3/gm.svg)}.fi-gm.fis{background-image:url(../flags/1x1/gm.svg)}.fi-gn{background-image:url(../flags/4x3/gn.svg)}.fi-gn.fis{background-image:url(../flags/1x1/gn.svg)}.fi-gp{background-image:url(../flags/4x3/gp.svg)}.fi-gp.fis{background-image:url(../flags/1x1/gp.svg)}.fi-gq{background-image:url(../flags/4x3/gq.svg)}.fi-gq.fis{background-image:url(../flags/1x1/gq.svg)}.fi-gr{background-image:url(../flags/4x3/gr.svg)}.fi-gr.fis{background-image:url(../flags/1x1/gr.svg)}.fi-gs{background-image:url(../flags/4x3/gs.svg)}.fi-gs.fis{background-image:url(../flags/1x1/gs.svg)}.fi-gt{background-image:url(../flags/4x3/gt.svg)}.fi-gt.fis{background-image:url(../flags/1x1/gt.svg)}.fi-gu{background-image:url(../flags/4x3/gu.svg)}.fi-gu.fis{background-image:url(../flags/1x1/gu.svg)}.fi-gw{background-image:url(../flags/4x3/gw.svg)}.fi-gw.fis{background-image:url(../flags/1x1/gw.svg)}.fi-gy{background-image:url(../flags/4x3/gy.svg)}.fi-gy.fis{background-image:url(../flags/1x1/gy.svg)}.fi-hk{background-image:url(../flags/4x3/hk.svg)}.fi-hk.fis{background-image:url(../flags/1x1/hk.svg)}.fi-hm{background-image:url(../flags/4x3/hm.svg)}.fi-hm.fis{background-image:url(../flags/1x1/hm.svg)}.fi-hn{background-image:url(../flags/4x3/hn.svg)}.fi-hn.fis{background-image:url(../flags/1x1/hn.svg)}.fi-hr{background-image:url(../flags/4x3/hr.svg)}.fi-hr.fis{background-image:url(../flags/1x1/hr.svg)}.fi-ht{background-image:url(../flags/4x3/ht.svg)}.fi-ht.fis{background-image:url(../flags/1x1/ht.svg)}.fi-hu{background-image:url(../flags/4x3/hu.svg)}.fi-hu.fis{background-image:url(../flags/1x1/hu.svg)}.fi-id{background-image:url(../flags/4x3/id.svg)}.fi-id.fis{background-image:url(../flags/1x1/id.svg)}.fi-ie{background-image:url(../flags/4x3/ie.svg)}.fi-ie.fis{background-image:url(../flags/1x1/ie.svg)}.fi-il{background-image:url(../flags/4x3/il.svg)}.fi-il.fis{background-image:url(../flags/1x1/il.svg)}.fi-im{background-image:url(../flags/4x3/im.svg)}.fi-im.fis{background-image:url(../flags/1x1/im.svg)}.fi-in{background-image:url(../flags/4x3/in.svg)}.fi-in.fis{background-image:url(../flags/1x1/in.svg)}.fi-io{background-image:url(../flags/4x3/io.svg)}.fi-io.fis{background-image:url(../flags/1x1/io.svg)}.fi-iq{background-image:url(../flags/4x3/iq.svg)}.fi-iq.fis{background-image:url(../flags/1x1/iq.svg)}.fi-ir{background-image:url(../flags/4x3/ir.svg)}.fi-ir.fis{background-image:url(../flags/1x1/ir.svg)}.fi-is{background-image:url(../flags/4x3/is.svg)}.fi-is.fis{background-image:url(../flags/1x1/is.svg)}.fi-it{background-image:url(../flags/4x3/it.svg)}.fi-it.fis{background-image:url(../flags/1x1/it.svg)}.fi-je{background-image:url(../flags/4x3/je.svg)}.fi-je.fis{background-image:url(../flags/1x1/je.svg)}.fi-jm{background-image:url(../flags/4x3/jm.svg)}.fi-jm.fis{background-image:url(../flags/1x1/jm.svg)}.fi-jo{background-image:url(../flags/4x3/jo.svg)}.fi-jo.fis{background-image:url(../flags/1x1/jo.svg)}.fi-jp{background-image:url(../flags/4x3/jp.svg)}.fi-jp.fis{background-image:url(../flags/1x1/jp.svg)}.fi-ke{background-image:url(../flags/4x3/ke.svg)}.fi-ke.fis{background-image:url(../flags/1x1/ke.svg)}.fi-kg{background-image:url(../flags/4x3/kg.svg)}.fi-kg.fis{background-image:url(../flags/1x1/kg.svg)}.fi-kh{background-image:url(../flags/4x3/kh.svg)}.fi-kh.fis{background-image:url(../flags/1x1/kh.svg)}.fi-ki{background-image:url(../flags/4x3/ki.svg)}.fi-ki.fis{background-image:url(../flags/1x1/ki.svg)}.fi-km{background-image:url(../flags/4x3/km.svg)}.fi-km.fis{background-image:url(../flags/1x1/km.svg)}.fi-kn{background-image:url(../flags/4x3/kn.svg)}.fi-kn.fis{background-image:url(../flags/1x1/kn.svg)}.fi-kp{background-image:url(../flags/4x3/kp.svg)}.fi-kp.fis{background-image:url(../flags/1x1/kp.svg)}.fi-kr{background-image:url(../flags/4x3/kr.svg)}.fi-kr.fis{background-image:url(../flags/1x1/kr.svg)}.fi-kw{background-image:url(../flags/4x3/kw.svg)}.fi-kw.fis{background-image:url(../flags/1x1/kw.svg)}.fi-ky{background-image:url(../flags/4x3/ky.svg)}.fi-ky.fis{background-image:url(../flags/1x1/ky.svg)}.fi-kz{background-image:url(../flags/4x3/kz.svg)}.fi-kz.fis{background-image:url(../flags/1x1/kz.svg)}.fi-la{background-image:url(../flags/4x3/la.svg)}.fi-la.fis{background-image:url(../flags/1x1/la.svg)}.fi-lb{background-image:url(../flags/4x3/lb.svg)}.fi-lb.fis{background-image:url(../flags/1x1/lb.svg)}.fi-lc{background-image:url(../flags/4x3/lc.svg)}.fi-lc.fis{background-image:url(../flags/1x1/lc.svg)}.fi-li{background-image:url(../flags/4x3/li.svg)}.fi-li.fis{background-image:url(../flags/1x1/li.svg)}.fi-lk{background-image:url(../flags/4x3/lk.svg)}.fi-lk.fis{background-image:url(../flags/1x1/lk.svg)}.fi-lr{background-image:url(../flags/4x3/lr.svg)}.fi-lr.fis{background-image:url(../flags/1x1/lr.svg)}.fi-ls{background-image:url(../flags/4x3/ls.svg)}.fi-ls.fis{background-image:url(../flags/1x1/ls.svg)}.fi-lt{background-image:url(../flags/4x3/lt.svg)}.fi-lt.fis{background-image:url(../flags/1x1/lt.svg)}.fi-lu{background-image:url(../flags/4x3/lu.svg)}.fi-lu.fis{background-image:url(../flags/1x1/lu.svg)}.fi-lv{background-image:url(../flags/4x3/lv.svg)}.fi-lv.fis{background-image:url(../flags/1x1/lv.svg)}.fi-ly{background-image:url(../flags/4x3/ly.svg)}.fi-ly.fis{background-image:url(../flags/1x1/ly.svg)}.fi-ma{background-image:url(../flags/4x3/ma.svg)}.fi-ma.fis{background-image:url(../flags/1x1/ma.svg)}.fi-mc{background-image:url(../flags/4x3/mc.svg)}.fi-mc.fis{background-image:url(../flags/1x1/mc.svg)}.fi-md{background-image:url(../flags/4x3/md.svg)}.fi-md.fis{background-image:url(../flags/1x1/md.svg)}.fi-me{background-image:url(../flags/4x3/me.svg)}.fi-me.fis{background-image:url(../flags/1x1/me.svg)}.fi-mf{background-image:url(../flags/4x3/mf.svg)}.fi-mf.fis{background-image:url(../flags/1x1/mf.svg)}.fi-mg{background-image:url(../flags/4x3/mg.svg)}.fi-mg.fis{background-image:url(../flags/1x1/mg.svg)}.fi-mh{background-image:url(../flags/4x3/mh.svg)}.fi-mh.fis{background-image:url(../flags/1x1/mh.svg)}.fi-mk{background-image:url(../flags/4x3/mk.svg)}.fi-mk.fis{background-image:url(../flags/1x1/mk.svg)}.fi-ml{background-image:url(../flags/4x3/ml.svg)}.fi-ml.fis{background-image:url(../flags/1x1/ml.svg)}.fi-mm{background-image:url(../flags/4x3/mm.svg)}.fi-mm.fis{background-image:url(../flags/1x1/mm.svg)}.fi-mn{background-image:url(../flags/4x3/mn.svg)}.fi-mn.fis{background-image:url(../flags/1x1/mn.svg)}.fi-mo{background-image:url(../flags/4x3/mo.svg)}.fi-mo.fis{background-image:url(../flags/1x1/mo.svg)}.fi-mp{background-image:url(../flags/4x3/mp.svg)}.fi-mp.fis{background-image:url(../flags/1x1/mp.svg)}.fi-mq{background-image:url(../flags/4x3/mq.svg)}.fi-mq.fis{background-image:url(../flags/1x1/mq.svg)}.fi-mr{background-image:url(../flags/4x3/mr.svg)}.fi-mr.fis{background-image:url(../flags/1x1/mr.svg)}.fi-ms{background-image:url(../flags/4x3/ms.svg)}.fi-ms.fis{background-image:url(../flags/1x1/ms.svg)}.fi-mt{background-image:url(../flags/4x3/mt.svg)}.fi-mt.fis{background-image:url(../flags/1x1/mt.svg)}.fi-mu{background-image:url(../flags/4x3/mu.svg)}.fi-mu.fis{background-image:url(../flags/1x1/mu.svg)}.fi-mv{background-image:url(../flags/4x3/mv.svg)}.fi-mv.fis{background-image:url(../flags/1x1/mv.svg)}.fi-mw{background-image:url(../flags/4x3/mw.svg)}.fi-mw.fis{background-image:url(../flags/1x1/mw.svg)}.fi-mx{background-image:url(../flags/4x3/mx.svg)}.fi-mx.fis{background-image:url(../flags/1x1/mx.svg)}.fi-my{background-image:url(../flags/4x3/my.svg)}.fi-my.fis{background-image:url(../flags/1x1/my.svg)}.fi-mz{background-image:url(../flags/4x3/mz.svg)}.fi-mz.fis{background-image:url(../flags/1x1/mz.svg)}.fi-na{background-image:url(../flags/4x3/na.svg)}.fi-na.fis{background-image:url(../flags/1x1/na.svg)}.fi-nc{background-image:url(../flags/4x3/nc.svg)}.fi-nc.fis{background-image:url(../flags/1x1/nc.svg)}.fi-ne{background-image:url(../flags/4x3/ne.svg)}.fi-ne.fis{background-image:url(../flags/1x1/ne.svg)}.fi-nf{background-image:url(../flags/4x3/nf.svg)}.fi-nf.fis{background-image:url(../flags/1x1/nf.svg)}.fi-ng{background-image:url(../flags/4x3/ng.svg)}.fi-ng.fis{background-image:url(../flags/1x1/ng.svg)}.fi-ni{background-image:url(../flags/4x3/ni.svg)}.fi-ni.fis{background-image:url(../flags/1x1/ni.svg)}.fi-nl{background-image:url(../flags/4x3/nl.svg)}.fi-nl.fis{background-image:url(../flags/1x1/nl.svg)}.fi-no{background-image:url(../flags/4x3/no.svg)}.fi-no.fis{background-image:url(../flags/1x1/no.svg)}.fi-np{background-image:url(../flags/4x3/np.svg)}.fi-np.fis{background-image:url(../flags/1x1/np.svg)}.fi-nr{background-image:url(../flags/4x3/nr.svg)}.fi-nr.fis{background-image:url(../flags/1x1/nr.svg)}.fi-nu{background-image:url(../flags/4x3/nu.svg)}.fi-nu.fis{background-image:url(../flags/1x1/nu.svg)}.fi-nz{background-image:url(../flags/4x3/nz.svg)}.fi-nz.fis{background-image:url(../flags/1x1/nz.svg)}.fi-om{background-image:url(../flags/4x3/om.svg)}.fi-om.fis{background-image:url(../flags/1x1/om.svg)}.fi-pa{background-image:url(../flags/4x3/pa.svg)}.fi-pa.fis{background-image:url(../flags/1x1/pa.svg)}.fi-pe{background-image:url(../flags/4x3/pe.svg)}.fi-pe.fis{background-image:url(../flags/1x1/pe.svg)}.fi-pf{background-image:url(../flags/4x3/pf.svg)}.fi-pf.fis{background-image:url(../flags/1x1/pf.svg)}.fi-pg{background-image:url(../flags/4x3/pg.svg)}.fi-pg.fis{background-image:url(../flags/1x1/pg.svg)}.fi-ph{background-image:url(../flags/4x3/ph.svg)}.fi-ph.fis{background-image:url(../flags/1x1/ph.svg)}.fi-pk{background-image:url(../flags/4x3/pk.svg)}.fi-pk.fis{background-image:url(../flags/1x1/pk.svg)}.fi-pl{background-image:url(../flags/4x3/pl.svg)}.fi-pl.fis{background-image:url(../flags/1x1/pl.svg)}.fi-pm{background-image:url(../flags/4x3/pm.svg)}.fi-pm.fis{background-image:url(../flags/1x1/pm.svg)}.fi-pn{background-image:url(../flags/4x3/pn.svg)}.fi-pn.fis{background-image:url(../flags/1x1/pn.svg)}.fi-pr{background-image:url(../flags/4x3/pr.svg)}.fi-pr.fis{background-image:url(../flags/1x1/pr.svg)}.fi-ps{background-image:url(../flags/4x3/ps.svg)}.fi-ps.fis{background-image:url(../flags/1x1/ps.svg)}.fi-pt{background-image:url(../flags/4x3/pt.svg)}.fi-pt.fis{background-image:url(../flags/1x1/pt.svg)}.fi-pw{background-image:url(../flags/4x3/pw.svg)}.fi-pw.fis{background-image:url(../flags/1x1/pw.svg)}.fi-py{background-image:url(../flags/4x3/py.svg)}.fi-py.fis{background-image:url(../flags/1x1/py.svg)}.fi-qa{background-image:url(../flags/4x3/qa.svg)}.fi-qa.fis{background-image:url(../flags/1x1/qa.svg)}.fi-re{background-image:url(../flags/4x3/re.svg)}.fi-re.fis{background-image:url(../flags/1x1/re.svg)}.fi-ro{background-image:url(../flags/4x3/ro.svg)}.fi-ro.fis{background-image:url(../flags/1x1/ro.svg)}.fi-rs{background-image:url(../flags/4x3/rs.svg)}.fi-rs.fis{background-image:url(../flags/1x1/rs.svg)}.fi-ru{background-image:url(../flags/4x3/ru.svg)}.fi-ru.fis{background-image:url(../flags/1x1/ru.svg)}.fi-rw{background-image:url(../flags/4x3/rw.svg)}.fi-rw.fis{background-image:url(../flags/1x1/rw.svg)}.fi-sa{background-image:url(../flags/4x3/sa.svg)}.fi-sa.fis{background-image:url(../flags/1x1/sa.svg)}.fi-sb{background-image:url(../flags/4x3/sb.svg)}.fi-sb.fis{background-image:url(../flags/1x1/sb.svg)}.fi-sc{background-image:url(../flags/4x3/sc.svg)}.fi-sc.fis{background-image:url(../flags/1x1/sc.svg)}.fi-sd{background-image:url(../flags/4x3/sd.svg)}.fi-sd.fis{background-image:url(../flags/1x1/sd.svg)}.fi-se{background-image:url(../flags/4x3/se.svg)}.fi-se.fis{background-image:url(../flags/1x1/se.svg)}.fi-sg{background-image:url(../flags/4x3/sg.svg)}.fi-sg.fis{background-image:url(../flags/1x1/sg.svg)}.fi-sh{background-image:url(../flags/4x3/sh.svg)}.fi-sh.fis{background-image:url(../flags/1x1/sh.svg)}.fi-si{background-image:url(../flags/4x3/si.svg)}.fi-si.fis{background-image:url(../flags/1x1/si.svg)}.fi-sj{background-image:url(../flags/4x3/sj.svg)}.fi-sj.fis{background-image:url(../flags/1x1/sj.svg)}.fi-sk{background-image:url(../flags/4x3/sk.svg)}.fi-sk.fis{background-image:url(../flags/1x1/sk.svg)}.fi-sl{background-image:url(../flags/4x3/sl.svg)}.fi-sl.fis{background-image:url(../flags/1x1/sl.svg)}.fi-sm{background-image:url(../flags/4x3/sm.svg)}.fi-sm.fis{background-image:url(../flags/1x1/sm.svg)}.fi-sn{background-image:url(../flags/4x3/sn.svg)}.fi-sn.fis{background-image:url(../flags/1x1/sn.svg)}.fi-so{background-image:url(../flags/4x3/so.svg)}.fi-so.fis{background-image:url(../flags/1x1/so.svg)}.fi-sr{background-image:url(../flags/4x3/sr.svg)}.fi-sr.fis{background-image:url(../flags/1x1/sr.svg)}.fi-ss{background-image:url(../flags/4x3/ss.svg)}.fi-ss.fis{background-image:url(../flags/1x1/ss.svg)}.fi-st{background-image:url(../flags/4x3/st.svg)}.fi-st.fis{background-image:url(../flags/1x1/st.svg)}.fi-sv{background-image:url(../flags/4x3/sv.svg)}.fi-sv.fis{background-image:url(../flags/1x1/sv.svg)}.fi-sx{background-image:url(../flags/4x3/sx.svg)}.fi-sx.fis{background-image:url(../flags/1x1/sx.svg)}.fi-sy{background-image:url(../flags/4x3/sy.svg)}.fi-sy.fis{background-image:url(../flags/1x1/sy.svg)}.fi-sz{background-image:url(../flags/4x3/sz.svg)}.fi-sz.fis{background-image:url(../flags/1x1/sz.svg)}.fi-tc{background-image:url(../flags/4x3/tc.svg)}.fi-tc.fis{background-image:url(../flags/1x1/tc.svg)}.fi-td{background-image:url(../flags/4x3/td.svg)}.fi-td.fis{background-image:url(../flags/1x1/td.svg)}.fi-tf{background-image:url(../flags/4x3/tf.svg)}.fi-tf.fis{background-image:url(../flags/1x1/tf.svg)}.fi-tg{background-image:url(../flags/4x3/tg.svg)}.fi-tg.fis{background-image:url(../flags/1x1/tg.svg)}.fi-th{background-image:url(../flags/4x3/th.svg)}.fi-th.fis{background-image:url(../flags/1x1/th.svg)}.fi-tj{background-image:url(../flags/4x3/tj.svg)}.fi-tj.fis{background-image:url(../flags/1x1/tj.svg)}.fi-tk{background-image:url(../flags/4x3/tk.svg)}.fi-tk.fis{background-image:url(../flags/1x1/tk.svg)}.fi-tl{background-image:url(../flags/4x3/tl.svg)}.fi-tl.fis{background-image:url(../flags/1x1/tl.svg)}.fi-tm{background-image:url(../flags/4x3/tm.svg)}.fi-tm.fis{background-image:url(../flags/1x1/tm.svg)}.fi-tn{background-image:url(../flags/4x3/tn.svg)}.fi-tn.fis{background-image:url(../flags/1x1/tn.svg)}.fi-to{background-image:url(../flags/4x3/to.svg)}.fi-to.fis{background-image:url(../flags/1x1/to.svg)}.fi-tr{background-image:url(../flags/4x3/tr.svg)}.fi-tr.fis{background-image:url(../flags/1x1/tr.svg)}.fi-tt{background-image:url(../flags/4x3/tt.svg)}.fi-tt.fis{background-image:url(../flags/1x1/tt.svg)}.fi-tv{background-image:url(../flags/4x3/tv.svg)}.fi-tv.fis{background-image:url(../flags/1x1/tv.svg)}.fi-tw{background-image:url(../flags/4x3/tw.svg)}.fi-tw.fis{background-image:url(../flags/1x1/tw.svg)}.fi-tz{background-image:url(../flags/4x3/tz.svg)}.fi-tz.fis{background-image:url(../flags/1x1/tz.svg)}.fi-ua{background-image:url(../flags/4x3/ua.svg)}.fi-ua.fis{background-image:url(../flags/1x1/ua.svg)}.fi-ug{background-image:url(../flags/4x3/ug.svg)}.fi-ug.fis{background-image:url(../flags/1x1/ug.svg)}.fi-um{background-image:url(../flags/4x3/um.svg)}.fi-um.fis{background-image:url(../flags/1x1/um.svg)}.fi-us{background-image:url(../flags/4x3/us.svg)}.fi-us.fis{background-image:url(../flags/1x1/us.svg)}.fi-uy{background-image:url(../flags/4x3/uy.svg)}.fi-uy.fis{background-image:url(../flags/1x1/uy.svg)}.fi-uz{background-image:url(../flags/4x3/uz.svg)}.fi-uz.fis{background-image:url(../flags/1x1/uz.svg)}.fi-va{background-image:url(../flags/4x3/va.svg)}.fi-va.fis{background-image:url(../flags/1x1/va.svg)}.fi-vc{background-image:url(../flags/4x3/vc.svg)}.fi-vc.fis{background-image:url(../flags/1x1/vc.svg)}.fi-ve{background-image:url(../flags/4x3/ve.svg)}.fi-ve.fis{background-image:url(../flags/1x1/ve.svg)}.fi-vg{background-image:url(../flags/4x3/vg.svg)}.fi-vg.fis{background-image:url(../flags/1x1/vg.svg)}.fi-vi{background-image:url(../flags/4x3/vi.svg)}.fi-vi.fis{background-image:url(../flags/1x1/vi.svg)}.fi-vn{background-image:url(../flags/4x3/vn.svg)}.fi-vn.fis{background-image:url(../flags/1x1/vn.svg)}.fi-vu{background-image:url(../flags/4x3/vu.svg)}.fi-vu.fis{background-image:url(../flags/1x1/vu.svg)}.fi-wf{background-image:url(../flags/4x3/wf.svg)}.fi-wf.fis{background-image:url(../flags/1x1/wf.svg)}.fi-ws{background-image:url(../flags/4x3/ws.svg)}.fi-ws.fis{background-image:url(../flags/1x1/ws.svg)}.fi-ye{background-image:url(../flags/4x3/ye.svg)}.fi-ye.fis{background-image:url(../flags/1x1/ye.svg)}.fi-yt{background-image:url(../flags/4x3/yt.svg)}.fi-yt.fis{background-image:url(../flags/1x1/yt.svg)}.fi-za{background-image:url(../flags/4x3/za.svg)}.fi-za.fis{background-image:url(../flags/1x1/za.svg)}.fi-zm{background-image:url(../flags/4x3/zm.svg)}.fi-zm.fis{background-image:url(../flags/1x1/zm.svg)}.fi-zw{background-image:url(../flags/4x3/zw.svg)}.fi-zw.fis{background-image:url(../flags/1x1/zw.svg)}.fi-ac{background-image:url(../flags/4x3/ac.svg)}.fi-ac.fis{background-image:url(../flags/1x1/ac.svg)}.fi-cefta{background-image:url(../flags/4x3/cefta.svg)}.fi-cefta.fis{background-image:url(../flags/1x1/cefta.svg)}.fi-cp{background-image:url(../flags/4x3/cp.svg)}.fi-cp.fis{background-image:url(../flags/1x1/cp.svg)}.fi-dg{background-image:url(../flags/4x3/dg.svg)}.fi-dg.fis{background-image:url(../flags/1x1/dg.svg)}.fi-ea{background-image:url(../flags/4x3/ea.svg)}.fi-ea.fis{background-image:url(../flags/1x1/ea.svg)}.fi-es-ct{background-image:url(../flags/4x3/es-ct.svg)}.fi-es-ct.fis{background-image:url(../flags/1x1/es-ct.svg)}.fi-es-ga{background-image:url(../flags/4x3/es-ga.svg)}.fi-es-ga.fis{background-image:url(../flags/1x1/es-ga.svg)}.fi-es-pv{background-image:url(../flags/4x3/es-pv.svg)}.fi-es-pv.fis{background-image:url(../flags/1x1/es-pv.svg)}.fi-eu{background-image:url(../flags/4x3/eu.svg)}.fi-eu.fis{background-image:url(../flags/1x1/eu.svg)}.fi-gb-eng{background-image:url(../flags/4x3/gb-eng.svg)}.fi-gb-eng.fis{background-image:url(../flags/1x1/gb-eng.svg)}.fi-gb-nir{background-image:url(../flags/4x3/gb-nir.svg)}.fi-gb-nir.fis{background-image:url(../flags/1x1/gb-nir.svg)}.fi-gb-sct{background-image:url(../flags/4x3/gb-sct.svg)}.fi-gb-sct.fis{background-image:url(../flags/1x1/gb-sct.svg)}.fi-gb-wls{background-image:url(../flags/4x3/gb-wls.svg)}.fi-gb-wls.fis{background-image:url(../flags/1x1/gb-wls.svg)}.fi-ic{background-image:url(../flags/4x3/ic.svg)}.fi-ic.fis{background-image:url(../flags/1x1/ic.svg)}.fi-ta{background-image:url(../flags/4x3/ta.svg)}.fi-ta.fis{background-image:url(../flags/1x1/ta.svg)}.fi-un{background-image:url(../flags/4x3/un.svg)}.fi-un.fis{background-image:url(../flags/1x1/un.svg)}.fi-xk{background-image:url(../flags/4x3/xk.svg)}.fi-xk.fis{background-image:url(../flags/1x1/xk.svg)} \ No newline at end of file diff --git a/static/vendor/flags/4x3/cn.svg b/static/vendor/flags/4x3/cn.svg new file mode 100644 index 00000000..b1aca5a7 --- /dev/null +++ b/static/vendor/flags/4x3/cn.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/de.svg b/static/vendor/flags/4x3/de.svg new file mode 100644 index 00000000..71629090 --- /dev/null +++ b/static/vendor/flags/4x3/de.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/es.svg b/static/vendor/flags/4x3/es.svg new file mode 100644 index 00000000..24a9d180 --- /dev/null +++ b/static/vendor/flags/4x3/es.svg @@ -0,0 +1,544 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/fr.svg b/static/vendor/flags/4x3/fr.svg new file mode 100644 index 00000000..deb14284 --- /dev/null +++ b/static/vendor/flags/4x3/fr.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/hk.svg b/static/vendor/flags/4x3/hk.svg new file mode 100644 index 00000000..8601f21a --- /dev/null +++ b/static/vendor/flags/4x3/hk.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/jp.svg b/static/vendor/flags/4x3/jp.svg new file mode 100644 index 00000000..f711d73e --- /dev/null +++ b/static/vendor/flags/4x3/jp.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/kr.svg b/static/vendor/flags/4x3/kr.svg new file mode 100644 index 00000000..0c214b8b --- /dev/null +++ b/static/vendor/flags/4x3/kr.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/ru.svg b/static/vendor/flags/4x3/ru.svg new file mode 100644 index 00000000..7f085c39 --- /dev/null +++ b/static/vendor/flags/4x3/ru.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/static/vendor/flags/4x3/us.svg b/static/vendor/flags/4x3/us.svg new file mode 100644 index 00000000..d63a3a36 --- /dev/null +++ b/static/vendor/flags/4x3/us.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/templates/base.html b/templates/base.html index ece094a8..1e7bca43 100644 --- a/templates/base.html +++ b/templates/base.html @@ -6,6 +6,7 @@ + {% block page_css %}{% endblock %} From 6b738a47693d026162538d8079c77e016609e784 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 19:15:55 +0800 Subject: [PATCH 04/10] fix(onboarding): update language handling and selection logic in onboarding process --- static/js/managers/OnboardingManager.js | 36 ++++++++++++++++--------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index 49d230e8..f937e609 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -5,7 +5,7 @@ export class OnboardingManager { constructor() { this.isActive = false; this.currentStep = 0; - this.selectedLanguage = 'en'; + this.selectedLanguage = 'en'; // Will be updated from state this.overlay = null; this.spotlight = null; this.popup = null; @@ -13,8 +13,8 @@ export class OnboardingManager { // Available languages with SVG flags (using flag-icons) this.languages = [ { code: 'en', name: 'English', flag: 'us' }, - { code: 'zh-cn', name: '简体中文', flag: 'cn' }, - { code: 'zh-tw', name: '繁體中文', flag: 'hk' }, + { code: 'zh-CN', name: '简体中文', flag: 'cn' }, + { code: 'zh-TW', name: '繁體中文', flag: 'hk' }, { code: 'ja', name: '日本語', flag: 'jp' }, { code: 'ko', name: '한국어', flag: 'kr' }, { code: 'es', name: 'Español', flag: 'es' }, @@ -98,6 +98,9 @@ export class OnboardingManager { // Show language selection modal showLanguageSelection() { return new Promise((resolve) => { + // Initialize selected language from current settings + this.selectedLanguage = state.global.settings.language || 'en'; + const modal = document.createElement('div'); modal.className = 'language-selection-modal'; modal.innerHTML = ` @@ -134,24 +137,33 @@ export class OnboardingManager { // Handle continue button document.getElementById('continueLanguageBtn').addEventListener('click', async () => { - if (this.selectedLanguage !== 'en') { - // Save language and reload page + const currentLanguage = state.global.settings.language || 'en'; + + // Only change language if it's different from current setting + if (this.selectedLanguage !== currentLanguage) { await this.changeLanguage(this.selectedLanguage); + } else { + document.body.removeChild(modal); + this.startTutorial(); + resolve(); } - document.body.removeChild(modal); - this.startTutorial(); - resolve(); }); - // Handle skip button + // Handle skip button - skip entire tutorial document.getElementById('skipLanguageBtn').addEventListener('click', () => { document.body.removeChild(modal); - this.startTutorial(); + this.skip(); // Skip entire tutorial instead of just language selection resolve(); }); - // Select English by default - modal.querySelector('[data-language="en"]').classList.add('selected'); + // Select current language by default + const currentLanguageOption = modal.querySelector(`[data-language="${this.selectedLanguage}"]`); + if (currentLanguageOption) { + currentLanguageOption.classList.add('selected'); + } else { + // Fallback to English if current language not found + modal.querySelector('[data-language="en"]').classList.add('selected'); + } }); } From 5520aecbba399402ff6d13f6f7155d137044caa3 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 19:22:53 +0800 Subject: [PATCH 05/10] fix(onboarding): adjust language selection logic to skip if already set and update prompt text --- static/css/onboarding.css | 2 +- static/js/managers/OnboardingManager.js | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/static/css/onboarding.css b/static/css/onboarding.css index 6dc17fbc..49bc01fc 100644 --- a/static/css/onboarding.css +++ b/static/css/onboarding.css @@ -117,7 +117,7 @@ border: 1px solid var(--lora-border); border-radius: var(--border-radius-base); padding: var(--space-3); - min-width: 400px; + min-width: 510px; text-align: center; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); backdrop-filter: blur(10px); diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index f937e609..8e44fab9 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -91,6 +91,12 @@ export class OnboardingManager { return; } + // If language has already been set, skip language selection + if (getStorageItem('onboarding_language_set')) { + this.startTutorial(); + return; + } + // Show language selection first await this.showLanguageSelection(); } @@ -106,7 +112,7 @@ export class OnboardingManager { modal.innerHTML = `

Welcome to LoRA Manager

-

Choose your preferred language to get started, or continue with English.

+

Choose Your Language / 选择语言 / 言語を選択

${this.languages.map(lang => `
From ea727aad2ee5d9f08a94c8755597a27b842fad95 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 21:44:23 +0800 Subject: [PATCH 06/10] feat(onboarding): enhance target highlighting with mask and pulsing effect --- static/css/onboarding.css | 33 +++++++- static/js/managers/OnboardingManager.js | 103 ++++++++++++++++++++++-- 2 files changed, 125 insertions(+), 11 deletions(-) diff --git a/static/css/onboarding.css b/static/css/onboarding.css index 49bc01fc..88e1ece0 100644 --- a/static/css/onboarding.css +++ b/static/css/onboarding.css @@ -8,6 +8,9 @@ background: rgba(0, 0, 0, 0.8); z-index: var(--z-overlay); display: none; + /* Use mask to create cutout for highlighted element */ + mask-composite: subtract; + -webkit-mask-composite: subtract; } .onboarding-overlay.active { @@ -19,10 +22,26 @@ 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; + /* Add glow effect */ + box-shadow: + 0 0 0 2px rgba(24, 144, 255, 0.3), + 0 0 20px rgba(24, 144, 255, 0.2), + inset 0 0 0 1px rgba(255, 255, 255, 0.1); +} + +/* Target element highlighting */ +.onboarding-target-highlight { + position: relative; + z-index: calc(var(--z-overlay) + 2) !important; + pointer-events: auto !important; +} + +/* Ensure highlighted elements are interactive */ +.onboarding-target-highlight * { + pointer-events: auto !important; } .onboarding-popup { @@ -33,7 +52,7 @@ padding: var(--space-3); min-width: 320px; max-width: 400px; - z-index: calc(var(--z-overlay) + 2); + z-index: calc(var(--z-overlay) + 3); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); backdrop-filter: blur(10px); } @@ -201,10 +220,16 @@ @keyframes onboarding-pulse { 0%, 100% { - box-shadow: 0 0 0 0 var(--lora-accent); + box-shadow: + 0 0 0 2px rgba(24, 144, 255, 0.4), + 0 0 20px rgba(24, 144, 255, 0.3), + inset 0 0 0 1px rgba(255, 255, 255, 0.1); } 50% { - box-shadow: 0 0 0 8px transparent; + box-shadow: + 0 0 0 4px rgba(24, 144, 255, 0.6), + 0 0 30px rgba(24, 144, 255, 0.4), + inset 0 0 0 1px rgba(255, 255, 255, 0.2); } } diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index 8e44fab9..1c08e700 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -9,6 +9,7 @@ export class OnboardingManager { this.overlay = null; this.spotlight = null; this.popup = null; + this.currentTarget = null; // Track current highlighted element // Available languages with SVG flags (using flag-icons) this.languages = [ @@ -252,16 +253,15 @@ export class OnboardingManager { return; } - // Position spotlight + // Clear previous target highlighting + this.clearTargetHighlight(); + + // Position spotlight and create mask 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'; + this.highlightTarget(target); } else { this.spotlight.style.display = 'none'; + this.clearOverlayMask(); } // Update popup content @@ -344,6 +344,92 @@ export class OnboardingManager { popup.style.transform = 'none'; } + // Highlight target element with mask approach + highlightTarget(target) { + const rect = target.getBoundingClientRect(); + const padding = 4; // Padding around the target element + const offset = 3; // Shift spotlight up and left by 3px + + // Position spotlight + this.spotlight.style.left = `${rect.left - padding - offset}px`; + this.spotlight.style.top = `${rect.top - padding - offset}px`; + this.spotlight.style.width = `${rect.width + padding * 2}px`; + this.spotlight.style.height = `${rect.height + padding * 2}px`; + this.spotlight.style.display = 'block'; + + // Create mask for overlay to cut out the highlighted area + this.createOverlayMask(rect, padding, offset); + + // Add highlight class to target and ensure it's interactive + target.classList.add('onboarding-target-highlight'); + this.currentTarget = target; + + // Add pulsing animation + this.spotlight.classList.add('onboarding-highlight'); + } + + // Create mask for overlay to cut out highlighted area + createOverlayMask(rect, padding, offset = 0) { + const x = rect.left - padding - offset; + const y = rect.top - padding - offset; + const width = rect.width + padding * 2; + const height = rect.height + padding * 2; + + // Create SVG mask + const maskId = 'onboarding-mask'; + let maskSvg = document.getElementById(maskId); + + if (!maskSvg) { + maskSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + maskSvg.id = maskId; + maskSvg.style.position = 'absolute'; + maskSvg.style.top = '0'; + maskSvg.style.left = '0'; + maskSvg.style.width = '100%'; + maskSvg.style.height = '100%'; + maskSvg.style.pointerEvents = 'none'; + document.body.appendChild(maskSvg); + } + + // Clear existing mask content + maskSvg.innerHTML = ` + + + + + + + `; + + // Apply mask to overlay + this.overlay.style.mask = 'url(#overlay-mask)'; + this.overlay.style.webkitMask = 'url(#overlay-mask)'; + } + + // Clear overlay mask + clearOverlayMask() { + this.overlay.style.mask = 'none'; + this.overlay.style.webkitMask = 'none'; + + const maskSvg = document.getElementById('onboarding-mask'); + if (maskSvg) { + maskSvg.remove(); + } + } + + // Clear target highlighting + clearTargetHighlight() { + if (this.currentTarget) { + this.currentTarget.classList.remove('onboarding-target-highlight'); + this.currentTarget = null; + } + + if (this.spotlight) { + this.spotlight.classList.remove('onboarding-highlight'); + } + } + // Navigate to next step nextStep() { this.showStep(this.currentStep + 1); @@ -370,6 +456,9 @@ export class OnboardingManager { // Clean up overlay elements cleanup() { + this.clearTargetHighlight(); + this.clearOverlayMask(); + if (this.overlay) { document.body.removeChild(this.overlay); this.overlay = null; From 8fbf2ab56d8f6ca61d34c0663fedf94a3968dacf Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 22:17:48 +0800 Subject: [PATCH 07/10] feat(onboarding): add multilingual support for onboarding steps and language selection --- locales/de.json | 48 ++++++++++++++++++++++ locales/en.json | 48 ++++++++++++++++++++++ locales/es.json | 48 ++++++++++++++++++++++ locales/fr.json | 48 ++++++++++++++++++++++ locales/ja.json | 48 ++++++++++++++++++++++ locales/ko.json | 48 ++++++++++++++++++++++ locales/ru.json | 48 ++++++++++++++++++++++ locales/zh-CN.json | 48 ++++++++++++++++++++++ locales/zh-TW.json | 48 ++++++++++++++++++++++ static/js/managers/OnboardingManager.js | 51 +++++++++++++----------- templates/components/folder_sidebar.html | 2 +- 11 files changed, 460 insertions(+), 25 deletions(-) diff --git a/locales/de.json b/locales/de.json index 874673c9..f2085f98 100644 --- a/locales/de.json +++ b/locales/de.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "Willkommen beim LoRA Manager", + "skip": "Überspringen", + "continue": "Weiter", + "changeFailed": "Fehler beim Ändern der Sprache: {message}" + }, + "tutorial": { + "skipTutorial": "Tutorial überspringen", + "back": "Zurück", + "next": "Weiter", + "finish": "Fertigstellen" + }, + "steps": { + "fetch": { + "title": "Modelldaten abrufen", + "content": "Klicken Sie auf die Schaltfläche Abrufen, um Modelldaten und Vorschaubilder von Civitai herunterzuladen." + }, + "download": { + "title": "Neue Modelle herunterladen", + "content": "Verwenden Sie die Schaltfläche Herunterladen, um Modelle direkt über Civitai-URLs herunterzuladen." + }, + "bulk": { + "title": "Massenoperationen", + "content": "Wechseln Sie in den Massenmodus, indem Sie auf diese Schaltfläche klicken oder B drücken. Wählen Sie mehrere Modelle aus und führen Sie Stapeloperationen durch. Mit Strg+A können Sie alle sichtbaren Modelle auswählen." + }, + "searchOptions": { + "title": "Suchoptionen", + "content": "Klicken Sie auf diese Schaltfläche, um festzulegen, in welchen Feldern gesucht werden soll: Dateiname, Modellname, Tags oder Erstellername. Passen Sie den Suchbereich an." + }, + "filter": { + "title": "Modelle filtern", + "content": "Verwenden Sie Filter, um Modelle nach Basis-Modelltyp (SD1.5, SDXL, Flux usw.) oder bestimmten Tags einzugrenzen." + }, + "breadcrumb": { + "title": "Breadcrumb-Navigation", + "content": "Die Breadcrumb-Navigation zeigt Ihren aktuellen Pfad und ermöglicht eine schnelle Navigation zwischen Ordnern. Klicken Sie auf einen Ordnernamen, um direkt dorthin zu springen." + }, + "modelCards": { + "title": "Modellkarten", + "content": "Einfachklick auf eine Modellkarte zeigt detaillierte Informationen und ermöglicht das Bearbeiten von Metadaten. Suchen Sie nach dem Stiftsymbol, wenn Sie über bearbeitbare Felder fahren." + }, + "contextMenu": { + "title": "Kontextmenü", + "content": "Rechtsklick auf eine Modellkarte öffnet ein Kontextmenü mit weiteren Aktionen." + } + } + }, "modelCard": { "actions": { "addToFavorites": "Zu Favoriten hinzufügen", diff --git a/locales/en.json b/locales/en.json index fc609e15..6076f0af 100644 --- a/locales/en.json +++ b/locales/en.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "Welcome to LoRA Manager", + "skip": "Skip", + "continue": "Continue", + "changeFailed": "Failed to change language: {message}" + }, + "tutorial": { + "skipTutorial": "Skip Tutorial", + "back": "Back", + "next": "Next", + "finish": "Finish" + }, + "steps": { + "fetch": { + "title": "Fetch Models Metadata", + "content": "Click the Fetch button to download model metadata and preview images from Civitai." + }, + "download": { + "title": "Download New Models", + "content": "Use the Download button to download models directly from Civitai URLs." + }, + "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." + }, + "searchOptions": { + "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." + }, + "filter": { + "title": "Filter Models", + "content": "Use filters to narrow down models by base model type (SD1.5, SDXL, Flux, etc.) or by specific tags." + }, + "breadcrumb": { + "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." + }, + "modelCards": { + "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." + }, + "contextMenu": { + "title": "Context Menu", + "content": "Right-click any model card for a context menu with additional actions." + } + } + }, "modelCard": { "actions": { "addToFavorites": "Add to favorites", diff --git a/locales/es.json b/locales/es.json index 9ca8a1aa..471eb111 100644 --- a/locales/es.json +++ b/locales/es.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "Bienvenido a LoRA Manager", + "skip": "Saltar", + "continue": "Continuar", + "changeFailed": "Error al cambiar el idioma: {message}" + }, + "tutorial": { + "skipTutorial": "Saltar tutorial", + "back": "Atrás", + "next": "Siguiente", + "finish": "Finalizar" + }, + "steps": { + "fetch": { + "title": "Obtener metadatos de modelos", + "content": "Haz clic en el botón Obtener para descargar metadatos y vistas previas de modelos desde Civitai." + }, + "download": { + "title": "Descargar nuevos modelos", + "content": "Usa el botón Descargar para descargar modelos directamente desde URLs de Civitai." + }, + "bulk": { + "title": "Operaciones masivas", + "content": "Entra en modo masivo haciendo clic en este botón o presionando B. Selecciona varios modelos y realiza operaciones por lotes. Usa Ctrl+A para seleccionar todos los modelos visibles." + }, + "searchOptions": { + "title": "Opciones de búsqueda", + "content": "Haz clic en este botón para configurar en qué campos buscar: nombre de archivo, nombre del modelo, etiquetas o nombre del creador. Personaliza el alcance de tu búsqueda." + }, + "filter": { + "title": "Filtrar modelos", + "content": "Utiliza los filtros para reducir los modelos por tipo de modelo base (SD1.5, SDXL, Flux, etc.) o por etiquetas específicas." + }, + "breadcrumb": { + "title": "Navegación por rutas", + "content": "La navegación por rutas muestra tu ruta actual y permite una navegación rápida entre carpetas. Haz clic en cualquier nombre de carpeta para ir directamente allí." + }, + "modelCards": { + "title": "Tarjetas de modelo", + "content": "Clic único en una tarjeta de modelo para ver información detallada y editar metadatos. Busca el icono de lápiz al pasar el ratón sobre campos editables." + }, + "contextMenu": { + "title": "Menú contextual", + "content": "Clic derecho en cualquier tarjeta de modelo para ver un menú contextual con acciones adicionales." + } + } + }, "modelCard": { "actions": { "addToFavorites": "Añadir a favoritos", diff --git a/locales/fr.json b/locales/fr.json index 399a3fcf..15ed204b 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -40,6 +40,54 @@ "tb": "To" } }, + "onboarding": { + "languageSelection": { + "title": "Bienvenue dans LoRA Manager", + "skip": "Passer", + "continue": "Continuer", + "changeFailed": "Échec du changement de langue : {message}" + }, + "tutorial": { + "skipTutorial": "Passer le tutoriel", + "back": "Retour", + "next": "Suivant", + "finish": "Terminer" + }, + "steps": { + "fetch": { + "title": "Récupérer les métadonnées des modèles", + "content": "Cliquez sur le bouton Récupérer pour télécharger les métadonnées et les images d'aperçu des modèles depuis Civitai." + }, + "download": { + "title": "Télécharger de nouveaux modèles", + "content": "Utilisez le bouton Télécharger pour télécharger des modèles directement à partir des URLs Civitai." + }, + "bulk": { + "title": "Opérations groupées", + "content": "Activez le mode groupé en cliquant sur ce bouton ou en appuyant sur B. Sélectionnez plusieurs modèles et effectuez des opérations par lot. Utilisez Ctrl+A pour sélectionner tous les modèles visibles." + }, + "searchOptions": { + "title": "Options de recherche", + "content": "Cliquez sur ce bouton pour configurer les champs à rechercher : nom de fichier, nom du modèle, tags ou nom du créateur. Personnalisez la portée de votre recherche." + }, + "filter": { + "title": "Filtrer les modèles", + "content": "Utilisez les filtres pour affiner les modèles par type de modèle de base (SD1.5, SDXL, Flux, etc.) ou par tags spécifiques." + }, + "breadcrumb": { + "title": "Navigation par fil d'Ariane", + "content": "La navigation par fil d'Ariane affiche votre chemin actuel et permet une navigation rapide entre les dossiers. Cliquez sur le nom d'un dossier pour y accéder directement." + }, + "modelCards": { + "title": "Cartes de modèles", + "content": "Cliquez une fois sur une carte de modèle pour afficher les informations détaillées et modifier les métadonnées. Cherchez l'icône crayon lors du survol des champs modifiables." + }, + "contextMenu": { + "title": "Menu contextuel", + "content": "Clic droit sur une carte de modèle pour accéder à un menu contextuel avec des actions supplémentaires." + } + } + }, "modelCard": { "actions": { "addToFavorites": "Ajouter aux favoris", diff --git a/locales/ja.json b/locales/ja.json index f531101e..99b63861 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "LoRA Managerへようこそ", + "skip": "スキップ", + "continue": "続行", + "changeFailed": "言語の変更に失敗しました:{message}" + }, + "tutorial": { + "skipTutorial": "チュートリアルをスキップ", + "back": "戻る", + "next": "次へ", + "finish": "完了" + }, + "steps": { + "fetch": { + "title": "モデルメタデータの取得", + "content": "取得ボタンをクリックして、Civitaiからモデルのメタデータとプレビュー画像をダウンロードします。" + }, + "download": { + "title": "新しいモデルのダウンロード", + "content": "ダウンロードボタンを使って、CivitaiのURLから直接モデルをダウンロードできます。" + }, + "bulk": { + "title": "一括操作", + "content": "このボタンをクリックするか、Bキーを押して一括モードに入ります。複数のモデルを選択して一括操作が可能です。Ctrl+Aで表示中のモデルをすべて選択できます。" + }, + "searchOptions": { + "title": "検索オプション", + "content": "このボタンをクリックして、検索対象フィールド(ファイル名、モデル名、タグ、作成者名)を設定できます。検索範囲をカスタマイズしましょう。" + }, + "filter": { + "title": "モデルのフィルタ", + "content": "フィルタを使って、ベースモデルタイプ(SD1.5、SDXL、Fluxなど)や特定のタグでモデルを絞り込めます。" + }, + "breadcrumb": { + "title": "パンくずナビゲーション", + "content": "パンくずナビゲーションは現在のパスを表示し、フォルダ間を素早く移動できます。フォルダ名をクリックすると直接ジャンプできます。" + }, + "modelCards": { + "title": "モデルカード", + "content": "モデルカードをクリックすると詳細情報の表示やメタデータの編集ができます。編集可能な項目はホバー時に鉛筆アイコンが表示されます。" + }, + "contextMenu": { + "title": "コンテキストメニュー", + "content": "モデルカードを右クリックすると追加の操作ができるコンテキストメニューが表示されます。" + } + } + }, "modelCard": { "actions": { "addToFavorites": "お気に入りに追加", diff --git a/locales/ko.json b/locales/ko.json index 2ea2fab2..51abf06b 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "LoRA Manager에 오신 것을 환영합니다", + "skip": "건너뛰기", + "continue": "계속", + "changeFailed": "언어 변경 실패: {message}" + }, + "tutorial": { + "skipTutorial": "튜토리얼 건너뛰기", + "back": "뒤로", + "next": "다음", + "finish": "완료" + }, + "steps": { + "fetch": { + "title": "모델 메타데이터 가져오기", + "content": "가져오기 버튼을 클릭하여 Civitai에서 모델 메타데이터와 미리보기 이미지를 다운로드하세요." + }, + "download": { + "title": "새 모델 다운로드", + "content": "다운로드 버튼을 사용하여 Civitai URL에서 모델을 직접 다운로드하세요." + }, + "bulk": { + "title": "일괄 작업", + "content": "이 버튼을 클릭하거나 B 키를 눌러 일괄 모드로 진입하세요. 여러 모델을 선택하여 일괄 작업을 수행할 수 있습니다. Ctrl+A로 모든 표시된 모델을 선택하세요." + }, + "searchOptions": { + "title": "검색 옵션", + "content": "이 버튼을 클릭하여 검색할 필드를 설정하세요: 파일명, 모델명, 태그, 제작자명 등. 검색 범위를 자유롭게 지정할 수 있습니다." + }, + "filter": { + "title": "모델 필터링", + "content": "필터를 사용하여 베이스 모델 유형(SD1.5, SDXL, Flux 등)이나 특정 태그로 모델을 좁혀보세요." + }, + "breadcrumb": { + "title": "경로(브레드크럼) 내비게이션", + "content": "브레드크럼 내비게이션은 현재 경로를 보여주며 폴더 간 빠른 이동을 지원합니다. 폴더 이름을 클릭하면 해당 위치로 바로 이동합니다." + }, + "modelCards": { + "title": "모델 카드", + "content": "한 번 클릭하면 모델 카드의 상세 정보와 메타데이터를 볼 수 있습니다. 편집 가능한 필드는 마우스를 올리면 연필 아이콘이 표시됩니다." + }, + "contextMenu": { + "title": "컨텍스트 메뉴", + "content": "오른쪽 클릭으로 모델 카드의 추가 작업 메뉴를 사용할 수 있습니다." + } + } + }, "modelCard": { "actions": { "addToFavorites": "즐겨찾기에 추가", diff --git a/locales/ru.json b/locales/ru.json index d3d9eb96..73e9afb6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -40,6 +40,54 @@ "tb": "ТБ" } }, + "onboarding": { + "languageSelection": { + "title": "Добро пожаловать в LoRA Manager", + "skip": "Пропустить", + "continue": "Продолжить", + "changeFailed": "Не удалось изменить язык: {message}" + }, + "tutorial": { + "skipTutorial": "Пропустить обучение", + "back": "Назад", + "next": "Далее", + "finish": "Завершить" + }, + "steps": { + "fetch": { + "title": "Получить метаданные моделей", + "content": "Нажмите кнопку Получить, чтобы загрузить метаданные моделей и изображения превью с Civitai." + }, + "download": { + "title": "Загрузить новые модели", + "content": "Используйте кнопку Загрузить для скачивания моделей напрямую по URL Civitai." + }, + "bulk": { + "title": "Массовые операции", + "content": "Войдите в массовый режим, нажав эту кнопку или клавишу B. Выберите несколько моделей и выполните пакетные операции. Используйте Ctrl+A для выбора всех видимых моделей." + }, + "searchOptions": { + "title": "Опции поиска", + "content": "Нажмите эту кнопку, чтобы настроить поля для поиска: имя файла, название модели, теги или имя автора. Настройте область поиска." + }, + "filter": { + "title": "Фильтрация моделей", + "content": "Используйте фильтры для сужения списка моделей по типу базовой модели (SD1.5, SDXL, Flux и др.) или по конкретным тегам." + }, + "breadcrumb": { + "title": "Навигация по папкам", + "content": "Навигация по папкам показывает ваш текущий путь и позволяет быстро переходить между папками. Нажмите на имя папки для перехода." + }, + "modelCards": { + "title": "Карточки моделей", + "content": "Одиночный клик по карточке модели откроет подробную информацию и позволит редактировать метаданные. Ищите значок карандаша при наведении на редактируемые поля." + }, + "contextMenu": { + "title": "Контекстное меню", + "content": "Правый клик по карточке модели откроет контекстное меню с дополнительными действиями." + } + } + }, "modelCard": { "actions": { "addToFavorites": "Добавить в избранное", diff --git a/locales/zh-CN.json b/locales/zh-CN.json index 28e2e551..f1426183 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "欢迎使用 LoRA 管理器", + "skip": "跳过", + "continue": "继续", + "changeFailed": "切换语言失败:{message}" + }, + "tutorial": { + "skipTutorial": "跳过教程", + "back": "返回", + "next": "下一步", + "finish": "完成" + }, + "steps": { + "fetch": { + "title": "获取模型元数据", + "content": "点击 获取 按钮,从 Civitai 下载模型元数据和预览图片。" + }, + "download": { + "title": "下载新模型", + "content": "使用 下载 按钮,可直接通过 Civitai URL 下载模型。" + }, + "bulk": { + "title": "批量操作", + "content": "点击此按钮或按 B 进入批量模式。可多选模型并进行批量操作。使用 Ctrl+A 全选所有可见模型。" + }, + "searchOptions": { + "title": "搜索选项", + "content": "点击此按钮可配置搜索字段:文件名、模型名称、标签或创作者名称。自定义你的搜索范围。" + }, + "filter": { + "title": "筛选模型", + "content": "使用筛选器按基础模型类型(SD1.5、SDXL、Flux 等)或特定标签缩小模型范围。" + }, + "breadcrumb": { + "title": "路径导航", + "content": "路径导航显示你当前所在位置,并可快速在文件夹间跳转。点击任意文件夹名称即可直接跳转。" + }, + "modelCards": { + "title": "模型卡片", + "content": "单击模型卡片可查看详细信息并编辑元数据。悬停在可编辑字段时会显示铅笔图标。" + }, + "contextMenu": { + "title": "右键菜单", + "content": "右键点击任意模型卡片可打开更多操作菜单。" + } + } + }, "modelCard": { "actions": { "addToFavorites": "添加到收藏", diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 4b2b564f..9047526e 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -40,6 +40,54 @@ "tb": "TB" } }, + "onboarding": { + "languageSelection": { + "title": "歡迎使用 LoRA 管理器", + "skip": "跳過", + "continue": "繼續", + "changeFailed": "切換語言失敗:{message}" + }, + "tutorial": { + "skipTutorial": "跳過教學", + "back": "返回", + "next": "下一步", + "finish": "完成" + }, + "steps": { + "fetch": { + "title": "取得模型 metadata", + "content": "點擊 取得 按鈕,從 Civitai 下載模型 metadata 與預覽圖片。" + }, + "download": { + "title": "下載新模型", + "content": "使用 下載 按鈕,直接從 Civitai 網址下載模型。" + }, + "bulk": { + "title": "批次操作", + "content": "點擊此按鈕或按下 B 進入批次模式。可選取多個模型並執行批量操作。使用 Ctrl+A 選取所有可見模型。" + }, + "searchOptions": { + "title": "搜尋選項", + "content": "點擊此按鈕可設定搜尋欄位:檔案名稱、模型名稱、標籤或創作者名稱。自訂搜尋範圍。" + }, + "filter": { + "title": "篩選模型", + "content": "使用篩選器依基礎模型類型(SD1.5、SDXL、Flux 等)或特定標籤縮小模型範圍。" + }, + "breadcrumb": { + "title": "麵包屑導覽", + "content": "麵包屑導覽顯示目前路徑,並可快速在資料夾間切換。點擊任一資料夾名稱即可直接跳轉。" + }, + "modelCards": { + "title": "模型卡片", + "content": "單擊模型卡片可檢視詳細資訊並編輯 metadata。滑鼠懸停於可編輯欄位時會顯示鉛筆圖示。" + }, + "contextMenu": { + "title": "右鍵選單", + "content": "右鍵點擊任一模型卡片可開啟更多操作選單。" + } + } + }, "modelCard": { "actions": { "addToFavorites": "加入收藏", diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index 1c08e700..e60c5217 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -1,5 +1,7 @@ import { getStorageItem, setStorageItem } from '../utils/storageHelpers.js'; import { state } from '../state/index.js'; +import { translate } from '../utils/i18nHelpers.js'; +import { showToast } from '../utils/uiHelpers.js'; export class OnboardingManager { constructor() { @@ -28,51 +30,51 @@ export class OnboardingManager { 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.', + title: () => translate('onboarding.steps.fetch.title', {}, 'Fetch Models Metadata'), + content: () => translate('onboarding.steps.fetch.content', {}, 'Click the Fetch button to download model metadata and preview images from Civitai.'), 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.', + title: () => translate('onboarding.steps.download.title', {}, 'Download New Models'), + content: () => translate('onboarding.steps.download.content', {}, 'Use the Download button to download models directly from Civitai URLs.'), 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.', + title: () => translate('onboarding.steps.bulk.title', {}, 'Bulk Operations'), + content: () => translate('onboarding.steps.bulk.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: '#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.', + title: () => translate('onboarding.steps.searchOptions.title', {}, 'Search Options'), + content: () => translate('onboarding.steps.searchOptions.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.', + title: () => translate('onboarding.steps.filter.title', {}, 'Filter Models'), + content: () => translate('onboarding.steps.filter.content', {}, 'Use filters to narrow down models by base model type (SD1.5, SDXL, Flux, etc.) or by specific tags.'), position: 'bottom' }, { 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.', + title: () => translate('onboarding.steps.breadcrumb.title', {}, 'Breadcrumb Navigation'), + content: () => translate('onboarding.steps.breadcrumb.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.', + title: () => translate('onboarding.steps.modelCards.title', {}, 'Model Cards'), + content: () => translate('onboarding.steps.modelCards.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: '20%', left: '50%' } }, { target: '.card-grid', - title: 'Context Menu', - content: 'Right-click any model card for a context menu with additional actions.', + title: () => translate('onboarding.steps.contextMenu.title', {}, 'Context Menu'), + content: () => translate('onboarding.steps.contextMenu.content', {}, 'Right-click any model card for a context menu with additional actions.'), position: 'top', customPosition: { top: '20%', left: '50%' } } @@ -112,7 +114,7 @@ export class OnboardingManager { modal.className = 'language-selection-modal'; modal.innerHTML = `
-

Welcome to LoRA Manager

+

${translate('onboarding.languageSelection.title', {}, 'Welcome to LoRA Manager')}

Choose Your Language / 选择语言 / 言語を選択

${this.languages.map(lang => ` @@ -125,8 +127,8 @@ export class OnboardingManager { `).join('')}
- - + +
`; @@ -201,6 +203,7 @@ export class OnboardingManager { } } catch (error) { console.error('Failed to change language:', error); + showToast('onboarding.languageSelection.changeFailed', { message: error.message }, 'error'); } } @@ -266,17 +269,17 @@ export class OnboardingManager { // Update popup content this.popup.innerHTML = ` -

${step.title}

-

${step.content}

+

${typeof step.title === 'function' ? step.title() : step.title}

+

${typeof step.content === 'function' ? step.content() : step.content}

${stepIndex + 1} / ${this.steps.length}
- - ${stepIndex > 0 ? '' : ''} + + ${stepIndex > 0 ? `` : ''}
diff --git a/templates/components/folder_sidebar.html b/templates/components/folder_sidebar.html index f5ea6c33..9fdbc3fe 100644 --- a/templates/components/folder_sidebar.html +++ b/templates/components/folder_sidebar.html @@ -12,7 +12,7 @@ -
From 25465803773bca7a11c3111d211d3a181bd38cf2 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 22:23:35 +0800 Subject: [PATCH 08/10] fix(localization): update French translations for "recipe" to ensure consistency in terminology --- locales/fr.json | 130 ++++++++++++++++++++++++------------------------ 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/locales/fr.json b/locales/fr.json index 15ed204b..9022dd8d 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -125,7 +125,7 @@ "appTitle": "LoRA Manager", "navigation": { "loras": "LoRAs", - "recipes": "Recettes", + "recipes": "Recipes", "checkpoints": "Checkpoints", "embeddings": "Embeddings", "statistics": "Statistiques" @@ -134,7 +134,7 @@ "placeholder": "Rechercher...", "placeholders": { "loras": "Rechercher des LoRAs...", - "recipes": "Rechercher des recettes...", + "recipes": "Rechercher des recipes...", "checkpoints": "Rechercher des checkpoints...", "embeddings": "Rechercher des embeddings..." }, @@ -146,7 +146,7 @@ "modelname": "Nom du modèle", "tags": "Tags", "creator": "Créateur", - "title": "Titre de la recette", + "title": "Titre de la recipe", "loraName": "Nom de fichier LoRA", "loraModel": "Nom du modèle LoRA" } @@ -332,7 +332,7 @@ "relinkCivitai": "Relier à nouveau à Civitai", "copySyntax": "Copier la syntaxe LoRA", "copyFilename": "Copier le nom de fichier du modèle", - "copyRecipeSyntax": "Copier la syntaxe de la recette", + "copyRecipeSyntax": "Copier la syntaxe de la recipe", "sendToWorkflowAppend": "Envoyer vers le workflow (Ajouter)", "sendToWorkflowReplace": "Envoyer vers le workflow (Remplacer)", "openExamples": "Ouvrir le dossier d'exemples", @@ -342,33 +342,33 @@ "moveToFolder": "Déplacer vers un dossier", "excludeModel": "Exclure le modèle", "deleteModel": "Supprimer le modèle", - "shareRecipe": "Partager la recette", + "shareRecipe": "Partager la recipe", "viewAllLoras": "Voir tous les LoRAs", "downloadMissingLoras": "Télécharger les LoRAs manquants", - "deleteRecipe": "Supprimer la recette" + "deleteRecipe": "Supprimer la recipe" } }, "recipes": { - "title": "Recettes LoRA", + "title": "LoRA Recipes", "controls": { "import": { "action": "Importer", - "title": "Importer une recette depuis une image ou une URL", + "title": "Importer une recipe depuis une image ou une URL", "urlLocalPath": "URL / Chemin local", "uploadImage": "Téléverser une image", - "urlSectionDescription": "Saisissez une URL d'image Civitai ou un chemin de fichier local pour l'importer comme recette.", + "urlSectionDescription": "Saisissez une URL d'image Civitai ou un chemin de fichier local pour l'importer comme recipe.", "imageUrlOrPath": "URL d'image ou chemin de fichier :", "urlPlaceholder": "https://civitai.com/images/... ou C:/chemin/vers/image.png", "fetchImage": "Récupérer l'image", - "uploadSectionDescription": "Téléversez une image avec des métadonnées LoRA pour l'importer comme recette.", + "uploadSectionDescription": "Téléversez une image avec des métadonnées LoRA pour l'importer comme recipe.", "selectImage": "Sélectionner une image", - "recipeName": "Nom de la recette", - "recipeNamePlaceholder": "Entrez le nom de la recette", + "recipeName": "Nom de la recipe", + "recipeNamePlaceholder": "Entrez le nom de la recipe", "tagsOptional": "Tags (optionnel)", "addTagPlaceholder": "Ajouter un tag", "addTag": "Ajouter", "noTagsAdded": "Aucun tag ajouté", - "lorasInRecipe": "LoRAs dans cette recette", + "lorasInRecipe": "LoRAs dans cette recipe", "downloadLocationPreview": "Aperçu de l'emplacement de téléchargement :", "useDefaultPath": "Utiliser le chemin par défaut", "useDefaultPathTooltip": "Lorsque activé, les fichiers sont automatiquement organisés selon les modèles de chemin configurés", @@ -378,14 +378,14 @@ "createNewFolder": "Créer un nouveau dossier", "root": "Racine", "browseFolders": "Parcourir les dossiers :", - "downloadAndSaveRecipe": "Télécharger et sauvegarder la recette", + "downloadAndSaveRecipe": "Télécharger et sauvegarder la recipe", "downloadMissingLoras": "Télécharger les LoRAs manquants", - "saveRecipe": "Sauvegarder la recette", + "saveRecipe": "Sauvegarder la recipe", "loraCountInfo": "({existing}/{total} dans la bibliothèque)", "processingInput": "Traitement de l'entrée...", "analyzingMetadata": "Analyse des métadonnées de l'image...", "downloadingLoras": "Téléchargement des LoRAs...", - "savingRecipe": "Sauvegarde de la recette...", + "savingRecipe": "Sauvegarde de la recipe...", "startingDownload": "Début du téléchargement pour le LoRA {current}/{total}", "deletedFromCivitai": "Supprimé de Civitai", "inLibrary": "Dans la bibliothèque", @@ -394,12 +394,12 @@ "earlyAccessEnds": "L'accès anticipé se termine le {date}.", "earlyAccess": "Accès anticipé", "verifyEarlyAccess": "Vérifiez que vous avez acheté l'accès anticipé avant de télécharger.", - "duplicateRecipesFound": "{count} recette(s) identique(s) trouvée(s) dans votre bibliothèque", - "duplicateRecipesDescription": "Ces recettes contiennent les mêmes LoRAs avec des poids identiques.", + "duplicateRecipesFound": "{count} recipe(s) identique(s) trouvée(s) dans votre bibliothèque", + "duplicateRecipesDescription": "Ces recipes contiennent les mêmes LoRAs avec des poids identiques.", "showDuplicates": "Afficher les doublons", "hideDuplicates": "Masquer les doublons", "loraCount": "{count} LoRAs", - "recipePreviewAlt": "Aperçu de la recette", + "recipePreviewAlt": "Aperçu de la recipe", "loraPreviewAlt": "Aperçu LoRA", "errors": { "selectImageFile": "Veuillez sélectionner un fichier image", @@ -408,7 +408,7 @@ } }, "refresh": { - "title": "Actualiser la liste des recettes" + "title": "Actualiser la liste des recipes" }, "filteredByLora": "Filtré par LoRA" }, @@ -419,20 +419,20 @@ }, "contextMenu": { "copyRecipe": { - "missingId": "Impossible de copier la recette : ID de recette manquant", - "failed": "Échec de la copie de la syntaxe de la recette" + "missingId": "Impossible de copier la recipe : ID de recipe manquant", + "failed": "Échec de la copie de la syntaxe de la recipe" }, "sendRecipe": { - "missingId": "Impossible d'envoyer la recette : ID de recette manquant", - "failed": "Échec de l'envoi de la recette vers le workflow" + "missingId": "Impossible d'envoyer la recipe : ID de recipe manquant", + "failed": "Échec de l'envoi de la recipe vers le workflow" }, "viewLoras": { - "missingId": "Impossible de voir les LoRAs : ID de recette manquant", - "noLorasFound": "Aucun LoRA trouvé dans cette recette", - "loadError": "Erreur lors du chargement des LoRAs de la recette : {message}" + "missingId": "Impossible de voir les LoRAs : ID de recipe manquant", + "noLorasFound": "Aucun LoRA trouvé dans cette recipe", + "loadError": "Erreur lors du chargement des LoRAs de la recipe : {message}" }, "downloadMissing": { - "missingId": "Impossible de télécharger les LoRAs : ID de recette manquant", + "missingId": "Impossible de télécharger les LoRAs : ID de recipe manquant", "noMissingLoras": "Aucun LoRA manquant à télécharger", "getInfoFailed": "Échec de l'obtention des informations pour les LoRAs manquants", "prepareError": "Erreur lors de la préparation des LoRAs pour le téléchargement : {message}" @@ -552,9 +552,9 @@ "message": "Êtes-vous sûr de vouloir exclure ce modèle ? Les modèles exclus n'apparaîtront pas dans les recherches ou listes de modèles." }, "deleteDuplicateRecipes": { - "title": "Supprimer les recettes dupliquées", - "message": "Êtes-vous sûr de vouloir supprimer les recettes dupliquées sélectionnées ?", - "countMessage": "recettes seront définitivement supprimées." + "title": "Supprimer les recipes dupliquées", + "message": "Êtes-vous sûr de vouloir supprimer les recipes dupliquées sélectionnées ?", + "countMessage": "recipes seront définitivement supprimées." }, "deleteDuplicateModels": { "title": "Supprimer les modèles dupliqués", @@ -685,12 +685,12 @@ "tabs": { "examples": "Exemples", "description": "Description du modèle", - "recipes": "Recettes" + "recipes": "Recipes" }, "loading": { "exampleImages": "Chargement des images d'exemple...", "description": "Chargement de la description du modèle...", - "recipes": "Chargement des recettes...", + "recipes": "Chargement des recipes...", "examples": "Chargement des exemples..." } } @@ -733,8 +733,8 @@ "message": "Scan et construction du cache embedding. Cela peut prendre quelques minutes..." }, "recipes": { - "title": "Initialisation du gestionnaire de recettes", - "message": "Chargement et traitement des recettes. Cela peut prendre quelques minutes..." + "title": "Initialisation du gestionnaire de recipes", + "message": "Chargement et traitement des recipes. Cela peut prendre quelques minutes..." }, "statistics": { "title": "Initialisation des statistiques", @@ -753,9 +753,9 @@ "alt": "Téléchargement Civitai" }, "recipes": { - "title": "Sauvegarder les recettes", - "description": "Créez des recettes pour sauvegarder vos combinaisons de modèles préférées pour une utilisation future.", - "alt": "Recettes" + "title": "Sauvegarder les recipes", + "description": "Créez des recipes pour sauvegarder vos combinaisons de modèles préférées pour une utilisation future.", + "alt": "Recipes" }, "filter": { "title": "Filtrage rapide", @@ -796,12 +796,12 @@ "loraAdded": "LoRA ajouté au workflow", "loraReplaced": "LoRA remplacé dans le workflow", "loraFailedToSend": "Échec de l'envoi du LoRA au workflow", - "recipeAdded": "Recette ajoutée au workflow", - "recipeReplaced": "Recette remplacée dans le workflow", - "recipeFailedToSend": "Échec de l'envoi de la recette au workflow" + "recipeAdded": "Recipe ajoutée au workflow", + "recipeReplaced": "Recipe remplacée dans le workflow", + "recipeFailedToSend": "Échec de l'envoi de la recipe au workflow" }, "nodeSelector": { - "recipe": "Recette", + "recipe": "Recipe", "lora": "LoRA", "replace": "Remplacer", "append": "Ajouter", @@ -835,7 +835,7 @@ "general": "Général", "troubleshooting": "Dépannage", "modelManagement": "Gestion des modèles", - "recipes": "Recettes", + "recipes": "Recipes", "settings": "Paramètres & Configuration", "extensions": "Extensions", "newBadge": "NOUVEAU" @@ -927,42 +927,42 @@ "downloadCompleted": "Téléchargement terminé avec succès" }, "recipes": { - "fetchFailed": "Échec de la récupération des recettes : {message}", + "fetchFailed": "Échec de la récupération des recipes : {message}", "reloadFailed": "Échec du rechargement des {modelType}s : {message}", "loadFailed": "Échec du chargement des {modelType}s : {message}", "refreshComplete": "Actualisation terminée", - "refreshFailed": "Échec de l'actualisation des recettes : {message}", - "updateFailed": "Échec de la mise à jour de la recette : {error}", - "updateError": "Erreur lors de la mise à jour de la recette : {message}", - "nameSaved": "Recette \"{name}\" sauvegardée avec succès", - "nameUpdated": "Nom de la recette mis à jour avec succès", - "tagsUpdated": "Tags de la recette mis à jour avec succès", + "refreshFailed": "Échec de l'actualisation des recipes : {message}", + "updateFailed": "Échec de la mise à jour de la recipe : {error}", + "updateError": "Erreur lors de la mise à jour de la recipe : {message}", + "nameSaved": "Recipe \"{name}\" sauvegardée avec succès", + "nameUpdated": "Nom de la recipe mis à jour avec succès", + "tagsUpdated": "Tags de la recipe mis à jour avec succès", "sourceUrlUpdated": "URL source mise à jour avec succès", - "noRecipeId": "Aucun ID de recette disponible", - "copyFailed": "Erreur lors de la copie de la syntaxe de la recette : {message}", + "noRecipeId": "Aucun ID de recipe disponible", + "copyFailed": "Erreur lors de la copie de la syntaxe de la recipe : {message}", "noMissingLoras": "Aucun LoRA manquant à télécharger", "missingLorasInfoFailed": "Échec de l'obtention des informations pour les LoRAs manquants", "preparingForDownloadFailed": "Erreur lors de la préparation des LoRAs pour le téléchargement", "enterLoraName": "Veuillez entrer un nom ou une syntaxe LoRA", "reconnectedSuccessfully": "LoRA reconnecté avec succès", "reconnectFailed": "Erreur lors de la reconnexion du LoRA : {message}", - "cannotSend": "Impossible d'envoyer la recette : ID de recette manquant", - "sendFailed": "Échec de l'envoi de la recette vers le workflow", - "sendError": "Erreur lors de l'envoi de la recette vers le workflow", - "cannotDelete": "Impossible de supprimer la recette : ID de recette manquant", + "cannotSend": "Impossible d'envoyer la recipe : ID de recipe manquant", + "sendFailed": "Échec de l'envoi de la recipe vers le workflow", + "sendError": "Erreur lors de l'envoi de la recipe vers le workflow", + "cannotDelete": "Impossible de supprimer la recipe : ID de recipe manquant", "deleteConfirmationError": "Erreur lors de l'affichage de la confirmation de suppression", - "deletedSuccessfully": "Recette supprimée avec succès", - "deleteFailed": "Erreur lors de la suppression de la recette : {message}", - "cannotShare": "Impossible de partager la recette : ID de recette manquant", - "preparingForSharing": "Préparation de la recette pour le partage...", - "downloadStarted": "Téléchargement de la recette démarré", - "shareError": "Erreur lors du partage de la recette : {message}", - "sharePreparationError": "Erreur lors de la préparation de la recette pour le partage", + "deletedSuccessfully": "Recipe supprimée avec succès", + "deleteFailed": "Erreur lors de la suppression de la recipe : {message}", + "cannotShare": "Impossible de partager la recipe : ID de recipe manquant", + "preparingForSharing": "Préparation de la recipe pour le partage...", + "downloadStarted": "Téléchargement de la recipe démarré", + "shareError": "Erreur lors du partage de la recipe : {message}", + "sharePreparationError": "Erreur lors de la préparation de la recipe pour le partage", "selectImageFirst": "Veuillez d'abord sélectionner une image", - "enterRecipeName": "Veuillez entrer un nom de recette", + "enterRecipeName": "Veuillez entrer un nom de recipe", "processingError": "Erreur de traitement : {message}", "folderBrowserError": "Erreur lors du chargement du navigateur de dossiers : {message}", - "recipeSaveFailed": "Échec de la sauvegarde de la recette : {error}", + "recipeSaveFailed": "Échec de la sauvegarde de la recipe : {error}", "importFailed": "Échec de l'importation : {message}", "folderTreeFailed": "Échec du chargement de l'arborescence des dossiers", "folderTreeError": "Erreur lors du chargement de l'arborescence des dossiers" From f041f4a11434b97e18382055fcfa97c4651fd594 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 22:48:29 +0800 Subject: [PATCH 09/10] feat(onboarding): prevent onboarding from starting if version-mismatch banner is visible --- static/js/core.js | 5 ++++- static/js/managers/BannerService.js | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/static/js/core.js b/static/js/core.js index 36376d89..1a250aca 100644 --- a/static/js/core.js +++ b/static/js/core.js @@ -68,7 +68,10 @@ export class AppCore { // Start onboarding if needed (after everything is initialized) setTimeout(() => { - onboardingManager.start(); + // Do not show onboarding if version-mismatch banner is visible + if (!bannerService.isBannerVisible('version-mismatch')) { + onboardingManager.start(); + } }, 1000); // Small delay to ensure all elements are rendered // Return the core instance for chaining diff --git a/static/js/managers/BannerService.js b/static/js/managers/BannerService.js index c2226fce..808bfb12 100644 --- a/static/js/managers/BannerService.js +++ b/static/js/managers/BannerService.js @@ -171,6 +171,16 @@ class BannerService { } } + /** + * Check if a banner is currently rendered and visible + * @param {string} bannerId + * @returns {boolean} + */ + isBannerVisible(bannerId) { + const el = document.querySelector(`[data-banner-id="${bannerId}"]`); + return !!el && el.offsetParent !== null; + } + /** * Update container visibility based on active banners */ From 192bc237bfdd115a5c0a5e43932b2e46bb950368 Mon Sep 17 00:00:00 2001 From: Will Miao <13051207myq@gmail.com> Date: Wed, 3 Sep 2025 23:04:06 +0800 Subject: [PATCH 10/10] fix(onboarding): update language selection button text and remove skip option from translations --- locales/de.json | 1 - locales/en.json | 1 - locales/es.json | 1 - locales/fr.json | 1 - locales/ja.json | 1 - locales/ko.json | 1 - locales/ru.json | 1 - locales/zh-CN.json | 1 - locales/zh-TW.json | 1 - static/js/managers/OnboardingManager.js | 16 ++++++---------- 10 files changed, 6 insertions(+), 19 deletions(-) diff --git a/locales/de.json b/locales/de.json index f2085f98..064b7306 100644 --- a/locales/de.json +++ b/locales/de.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "Willkommen beim LoRA Manager", - "skip": "Überspringen", "continue": "Weiter", "changeFailed": "Fehler beim Ändern der Sprache: {message}" }, diff --git a/locales/en.json b/locales/en.json index 6076f0af..c518574a 100644 --- a/locales/en.json +++ b/locales/en.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "Welcome to LoRA Manager", - "skip": "Skip", "continue": "Continue", "changeFailed": "Failed to change language: {message}" }, diff --git a/locales/es.json b/locales/es.json index 471eb111..db682c59 100644 --- a/locales/es.json +++ b/locales/es.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "Bienvenido a LoRA Manager", - "skip": "Saltar", "continue": "Continuar", "changeFailed": "Error al cambiar el idioma: {message}" }, diff --git a/locales/fr.json b/locales/fr.json index 9022dd8d..5739e7cf 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "Bienvenue dans LoRA Manager", - "skip": "Passer", "continue": "Continuer", "changeFailed": "Échec du changement de langue : {message}" }, diff --git a/locales/ja.json b/locales/ja.json index 99b63861..d1fc6bd5 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "LoRA Managerへようこそ", - "skip": "スキップ", "continue": "続行", "changeFailed": "言語の変更に失敗しました:{message}" }, diff --git a/locales/ko.json b/locales/ko.json index 51abf06b..5d1b5447 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "LoRA Manager에 오신 것을 환영합니다", - "skip": "건너뛰기", "continue": "계속", "changeFailed": "언어 변경 실패: {message}" }, diff --git a/locales/ru.json b/locales/ru.json index 73e9afb6..d3d13607 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "Добро пожаловать в LoRA Manager", - "skip": "Пропустить", "continue": "Продолжить", "changeFailed": "Не удалось изменить язык: {message}" }, diff --git a/locales/zh-CN.json b/locales/zh-CN.json index f1426183..c843ed9d 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "欢迎使用 LoRA 管理器", - "skip": "跳过", "continue": "继续", "changeFailed": "切换语言失败:{message}" }, diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 9047526e..c04f39ae 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -43,7 +43,6 @@ "onboarding": { "languageSelection": { "title": "歡迎使用 LoRA 管理器", - "skip": "跳過", "continue": "繼續", "changeFailed": "切換語言失敗:{message}" }, diff --git a/static/js/managers/OnboardingManager.js b/static/js/managers/OnboardingManager.js index e60c5217..ccbb971f 100644 --- a/static/js/managers/OnboardingManager.js +++ b/static/js/managers/OnboardingManager.js @@ -127,7 +127,7 @@ export class OnboardingManager { `).join('')}
- +
@@ -231,13 +231,6 @@ export class OnboardingManager { 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 @@ -412,8 +405,10 @@ export class OnboardingManager { // Clear overlay mask clearOverlayMask() { - this.overlay.style.mask = 'none'; - this.overlay.style.webkitMask = 'none'; + if (this.overlay) { + this.overlay.style.mask = 'none'; + this.overlay.style.webkitMask = 'none'; + } const maskSvg = document.getElementById('onboarding-mask'); if (maskSvg) { @@ -482,6 +477,7 @@ export class OnboardingManager { localStorage.removeItem('lora_manager_onboarding_completed'); localStorage.removeItem('lora_manager_onboarding_skipped'); localStorage.removeItem('lora_manager_onboarding_language_set'); + localStorage.setItem('lora_manager_version_info', '0.8.30-2546581'); } }