diff --git a/locales/de.json b/locales/de.json index 371a9217..9ec6c77c 100644 --- a/locales/de.json +++ b/locales/de.json @@ -435,7 +435,10 @@ }, "updates": { "title": "Nur Modelle mit verfügbaren Updates anzeigen", - "action": "Updates" + "action": "Updates", + "menuLabel": "Weitere Update-Optionen anzeigen", + "check": "Updates prüfen", + "checkTooltip": "Die Aktualisierungssuche kann einige Zeit dauern." } }, "bulkOperations": { diff --git a/locales/en.json b/locales/en.json index c2971cea..1eaee6bb 100644 --- a/locales/en.json +++ b/locales/en.json @@ -434,7 +434,10 @@ }, "updates": { "title": "Show models with updates available", - "action": "Updates" + "action": "Updates", + "menuLabel": "Show update options", + "check": "Check updates", + "checkTooltip": "Checking updates may take a while." } }, "bulkOperations": { diff --git a/locales/es.json b/locales/es.json index 94a9930d..c98e3d80 100644 --- a/locales/es.json +++ b/locales/es.json @@ -434,7 +434,10 @@ }, "updates": { "title": "Mostrar solo modelos con actualizaciones disponibles", - "action": "Actualizaciones" + "action": "Actualizaciones", + "menuLabel": "Mostrar opciones de actualización", + "check": "Buscar actualizaciones", + "checkTooltip": "Comprobar actualizaciones puede tardar." } }, "bulkOperations": { diff --git a/locales/fr.json b/locales/fr.json index fb951729..84920ec8 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -434,7 +434,10 @@ }, "updates": { "title": "Afficher uniquement les modèles avec des mises à jour disponibles", - "action": "Mises à jour" + "action": "Mises à jour", + "menuLabel": "Afficher les options de mise à jour", + "check": "Rechercher des mises à jour", + "checkTooltip": "La vérification peut prendre du temps." } }, "bulkOperations": { diff --git a/locales/he.json b/locales/he.json index 158be995..a4c43568 100644 --- a/locales/he.json +++ b/locales/he.json @@ -434,7 +434,10 @@ }, "updates": { "title": "הצג רק דגמים עם עדכונים זמינים", - "action": "עדכונים" + "action": "עדכונים", + "menuLabel": "הצגת אפשרויות עדכון", + "check": "בדוק עדכונים", + "checkTooltip": "בדיקת עדכונים עלולה לקחת זמן." } }, "bulkOperations": { diff --git a/locales/ja.json b/locales/ja.json index e95c2247..7555f526 100644 --- a/locales/ja.json +++ b/locales/ja.json @@ -434,7 +434,10 @@ }, "updates": { "title": "アップデート可能なモデルのみ表示", - "action": "アップデート" + "action": "アップデート", + "menuLabel": "更新オプションを表示", + "check": "アップデートを確認", + "checkTooltip": "確認には時間がかかる場合があります。" } }, "bulkOperations": { diff --git a/locales/ko.json b/locales/ko.json index eafdf67b..add8a0be 100644 --- a/locales/ko.json +++ b/locales/ko.json @@ -434,7 +434,10 @@ }, "updates": { "title": "업데이트 가능한 모델만 표시", - "action": "업데이트" + "action": "업데이트", + "menuLabel": "업데이트 옵션 표시", + "check": "업데이트 확인", + "checkTooltip": "업데이트 확인에는 시간이 걸릴 수 있습니다." } }, "bulkOperations": { diff --git a/locales/ru.json b/locales/ru.json index 7cf09afc..6036140d 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -434,7 +434,10 @@ }, "updates": { "title": "Показывать только модели с доступными обновлениями", - "action": "Обновления" + "action": "Обновления", + "menuLabel": "Показать параметры обновления", + "check": "Проверить обновления", + "checkTooltip": "Проверка может занять время." } }, "bulkOperations": { diff --git a/locales/zh-CN.json b/locales/zh-CN.json index f2769449..2440942a 100644 --- a/locales/zh-CN.json +++ b/locales/zh-CN.json @@ -434,7 +434,10 @@ }, "updates": { "title": "仅显示可用更新的模型", - "action": "更新" + "action": "更新", + "menuLabel": "显示更新选项", + "check": "检查更新", + "checkTooltip": "检查更新可能耗时。" } }, "bulkOperations": { diff --git a/locales/zh-TW.json b/locales/zh-TW.json index 892b2153..02a2045e 100644 --- a/locales/zh-TW.json +++ b/locales/zh-TW.json @@ -434,7 +434,10 @@ }, "updates": { "title": "僅顯示可用更新的模型", - "action": "更新" + "action": "更新", + "menuLabel": "顯示更新選項", + "check": "檢查更新", + "checkTooltip": "檢查更新可能耗時。" } }, "bulkOperations": { diff --git a/static/css/layout.css b/static/css/layout.css index 5f862b00..91543848 100644 --- a/static/css/layout.css +++ b/static/css/layout.css @@ -104,6 +104,19 @@ opacity: 1; } +.control-group button:disabled { + cursor: not-allowed; + opacity: 0.6; + pointer-events: none; +} + +.control-group button.loading, +.dropdown-toggle.loading { + cursor: wait; + opacity: 0.7; + pointer-events: none; +} + /* Controls */ .control-group button.favorite-filter, .control-group button.update-filter { @@ -135,6 +148,16 @@ color: white; } +.update-filter-group .dropdown-main.update-filter.active + .dropdown-toggle { + background: var(--lora-accent); + border-color: var(--lora-accent); + color: white; +} + +.update-filter-group .dropdown-main.update-filter.active + .dropdown-toggle i { + color: inherit; +} + /* Active state for buttons that can be toggled */ .control-group button.active { background: var(--lora-accent); @@ -322,6 +345,9 @@ border-top-left-radius: 0; border-bottom-left-radius: 0; padding: 0 !important; + display: flex; + align-items: center; + justify-content: center; } .dropdown-menu { @@ -354,6 +380,12 @@ transition: background-color 0.2s ease; } +.dropdown-item.disabled { + cursor: default; + opacity: 0.6; + pointer-events: none; +} + .dropdown-item:hover { background-color: oklch(var(--lora-accent) / 0.1); } diff --git a/static/js/components/ContextMenu/GlobalContextMenu.js b/static/js/components/ContextMenu/GlobalContextMenu.js index a075c21f..2ae3bb5d 100644 --- a/static/js/components/ContextMenu/GlobalContextMenu.js +++ b/static/js/components/ContextMenu/GlobalContextMenu.js @@ -1,9 +1,7 @@ import { BaseContextMenu } from './BaseContextMenu.js'; import { showToast } from '../../utils/uiHelpers.js'; import { state } from '../../state/index.js'; -import { translate } from '../../utils/i18nHelpers.js'; -import { getCompleteApiConfig, getCurrentModelType } from '../../api/apiConfig.js'; -import { resetAndReload } from '../../api/modelApiFactory.js'; +import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; export class GlobalContextMenu extends BaseContextMenu { constructor() { @@ -116,68 +114,23 @@ export class GlobalContextMenu extends BaseContextMenu { return; } - const modelType = getCurrentModelType(); - const apiConfig = getCompleteApiConfig(modelType); - - if (!apiConfig?.endpoints?.refreshUpdates) { - console.warn('Refresh updates endpoint not configured for model type:', modelType); - return; - } - this._updateCheckInProgress = true; menuItem?.classList.add('disabled'); - const displayName = apiConfig.config?.displayName ?? 'Model'; - const loadingMessage = translate( - 'globalContextMenu.checkModelUpdates.loading', - { type: displayName }, - `Checking for ${displayName} updates...` - ); - - state.loadingManager?.showSimpleLoading?.(loadingMessage); - try { - const response = await fetch(apiConfig.endpoints.refreshUpdates, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ force: false }) + await performModelUpdateCheck({ + onComplete: () => { + menuItem?.classList.remove('disabled'); + this._updateCheckInProgress = false; + } }); - - let payload = {}; - try { - payload = await response.json(); - } catch { - payload = {}; - } - - if (!response.ok || payload.success !== true) { - const errorMessage = payload?.error || response.statusText || 'Unknown error'; - throw new Error(errorMessage); - } - - const records = Array.isArray(payload.records) ? payload.records : []; - - if (records.length > 0) { - showToast('globalContextMenu.checkModelUpdates.success', { count: records.length, type: displayName }, 'success'); - } else { - showToast('globalContextMenu.checkModelUpdates.none', { type: displayName }, 'info'); - } - - await resetAndReload(false); } catch (error) { - console.error('Error checking model updates:', error); - showToast( - 'globalContextMenu.checkModelUpdates.error', - { message: error?.message ?? 'Unknown error', type: displayName }, - 'error' - ); + console.error('Failed to check model updates:', error); } finally { - state.loadingManager?.hide?.(); - if (typeof state.loadingManager?.restoreProgressBar === 'function') { - state.loadingManager.restoreProgressBar(); + if (this._updateCheckInProgress) { + this._updateCheckInProgress = false; + menuItem?.classList.remove('disabled'); } - menuItem?.classList.remove('disabled'); - this._updateCheckInProgress = false; } } } diff --git a/static/js/components/controls/PageControls.js b/static/js/components/controls/PageControls.js index b3b8b9a3..e83112bd 100644 --- a/static/js/components/controls/PageControls.js +++ b/static/js/components/controls/PageControls.js @@ -2,6 +2,7 @@ import { getCurrentPageState, setCurrentPageType } from '../../state/index.js'; import { getStorageItem, setStorageItem, getSessionItem, setSessionItem } from '../../utils/storageHelpers.js'; import { showToast } from '../../utils/uiHelpers.js'; +import { performModelUpdateCheck } from '../../utils/updateCheckHelpers.js'; import { sidebarManager } from '../SidebarManager.js'; /** @@ -26,7 +27,9 @@ export class PageControls { // Use global sidebar manager this.sidebarManager = sidebarManager; - + + this._updateCheckInProgress = false; + // Initialize event listeners this.initEventListeners(); @@ -156,7 +159,15 @@ export class PageControls { document.querySelector('.dropdown-group.active')?.classList.remove('active'); }); } - + + const checkUpdatesOption = document.getElementById('checkUpdatesMenuItem'); + if (checkUpdatesOption) { + checkUpdatesOption.addEventListener('click', async (e) => { + e.stopPropagation(); + await this.handleCheckModelUpdates(e.currentTarget); + }); + } + // Close dropdowns when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.dropdown-group')) { @@ -166,7 +177,73 @@ export class PageControls { } }); } - + + async handleCheckModelUpdates(menuItem) { + if (this._updateCheckInProgress) { + return; + } + + const updateFilterBtn = document.getElementById('updateFilterBtn'); + const dropdownToggle = document.getElementById('updateFilterMenuToggle'); + const dropdownGroup = menuItem?.closest('.dropdown-group'); + const iconElement = updateFilterBtn?.querySelector('i'); + + const setLoadingState = (isLoading) => { + if (updateFilterBtn) { + updateFilterBtn.disabled = isLoading; + updateFilterBtn.classList.toggle('loading', isLoading); + updateFilterBtn.setAttribute('aria-busy', isLoading ? 'true' : 'false'); + + if (iconElement) { + if (isLoading) { + if (!iconElement.dataset.originalClass) { + iconElement.dataset.originalClass = iconElement.className; + } + iconElement.className = 'fas fa-spinner fa-spin'; + } else { + const originalClass = iconElement.dataset.originalClass; + if (originalClass) { + iconElement.className = originalClass; + delete iconElement.dataset.originalClass; + } else { + iconElement.classList.remove('fa-spinner', 'fa-spin'); + if (!iconElement.classList.contains('fa-exclamation-circle')) { + iconElement.classList.add('fa-exclamation-circle'); + } + } + } + } + } + + if (dropdownToggle) { + dropdownToggle.disabled = isLoading; + dropdownToggle.classList.toggle('loading', isLoading); + } + + if (menuItem) { + menuItem.classList.toggle('disabled', isLoading); + if (isLoading) { + menuItem.setAttribute('aria-disabled', 'true'); + } else { + menuItem.removeAttribute('aria-disabled'); + } + } + }; + + this._updateCheckInProgress = true; + setLoadingState(true); + + try { + await performModelUpdateCheck(); + } catch (error) { + console.error('Failed to check model updates:', error); + } finally { + this._updateCheckInProgress = false; + setLoadingState(false); + dropdownGroup?.classList.remove('active'); + } + } + /** * Initialize page-specific event listeners */ diff --git a/static/js/utils/updateCheckHelpers.js b/static/js/utils/updateCheckHelpers.js new file mode 100644 index 00000000..408a6fe0 --- /dev/null +++ b/static/js/utils/updateCheckHelpers.js @@ -0,0 +1,85 @@ +import { state } from '../state/index.js'; +import { translate } from './i18nHelpers.js'; +import { showToast } from './uiHelpers.js'; +import { getCompleteApiConfig, getCurrentModelType } from '../api/apiConfig.js'; +import { resetAndReload } from '../api/modelApiFactory.js'; + +/** + * Perform a model update check using the shared backend endpoint. + * @param {Object} [options] + * @param {Function} [options.onStart] - Callback invoked before the request is sent. + * @param {Function} [options.onComplete] - Callback invoked after the request settles. + * @returns {Promise<{status: 'success' | 'error' | 'unsupported', displayName: string, records: Array, error: Error | null}>} + */ +export async function performModelUpdateCheck({ onStart, onComplete } = {}) { + const modelType = getCurrentModelType(); + const apiConfig = getCompleteApiConfig(modelType); + const displayName = apiConfig?.config?.displayName ?? 'Model'; + + if (!apiConfig?.endpoints?.refreshUpdates) { + console.warn('Refresh updates endpoint not configured for model type:', modelType); + onComplete?.({ status: 'unsupported', displayName, records: [], error: null }); + return { status: 'unsupported', displayName, records: [], error: null }; + } + + const loadingMessage = translate( + 'globalContextMenu.checkModelUpdates.loading', + { type: displayName }, + `Checking for ${displayName} updates...` + ); + + onStart?.({ displayName, loadingMessage }); + + state.loadingManager?.showSimpleLoading?.(loadingMessage); + + let status = 'success'; + let records = []; + let error = null; + + try { + const response = await fetch(apiConfig.endpoints.refreshUpdates, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ force: false }) + }); + + let payload = {}; + try { + payload = await response.json(); + } catch { + payload = {}; + } + + if (!response.ok || payload.success !== true) { + const errorMessage = payload?.error || response.statusText || 'Unknown error'; + throw new Error(errorMessage); + } + + records = Array.isArray(payload.records) ? payload.records : []; + + if (records.length > 0) { + showToast('globalContextMenu.checkModelUpdates.success', { count: records.length, type: displayName }, 'success'); + } else { + showToast('globalContextMenu.checkModelUpdates.none', { type: displayName }, 'info'); + } + + await resetAndReload(false); + } catch (err) { + status = 'error'; + error = err instanceof Error ? err : new Error(String(err)); + console.error('Error checking model updates:', error); + showToast( + 'globalContextMenu.checkModelUpdates.error', + { message: error?.message ?? 'Unknown error', type: displayName }, + 'error' + ); + } finally { + state.loadingManager?.hide?.(); + if (typeof state.loadingManager?.restoreProgressBar === 'function') { + state.loadingManager.restoreProgressBar(); + } + onComplete?.({ status, displayName, records, error }); + } + + return { status, displayName, records, error }; +} diff --git a/templates/components/controls.html b/templates/components/controls.html index 596ced21..df8dcd0c 100644 --- a/templates/components/controls.html +++ b/templates/components/controls.html @@ -56,10 +56,18 @@ {{ t('loras.controls.favorites.action') }} -