feat(updates): improve check updates confirmation

This commit is contained in:
pixelpaws
2025-11-03 22:36:57 +08:00
parent d73903e82e
commit 6dea9a76bc
15 changed files with 231 additions and 4 deletions

View File

@@ -731,6 +731,12 @@
"countMessage": "Modelle werden dauerhaft gelöscht.", "countMessage": "Modelle werden dauerhaft gelöscht.",
"action": "Alle löschen" "action": "Alle löschen"
}, },
"checkUpdates": {
"title": "Alle {typePlural} auf Updates prüfen?",
"message": "Damit werden alle {typePlural} in deiner Bibliothek auf Updates geprüft. Bei großen Sammlungen kann das etwas länger dauern.",
"tip": "Du möchtest in Etappen prüfen? Wechsle in den Sammelmodus, wähle die benötigten Modelle aus und nutze anschließend \"Auswahl auf Updates prüfen\".",
"action": "Alles prüfen"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Tags zu mehreren Modellen hinzufügen", "title": "Tags zu mehreren Modellen hinzufügen",
"description": "Tags hinzufügen zu", "description": "Tags hinzufügen zu",

View File

@@ -730,6 +730,12 @@
"countMessage": "models will be permanently deleted.", "countMessage": "models will be permanently deleted.",
"action": "Delete All" "action": "Delete All"
}, },
"checkUpdates": {
"title": "Check updates for all {typePlural}?",
"message": "This checks every {typePlural} in your library for updates. Large collections may take a little longer.",
"tip": "To work in smaller batches, switch to bulk mode, choose the ones you need, then use \"Check Updates for Selected\".",
"action": "Check All"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Add Tags to Multiple Models", "title": "Add Tags to Multiple Models",
"description": "Add tags to", "description": "Add tags to",

View File

@@ -730,6 +730,12 @@
"countMessage": "modelos serán eliminados permanentemente.", "countMessage": "modelos serán eliminados permanentemente.",
"action": "Eliminar todo" "action": "Eliminar todo"
}, },
"checkUpdates": {
"title": "¿Comprobar actualizaciones para todos los {typePlural}?",
"message": "Esto comprobará las actualizaciones de todos los {typePlural} de tu biblioteca. En colecciones grandes puede tardar un poco más.",
"tip": "¿Quieres hacerlo por partes? Activa el modo por lotes, selecciona los modelos que necesites y usa \"Comprobar actualizaciones para la selección\".",
"action": "Comprobar todo"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Añadir etiquetas a múltiples modelos", "title": "Añadir etiquetas a múltiples modelos",
"description": "Añadir etiquetas a", "description": "Añadir etiquetas a",

View File

@@ -730,6 +730,12 @@
"countMessage": "modèles seront définitivement supprimés.", "countMessage": "modèles seront définitivement supprimés.",
"action": "Tout supprimer" "action": "Tout supprimer"
}, },
"checkUpdates": {
"title": "Vérifier les mises à jour pour tous les {typePlural} ?",
"message": "Cette action vérifie les mises à jour pour tous les {typePlural} de votre bibliothèque. Les grandes collections peuvent prendre un peu plus de temps.",
"tip": "Besoin de procéder par étapes ? Passez en mode lot, sélectionnez les modèles souhaités puis utilisez \"Vérifier les mises à jour pour la sélection\".",
"action": "Tout vérifier"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Ajouter des tags à plusieurs modèles", "title": "Ajouter des tags à plusieurs modèles",
"description": "Ajouter des tags à", "description": "Ajouter des tags à",

View File

@@ -730,6 +730,12 @@
"countMessage": "מודלים יימחקו לצמיתות.", "countMessage": "מודלים יימחקו לצמיתות.",
"action": "מחק הכל" "action": "מחק הכל"
}, },
"checkUpdates": {
"title": "לבדוק עדכונים לכל ה-{typePlural}?",
"message": "הפעולה תבדוק עדכונים עבור כל ה-{typePlural} בספרייה שלך. באוספים גדולים זה עלול לקחת מעט יותר זמן.",
"tip": "רוצים לחלק למנות קטנות? עברו למצב קבוצתי, בחרו את המודלים הדרושים ואז השתמשו ב\"בדוק עדכונים לנבחרים\".",
"action": "בדוק הכל"
},
"bulkAddTags": { "bulkAddTags": {
"title": "הוסף תגיות למספר מודלים", "title": "הוסף תגיות למספר מודלים",
"description": "הוסף תגיות ל-", "description": "הוסף תגיות ל-",

View File

@@ -730,6 +730,12 @@
"countMessage": "モデルが完全に削除されます。", "countMessage": "モデルが完全に削除されます。",
"action": "すべて削除" "action": "すべて削除"
}, },
"checkUpdates": {
"title": "すべての{type}の更新を確認しますか?",
"message": "ライブラリ内のすべての{type}で更新を確認します。コレクションが大きい場合は時間がかかることがあります。",
"tip": "少しずつ確認したい場合はバルクモードに切り替え、必要なモデルを選んで「選択項目の更新を確認」を使ってください。",
"action": "すべて確認"
},
"bulkAddTags": { "bulkAddTags": {
"title": "複数モデルにタグを追加", "title": "複数モデルにタグを追加",
"description": "タグを追加するモデル:", "description": "タグを追加するモデル:",

View File

@@ -730,6 +730,12 @@
"countMessage": "개의 모델이 영구적으로 삭제됩니다.", "countMessage": "개의 모델이 영구적으로 삭제됩니다.",
"action": "모두 삭제" "action": "모두 삭제"
}, },
"checkUpdates": {
"title": "{type} 전체 업데이트를 확인할까요?",
"message": "라이브러리에 있는 모든 {type}의 업데이트를 확인합니다. 컬렉션이 클수록 시간이 조금 더 걸릴 수 있습니다.",
"tip": "나눠서 진행하고 싶다면 벌크 모드로 전환해 필요한 모델만 선택한 뒤 \"선택 항목 업데이트 확인\"을 사용하세요.",
"action": "전체 확인"
},
"bulkAddTags": { "bulkAddTags": {
"title": "여러 모델에 태그 추가", "title": "여러 모델에 태그 추가",
"description": "다음에 태그를 추가합니다:", "description": "다음에 태그를 추가합니다:",

View File

@@ -730,6 +730,12 @@
"countMessage": "моделей будут удалены навсегда.", "countMessage": "моделей будут удалены навсегда.",
"action": "Удалить все" "action": "Удалить все"
}, },
"checkUpdates": {
"title": "Проверить обновления для всех {typePlural}?",
"message": "Будут проверены обновления для всех {typePlural} в вашей библиотеке. Для больших коллекций это может занять немного больше времени.",
"tip": "Хотите проверять по частям? Переключитесь в массовый режим, выберите нужные модели и используйте \"Проверить обновления для выбранных\".",
"action": "Проверить всё"
},
"bulkAddTags": { "bulkAddTags": {
"title": "Добавить теги к нескольким моделям", "title": "Добавить теги к нескольким моделям",
"description": "Добавить теги к", "description": "Добавить теги к",

View File

@@ -730,6 +730,12 @@
"countMessage": "模型将被永久删除。", "countMessage": "模型将被永久删除。",
"action": "全部删除" "action": "全部删除"
}, },
"checkUpdates": {
"title": "检查所有 {type} 的更新?",
"message": "这会为库中的每个 {type} 检查更新,大型集合可能需要一些时间。",
"tip": "想分批进行?切换到批量模式,选中需要的模型,然后使用“检查所选更新”。",
"action": "检查全部"
},
"bulkAddTags": { "bulkAddTags": {
"title": "批量添加标签", "title": "批量添加标签",
"description": "为多个模型添加标签", "description": "为多个模型添加标签",

View File

@@ -730,6 +730,12 @@
"countMessage": "模型將被永久刪除。", "countMessage": "模型將被永久刪除。",
"action": "全部刪除" "action": "全部刪除"
}, },
"checkUpdates": {
"title": "要檢查所有 {type} 的更新嗎?",
"message": "這會為資料庫中的每個 {type} 檢查更新,大型收藏可能會花上一些時間。",
"tip": "想分批處理?切換到批次模式,選擇需要的模型,然後使用「檢查所選更新」。",
"action": "全部檢查"
},
"bulkAddTags": { "bulkAddTags": {
"title": "新增標籤到多個模型", "title": "新增標籤到多個模型",
"description": "新增標籤到", "description": "新增標籤到",

View File

@@ -235,13 +235,22 @@ export class PageControls {
this._updateCheckInProgress = true; this._updateCheckInProgress = true;
setLoadingState(true); setLoadingState(true);
const handleComplete = () => {
this._updateCheckInProgress = false;
setLoadingState(false);
};
try { try {
await performModelUpdateCheck(); await performModelUpdateCheck({
onComplete: handleComplete,
});
} catch (error) { } catch (error) {
console.error('Failed to check model updates:', error); console.error('Failed to check model updates:', error);
} finally { } finally {
this._updateCheckInProgress = false; if (this._updateCheckInProgress) {
setLoadingState(false); this._updateCheckInProgress = false;
setLoadingState(false);
}
dropdownGroup?.classList.remove('active'); dropdownGroup?.classList.remove('active');
} }
} }

View File

@@ -195,6 +195,18 @@ export class ModalManager {
}); });
} }
// Add checkUpdatesConfirmModal registration
const checkUpdatesConfirmModal = document.getElementById('checkUpdatesConfirmModal');
if (checkUpdatesConfirmModal) {
this.registerModal('checkUpdatesConfirmModal', {
element: checkUpdatesConfirmModal,
onClose: () => {
this.getModal('checkUpdatesConfirmModal').element.classList.remove('show');
document.body.classList.remove('modal-open');
}
});
}
// Add helpModal registration // Add helpModal registration
const helpModal = document.getElementById('helpModal'); const helpModal = document.getElementById('helpModal');
if (helpModal) { if (helpModal) {
@@ -339,7 +351,8 @@ export class ModalManager {
id === "duplicateDeleteModal" || id === "duplicateDeleteModal" ||
id === "modelDuplicateDeleteModal" || id === "modelDuplicateDeleteModal" ||
id === "clearCacheModal" || id === "clearCacheModal" ||
id === "bulkDeleteModal" id === "bulkDeleteModal" ||
id === "checkUpdatesConfirmModal"
) { ) {
modal.element.classList.add("show"); modal.element.classList.add("show");
} else { } else {

View File

@@ -3,6 +3,10 @@ import { translate } from './i18nHelpers.js';
import { showToast } from './uiHelpers.js'; import { showToast } from './uiHelpers.js';
import { getCompleteApiConfig, getCurrentModelType } from '../api/apiConfig.js'; import { getCompleteApiConfig, getCurrentModelType } from '../api/apiConfig.js';
import { resetAndReload } from '../api/modelApiFactory.js'; import { resetAndReload } from '../api/modelApiFactory.js';
import { getStorageItem, setStorageItem } from './storageHelpers.js';
import { modalManager } from '../managers/ModalManager.js';
const CHECK_UPDATES_CONFIRMATION_KEY = 'ack_check_updates_for_all_models';
/** /**
* Perform a model update check using the shared backend endpoint. * Perform a model update check using the shared backend endpoint.
@@ -22,6 +26,12 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
return { status: 'unsupported', displayName, records: [], error: null }; return { status: 'unsupported', displayName, records: [], error: null };
} }
const proceed = await ensureCheckUpdatesConfirmation(displayName);
if (!proceed) {
onComplete?.({ status: 'cancelled', displayName, records: [], error: null });
return { status: 'cancelled', displayName, records: [], error: null };
}
const loadingMessage = translate( const loadingMessage = translate(
'globalContextMenu.checkModelUpdates.loading', 'globalContextMenu.checkModelUpdates.loading',
{ type: displayName }, { type: displayName },
@@ -83,3 +93,101 @@ export async function performModelUpdateCheck({ onStart, onComplete } = {}) {
return { status, displayName, records, error }; return { status, displayName, records, error };
} }
function getTypePlural(displayName) {
if (!displayName) {
return 'models';
}
const lower = displayName.toLowerCase();
if (lower.endsWith('s')) {
return displayName;
}
return `${displayName}s`;
}
async function ensureCheckUpdatesConfirmation(displayName) {
const hasConfirmed = getStorageItem(CHECK_UPDATES_CONFIRMATION_KEY, false);
if (hasConfirmed) {
return true;
}
const modalElement = document.getElementById('checkUpdatesConfirmModal');
if (!modalElement) {
return true;
}
const typePlural = getTypePlural(displayName);
const titleElement = modalElement.querySelector('[data-role="title"]');
if (titleElement) {
titleElement.textContent = translate(
'modals.checkUpdates.title',
{ type: displayName, typePlural },
`Check updates for all ${typePlural}?`
);
}
const messageElement = modalElement.querySelector('[data-role="message"]');
if (messageElement) {
messageElement.textContent = translate(
'modals.checkUpdates.message',
{ type: displayName, typePlural },
`This checks every ${typePlural} in your library for updates. Large collections may take a little longer.`
);
}
const tipElement = modalElement.querySelector('[data-role="tip"]');
if (tipElement) {
tipElement.textContent = translate(
'modals.checkUpdates.tip',
{ type: displayName, typePlural },
'To work in smaller batches, switch to bulk mode, pick the ones you need, then use "Check Updates for Selected".'
);
}
const confirmButton = modalElement.querySelector('[data-action="confirm-check-updates"]');
const cancelButton = modalElement.querySelector('[data-action="cancel-check-updates"]');
if (!confirmButton || !cancelButton) {
return true;
}
return new Promise((resolve) => {
let resolved = false;
const cleanup = () => {
confirmButton.removeEventListener('click', handleConfirm);
cancelButton.removeEventListener('click', handleCancel);
};
const finalize = (proceed) => {
if (resolved) {
return;
}
resolved = true;
cleanup();
resolve(proceed);
};
const handleConfirm = (event) => {
event.preventDefault();
setStorageItem(CHECK_UPDATES_CONFIRMATION_KEY, true);
finalize(true);
modalManager.closeModal('checkUpdatesConfirmModal');
};
const handleCancel = (event) => {
event.preventDefault();
finalize(false);
modalManager.closeModal('checkUpdatesConfirmModal');
};
confirmButton.addEventListener('click', handleConfirm);
cancelButton.addEventListener('click', handleCancel);
modalManager.showModal('checkUpdatesConfirmModal', null, () => finalize(false));
});
}

View File

@@ -68,3 +68,16 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Check Updates Confirmation Modal -->
<div id="checkUpdatesConfirmModal" class="modal delete-modal">
<div class="modal-content delete-modal-content">
<h2 data-role="title"></h2>
<p class="confirmation-message" data-role="message"></p>
<p class="confirmation-tip" data-role="tip"></p>
<div class="modal-actions">
<button class="cancel-btn" data-action="cancel-check-updates">{{ t('common.actions.cancel') }}</button>
<button class="primary-btn" data-action="confirm-check-updates">{{ t('modals.checkUpdates.action') }}</button>
</div>
</div>
</div>

View File

@@ -75,6 +75,26 @@ vi.mock('../../../static/js/utils/storageHelpers.js', () => ({
setSessionItem: vi.fn(), setSessionItem: vi.fn(),
removeSessionItem: vi.fn(), removeSessionItem: vi.fn(),
getSessionItem: vi.fn(), getSessionItem: vi.fn(),
getStorageItem: vi.fn((key, defaultValue = null) => {
const value = localStorage.getItem(`lora_manager_${key}`);
if (value === null) {
return defaultValue;
}
try {
return JSON.parse(value);
} catch (error) {
return value;
}
}),
setStorageItem: vi.fn((key, value) => {
const prefixedKey = `lora_manager_${key}`;
if (typeof value === 'object' && value !== null) {
localStorage.setItem(prefixedKey, JSON.stringify(value));
} else {
localStorage.setItem(prefixedKey, value);
}
}),
})); }));
vi.mock('../../../static/js/api/modelApiFactory.js', () => ({ vi.mock('../../../static/js/api/modelApiFactory.js', () => ({
@@ -350,11 +370,15 @@ describe('Interaction-level regression coverage', () => {
expect(cleanupItem.classList.contains('disabled')).toBe(false); expect(cleanupItem.classList.contains('disabled')).toBe(false);
expect(menu._cleanupInProgress).toBe(false); expect(menu._cleanupInProgress).toBe(false);
localStorage.setItem('lora_manager_ack_check_updates_for_all_models', 'true');
menu.showMenu(360, 420); menu.showMenu(360, 420);
const checkUpdatesItem = document.querySelector('[data-action="check-model-updates"]'); const checkUpdatesItem = document.querySelector('[data-action="check-model-updates"]');
checkUpdatesItem.dispatchEvent(new Event('click', { bubbles: true })); checkUpdatesItem.dispatchEvent(new Event('click', { bubbles: true }));
expect(checkUpdatesItem.classList.contains('disabled')).toBe(true); expect(checkUpdatesItem.classList.contains('disabled')).toBe(true);
await flushAsyncTasks();
expect(global.fetch).toHaveBeenLastCalledWith('/api/lm/loras/updates/refresh', { expect(global.fetch).toHaveBeenLastCalledWith('/api/lm/loras/updates/refresh', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },