mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
refactor(filter): extract preset management logic into FilterPresetManager
Move filter preset creation, deletion, application, and storage logic from FilterManager into a dedicated FilterPresetManager class to improve separation of concerns and maintainability. - Add FilterPresetManager with preset CRUD operations - Update FilterManager to use preset manager via composition - Handle EMPTY_WILDCARD_MARKER for wildcard base model filters - Add preset-related translations to all locale files - Update filter preset UI styling and interactions
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
"back": "Zurück",
|
"back": "Zurück",
|
||||||
"next": "Weiter",
|
"next": "Weiter",
|
||||||
"backToTop": "Nach oben",
|
"backToTop": "Nach oben",
|
||||||
|
"add": "Hinzufügen",
|
||||||
"settings": "Einstellungen",
|
"settings": "Einstellungen",
|
||||||
"help": "Hilfe"
|
"help": "Hilfe"
|
||||||
},
|
},
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "Modelle filtern",
|
"title": "Modelle filtern",
|
||||||
|
"presets": "Voreinstellungen",
|
||||||
|
"savePreset": "Aktive Filter als neue Voreinstellung speichern.",
|
||||||
|
"savePresetDisabledActive": "Speichern nicht möglich: Eine Voreinstellung ist bereits aktiv. Ändern Sie die Filter, um eine neue Voreinstellung zu speichern",
|
||||||
|
"savePresetDisabledNoFilters": "Wählen Sie zuerst Filter aus, um als Voreinstellung zu speichern",
|
||||||
|
"savePresetPrompt": "Voreinstellungsname eingeben:",
|
||||||
|
"presetClickTooltip": "Voreinstellung \"{name}\" anwenden",
|
||||||
|
"presetDeleteTooltip": "Voreinstellung löschen",
|
||||||
|
"presetDeleteConfirm": "Voreinstellung \"{name}\" löschen?",
|
||||||
|
"presetDeleteConfirmClick": "Zum Bestätigen erneut klicken",
|
||||||
|
"presetOverwriteConfirm": "Voreinstellung \"{name}\" existiert bereits. Überschreiben?",
|
||||||
|
"presetNamePlaceholder": "Voreinstellungsname...",
|
||||||
|
"restoreDefaults": "Standard wiederherstellen",
|
||||||
|
"noPresets": "Noch keine Voreinstellungen gespeichert. Filter unten auswählen und auf + klicken zum Speichern",
|
||||||
"baseModel": "Basis-Modell",
|
"baseModel": "Basis-Modell",
|
||||||
"modelTags": "Tags (Top 20)",
|
"modelTags": "Tags (Top 20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "Filter gelöscht",
|
"cleared": "Filter gelöscht",
|
||||||
"noCustomFilterToClear": "Kein benutzerdefinierter Filter zum Löschen"
|
"noCustomFilterToClear": "Kein benutzerdefinierter Filter zum Löschen",
|
||||||
|
"noActiveFilters": "Keine aktiven Filter zum Speichern"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "Voreinstellung \"{name}\" erstellt",
|
||||||
|
"deleted": "Voreinstellung \"{name}\" gelöscht",
|
||||||
|
"applied": "Voreinstellung \"{name}\" angewendet",
|
||||||
|
"overwritten": "Voreinstellung \"{name}\" überschrieben",
|
||||||
|
"restored": "Standard-Voreinstellungen wiederhergestellt"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "Voreinstellungsname darf nicht leer sein",
|
||||||
|
"presetNameTooLong": "Voreinstellungsname darf maximal {max} Zeichen haben",
|
||||||
|
"presetNameInvalidChars": "Voreinstellungsname enthält ungültige Zeichen",
|
||||||
|
"presetNameExists": "Eine Voreinstellung mit diesem Namen existiert bereits",
|
||||||
|
"maxPresetsReached": "Maximal {max} Voreinstellungen erlaubt. Löschen Sie eine, um weitere hinzuzufügen.",
|
||||||
|
"presetNotFound": "Voreinstellung nicht gefunden",
|
||||||
|
"invalidPreset": "Ungültige Voreinstellungsdaten",
|
||||||
|
"deletePresetFailed": "Fehler beim Löschen der Voreinstellung",
|
||||||
|
"applyPresetFailed": "Fehler beim Anwenden der Voreinstellung"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "Beispielbilder {action} abgeschlossen",
|
"imagesCompleted": "Beispielbilder {action} abgeschlossen",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "Next",
|
"next": "Next",
|
||||||
"backToTop": "Back to top",
|
"backToTop": "Back to top",
|
||||||
"settings": "Settings",
|
"settings": "Settings",
|
||||||
"help": "Help"
|
"help": "Help",
|
||||||
|
"add": "Add"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
@@ -205,7 +206,17 @@
|
|||||||
"filter": {
|
"filter": {
|
||||||
"title": "Filter Models",
|
"title": "Filter Models",
|
||||||
"presets": "Presets",
|
"presets": "Presets",
|
||||||
"savePreset": "Save current active filters as a new preset (first select filters below, then click here)",
|
"savePreset": "Save current active filters as a new preset.",
|
||||||
|
"savePresetDisabledActive": "Cannot save: A preset is already active. Modify filters to save new preset.",
|
||||||
|
"savePresetDisabledNoFilters": "Select filters first to save as preset",
|
||||||
|
"savePresetPrompt": "Enter preset name:",
|
||||||
|
"presetClickTooltip": "Click to apply preset \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "Delete preset",
|
||||||
|
"presetDeleteConfirm": "Delete preset \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "Click again to confirm",
|
||||||
|
"presetOverwriteConfirm": "Preset \"{name}\" already exists. Overwrite?",
|
||||||
|
"presetNamePlaceholder": "Preset name...",
|
||||||
|
"restoreDefaults": "Restore defaults",
|
||||||
"noPresets": "No presets saved yet. Select filters below and click + to save",
|
"noPresets": "No presets saved yet. Select filters below and click + to save",
|
||||||
"baseModel": "Base Model",
|
"baseModel": "Base Model",
|
||||||
"modelTags": "Tags (Top 20)",
|
"modelTags": "Tags (Top 20)",
|
||||||
@@ -1422,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "Filters cleared",
|
"cleared": "Filters cleared",
|
||||||
"noCustomFilterToClear": "No custom filter to clear"
|
"noCustomFilterToClear": "No custom filter to clear",
|
||||||
|
"noActiveFilters": "No active filters to save"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "Preset \"{name}\" created",
|
||||||
|
"deleted": "Preset \"{name}\" deleted",
|
||||||
|
"applied": "Preset \"{name}\" applied",
|
||||||
|
"overwritten": "Preset \"{name}\" overwritten",
|
||||||
|
"restored": "Default presets restored"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "Preset name cannot be empty",
|
||||||
|
"presetNameTooLong": "Preset name must be {max} characters or less",
|
||||||
|
"presetNameInvalidChars": "Preset name contains invalid characters",
|
||||||
|
"presetNameExists": "A preset with this name already exists",
|
||||||
|
"maxPresetsReached": "Maximum {max} presets allowed. Delete one to add more.",
|
||||||
|
"presetNotFound": "Preset not found",
|
||||||
|
"invalidPreset": "Invalid preset data",
|
||||||
|
"deletePresetFailed": "Failed to delete preset",
|
||||||
|
"applyPresetFailed": "Failed to apply preset"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "Example images {action} completed",
|
"imagesCompleted": "Example images {action} completed",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "Siguiente",
|
"next": "Siguiente",
|
||||||
"backToTop": "Volver arriba",
|
"backToTop": "Volver arriba",
|
||||||
"settings": "Configuración",
|
"settings": "Configuración",
|
||||||
"help": "Ayuda"
|
"help": "Ayuda",
|
||||||
|
"add": "Añadir"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Cargando...",
|
"loading": "Cargando...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "Filtrar modelos",
|
"title": "Filtrar modelos",
|
||||||
|
"presets": "Preajustes",
|
||||||
|
"savePreset": "Guardar filtros activos como nuevo preajuste.",
|
||||||
|
"savePresetDisabledActive": "No se puede guardar: Ya hay un preajuste activo. Modifique los filtros para guardar un nuevo preajuste",
|
||||||
|
"savePresetDisabledNoFilters": "Seleccione filtros primero para guardar como preajuste",
|
||||||
|
"savePresetPrompt": "Ingrese el nombre del preajuste:",
|
||||||
|
"presetClickTooltip": "Hacer clic para aplicar preajuste \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "Eliminar preajuste",
|
||||||
|
"presetDeleteConfirm": "¿Eliminar preajuste \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "Haga clic de nuevo para confirmar",
|
||||||
|
"presetOverwriteConfirm": "El preset \"{name}\" ya existe. ¿Sobrescribir?",
|
||||||
|
"presetNamePlaceholder": "Nombre del preajuste...",
|
||||||
|
"restoreDefaults": "Restaurar predeterminados",
|
||||||
|
"noPresets": "Aún no hay preajustes guardados. Seleccione filtros abajo y haga clic en + para guardar",
|
||||||
"baseModel": "Modelo base",
|
"baseModel": "Modelo base",
|
||||||
"modelTags": "Etiquetas (Top 20)",
|
"modelTags": "Etiquetas (Top 20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "Filtros limpiados",
|
"cleared": "Filtros limpiados",
|
||||||
"noCustomFilterToClear": "No hay filtro personalizado para limpiar"
|
"noCustomFilterToClear": "No hay filtro personalizado para limpiar",
|
||||||
|
"noActiveFilters": "No hay filtros activos para guardar"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "Preajuste \"{name}\" creado",
|
||||||
|
"deleted": "Preajuste \"{name}\" eliminado",
|
||||||
|
"applied": "Preajuste \"{name}\" aplicado",
|
||||||
|
"overwritten": "Preset \"{name}\" sobrescrito",
|
||||||
|
"restored": "Presets predeterminados restaurados"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "El nombre del preajuste no puede estar vacío",
|
||||||
|
"presetNameTooLong": "El nombre del preajuste debe tener {max} caracteres o menos",
|
||||||
|
"presetNameInvalidChars": "El nombre del preajuste contiene caracteres inválidos",
|
||||||
|
"presetNameExists": "Ya existe un preajuste con este nombre",
|
||||||
|
"maxPresetsReached": "Máximo {max} preajustes permitidos. Elimine uno para agregar más.",
|
||||||
|
"presetNotFound": "Preajuste no encontrado",
|
||||||
|
"invalidPreset": "Datos de preajuste inválidos",
|
||||||
|
"deletePresetFailed": "Error al eliminar el preajuste",
|
||||||
|
"applyPresetFailed": "Error al aplicar el preajuste"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "Imágenes de ejemplo {action} completadas",
|
"imagesCompleted": "Imágenes de ejemplo {action} completadas",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "Suivant",
|
"next": "Suivant",
|
||||||
"backToTop": "Retour en haut",
|
"backToTop": "Retour en haut",
|
||||||
"settings": "Paramètres",
|
"settings": "Paramètres",
|
||||||
"help": "Aide"
|
"help": "Aide",
|
||||||
|
"add": "Ajouter"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Chargement...",
|
"loading": "Chargement...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "Filtrer les modèles",
|
"title": "Filtrer les modèles",
|
||||||
|
"presets": "Préréglages",
|
||||||
|
"savePreset": "Enregistrer les filtres actifs comme nouveau préréglage.",
|
||||||
|
"savePresetDisabledActive": "Impossible d'enregistrer : Un préréglage est déjà actif. Modifiez les filtres pour enregistrer un nouveau préréglage",
|
||||||
|
"savePresetDisabledNoFilters": "Sélectionnez d'abord des filtres à enregistrer comme préréglage",
|
||||||
|
"savePresetPrompt": "Entrez le nom du préréglage :",
|
||||||
|
"presetClickTooltip": "Cliquer pour appliquer le préréglage \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "Supprimer le préréglage",
|
||||||
|
"presetDeleteConfirm": "Supprimer le préréglage \"{name}\" ?",
|
||||||
|
"presetDeleteConfirmClick": "Cliquez à nouveau pour confirmer",
|
||||||
|
"presetOverwriteConfirm": "Le préréglage \"{name}\" existe déjà. Remplacer?",
|
||||||
|
"presetNamePlaceholder": "Nom du préréglage...",
|
||||||
|
"restoreDefaults": "Restaurer les paramètres par défaut",
|
||||||
|
"noPresets": "Aucun préréglage enregistré. Sélectionnez des filtres ci-dessous et cliquez sur + pour enregistrer",
|
||||||
"baseModel": "Modèle de base",
|
"baseModel": "Modèle de base",
|
||||||
"modelTags": "Tags (Top 20)",
|
"modelTags": "Tags (Top 20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "Filtres effacés",
|
"cleared": "Filtres effacés",
|
||||||
"noCustomFilterToClear": "Aucun filtre personnalisé à effacer"
|
"noCustomFilterToClear": "Aucun filtre personnalisé à effacer",
|
||||||
|
"noActiveFilters": "Aucun filtre actif à enregistrer"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "Préréglage \"{name}\" créé",
|
||||||
|
"deleted": "Préréglage \"{name}\" supprimé",
|
||||||
|
"applied": "Préréglage \"{name}\" appliqué",
|
||||||
|
"overwritten": "Préréglage \"{name}\" remplacé",
|
||||||
|
"restored": "Paramètres par défaut restaurés"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "Le nom du préréglage ne peut pas être vide",
|
||||||
|
"presetNameTooLong": "Le nom du préréglage doit contenir au maximum {max} caractères",
|
||||||
|
"presetNameInvalidChars": "Le nom du préréglage contient des caractères invalides",
|
||||||
|
"presetNameExists": "Un préréglage avec ce nom existe déjà",
|
||||||
|
"maxPresetsReached": "Maximum {max} préréglages autorisés. Supprimez-en un pour en ajouter plus.",
|
||||||
|
"presetNotFound": "Préréglage non trouvé",
|
||||||
|
"invalidPreset": "Données de préréglage invalides",
|
||||||
|
"deletePresetFailed": "Échec de la suppression du préréglage",
|
||||||
|
"applyPresetFailed": "Échec de l'application du préréglage"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "Images d'exemple {action} terminées",
|
"imagesCompleted": "Images d'exemple {action} terminées",
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"back": "חזור",
|
"back": "חזור",
|
||||||
"next": "הבא",
|
"next": "הבא",
|
||||||
"backToTop": "חזור למעלה",
|
"backToTop": "חזור למעלה",
|
||||||
|
"add": "הוסף",
|
||||||
"settings": "הגדרות",
|
"settings": "הגדרות",
|
||||||
"help": "עזרה"
|
"help": "עזרה"
|
||||||
},
|
},
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "סנן מודלים",
|
"title": "סנן מודלים",
|
||||||
|
"presets": "קביעות מראש",
|
||||||
|
"savePreset": "שמור מסננים פעילים כקביעה מראש חדשה.",
|
||||||
|
"savePresetDisabledActive": "לא ניתן לשמור: קביעה מראש כבר פעילה. שנה מסננים כדי לשמור קביעה מראש חדשה",
|
||||||
|
"savePresetDisabledNoFilters": "בחר מסננים תחילה כדי לשמור כקביעה מראש",
|
||||||
|
"savePresetPrompt": "הזן שם קביעה מראש:",
|
||||||
|
"presetClickTooltip": "לחץ כדי להפעיל קביעה מראש \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "מחק קביעה מראש",
|
||||||
|
"presetDeleteConfirm": "למחוק קביעה מראש \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "לחץ שוב לאישור",
|
||||||
|
"presetOverwriteConfirm": "הפריסט \"{name}\" כבר קיים. לדרוס?",
|
||||||
|
"presetNamePlaceholder": "שם קביעה מראש...",
|
||||||
|
"restoreDefaults": "שחזור ברירות מחדל",
|
||||||
|
"noPresets": "עדיין אין קביעות מראש שמורות. בחר מסננים למטה ולחץ על + כדי לשמור",
|
||||||
"baseModel": "מודל בסיס",
|
"baseModel": "מודל בסיס",
|
||||||
"modelTags": "תגיות (20 המובילות)",
|
"modelTags": "תגיות (20 המובילות)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "המסננים נוקו",
|
"cleared": "המסננים נוקו",
|
||||||
"noCustomFilterToClear": "אין מסנן מותאם אישית לניקוי"
|
"noCustomFilterToClear": "אין מסנן מותאם אישית לניקוי",
|
||||||
|
"noActiveFilters": "אין מסננים פעילים לשמירה"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "קביעה מראש \"{name}\" נוצרה",
|
||||||
|
"deleted": "קביעה מראש \"{name}\" נמחקה",
|
||||||
|
"applied": "קביעה מראש \"{name}\" הופעלה",
|
||||||
|
"overwritten": "קביעה מראש \"{name}\" נדרסה",
|
||||||
|
"restored": "ברירות המחדל שוחזרו"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "שם קביעה מראש לא יכול להיות ריק",
|
||||||
|
"presetNameTooLong": "שם קביעה מראש חייב להיות {max} תווים או פחות",
|
||||||
|
"presetNameInvalidChars": "שם קביעה מראש מכיל תווים לא חוקיים",
|
||||||
|
"presetNameExists": "קביעה מראש עם שם זה כבר קיימת",
|
||||||
|
"maxPresetsReached": "מותר מקסימום {max} קביעות מראש. מחק אחת כדי להוסיף עוד.",
|
||||||
|
"presetNotFound": "קביעה מראש לא נמצאה",
|
||||||
|
"invalidPreset": "נתוני קביעה מראש לא חוקיים",
|
||||||
|
"deletePresetFailed": "מחיקת קביעה מראש נכשלה",
|
||||||
|
"applyPresetFailed": "הפעלת קביעה מראש נכשלה"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "{action} תמונות הדוגמה הושלם",
|
"imagesCompleted": "{action} תמונות הדוגמה הושלם",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "次へ",
|
"next": "次へ",
|
||||||
"backToTop": "トップに戻る",
|
"backToTop": "トップに戻る",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "ヘルプ"
|
"help": "ヘルプ",
|
||||||
|
"add": "追加"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "読み込み中...",
|
"loading": "読み込み中...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "モデルをフィルタ",
|
"title": "モデルをフィルタ",
|
||||||
|
"presets": "プリセット",
|
||||||
|
"savePreset": "現在のアクティブフィルタを新しいプリセットとして保存。",
|
||||||
|
"savePresetDisabledActive": "保存できません:プリセットがすでにアクティブです。フィルタを変更して新しいプリセットを保存してください",
|
||||||
|
"savePresetDisabledNoFilters": "先にフィルタを選択してからプリセットとして保存",
|
||||||
|
"savePresetPrompt": "プリセット名を入力:",
|
||||||
|
"presetClickTooltip": "プリセット \"{name}\" を適用するにはクリック",
|
||||||
|
"presetDeleteTooltip": "プリセットを削除",
|
||||||
|
"presetDeleteConfirm": "プリセット \"{name}\" を削除しますか?",
|
||||||
|
"presetDeleteConfirmClick": "もう一度クリックして確認",
|
||||||
|
"presetOverwriteConfirm": "プリセット「{name}」は既に存在します。上書きしますか?",
|
||||||
|
"presetNamePlaceholder": "プリセット名...",
|
||||||
|
"restoreDefaults": "デフォルトを復元",
|
||||||
|
"noPresets": "まだプリセットが保存されていません。下のフィルタを選択して+をクリックして保存",
|
||||||
"baseModel": "ベースモデル",
|
"baseModel": "ベースモデル",
|
||||||
"modelTags": "タグ(上位20)",
|
"modelTags": "タグ(上位20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "フィルタがクリアされました",
|
"cleared": "フィルタがクリアされました",
|
||||||
"noCustomFilterToClear": "クリアするカスタムフィルタがありません"
|
"noCustomFilterToClear": "クリアするカスタムフィルタがありません",
|
||||||
|
"noActiveFilters": "保存するアクティブフィルタがありません"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "プリセット \"{name}\" が作成されました",
|
||||||
|
"deleted": "プリセット \"{name}\" が削除されました",
|
||||||
|
"applied": "プリセット \"{name}\" が適用されました",
|
||||||
|
"overwritten": "プリセット「{name}」を上書きしました",
|
||||||
|
"restored": "デフォルトのプリセットを復元しました"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "プリセット名を入力してください",
|
||||||
|
"presetNameTooLong": "プリセット名は{max}文字以内にしてください",
|
||||||
|
"presetNameInvalidChars": "プリセット名に使用できない文字が含まれています",
|
||||||
|
"presetNameExists": "同じ名前のプリセットが既に存在します",
|
||||||
|
"maxPresetsReached": "プリセットは最大{max}個までです。追加するには既存のものを削除してください。",
|
||||||
|
"presetNotFound": "プリセットが見つかりません",
|
||||||
|
"invalidPreset": "無効なプリセットデータです",
|
||||||
|
"deletePresetFailed": "プリセットの削除に失敗しました",
|
||||||
|
"applyPresetFailed": "プリセットの適用に失敗しました"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "例画像 {action} が完了しました",
|
"imagesCompleted": "例画像 {action} が完了しました",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "다음",
|
"next": "다음",
|
||||||
"backToTop": "맨 위로",
|
"backToTop": "맨 위로",
|
||||||
"settings": "설정",
|
"settings": "설정",
|
||||||
"help": "도움말"
|
"help": "도움말",
|
||||||
|
"add": "추가"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "로딩 중...",
|
"loading": "로딩 중...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "모델 필터",
|
"title": "모델 필터",
|
||||||
|
"presets": "프리셋",
|
||||||
|
"savePreset": "현재 활성 필터를 새 프리셋으로 저장.",
|
||||||
|
"savePresetDisabledActive": "저장할 수 없음: 프리셋이 이미 활성화되어 있습니다. 필터를 수정한 후 새 프리셋을 저장하세요",
|
||||||
|
"savePresetDisabledNoFilters": "먼저 필터를 선택한 후 프리셋으로 저장",
|
||||||
|
"savePresetPrompt": "프리셋 이름 입력:",
|
||||||
|
"presetClickTooltip": "프리셋 \"{name}\" 적용하려면 클릭",
|
||||||
|
"presetDeleteTooltip": "프리셋 삭제",
|
||||||
|
"presetDeleteConfirm": "프리셋 \"{name}\" 삭제하시겠습니까?",
|
||||||
|
"presetDeleteConfirmClick": "다시 클릭하여 확인",
|
||||||
|
"presetOverwriteConfirm": "프리셋 \"{name}\"이(가) 이미 존재합니다. 덮어쓰시겠습니까?",
|
||||||
|
"presetNamePlaceholder": "프리셋 이름...",
|
||||||
|
"restoreDefaults": "기본값 복원",
|
||||||
|
"noPresets": "저장된 프리셋이 없습니다. 아래 필터를 선택하고 +를 클릭하여 저장",
|
||||||
"baseModel": "베이스 모델",
|
"baseModel": "베이스 모델",
|
||||||
"modelTags": "태그 (상위 20개)",
|
"modelTags": "태그 (상위 20개)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "필터가 지워졌습니다",
|
"cleared": "필터가 지워졌습니다",
|
||||||
"noCustomFilterToClear": "지울 사용자 정의 필터가 없습니다"
|
"noCustomFilterToClear": "지울 사용자 정의 필터가 없습니다",
|
||||||
|
"noActiveFilters": "저장할 활성 필터가 없습니다"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "프리셋 \"{name}\" 생성됨",
|
||||||
|
"deleted": "프리셋 \"{name}\" 삭제됨",
|
||||||
|
"applied": "프리셋 \"{name}\" 적용됨",
|
||||||
|
"overwritten": "프리셋 \"{name}\" 덮어쓰기 완료",
|
||||||
|
"restored": "기본 프리셋 복원 완료"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "프리셋 이름을 입력하세요",
|
||||||
|
"presetNameTooLong": "프리셋 이름은 {max}자 이하여야 합니다",
|
||||||
|
"presetNameInvalidChars": "프리셋 이름에 유효하지 않은 문자가 포함되어 있습니다",
|
||||||
|
"presetNameExists": "동일한 이름의 프리셋이 이미 존재합니다",
|
||||||
|
"maxPresetsReached": "최대 {max}개의 프리셋만 허용됩니다. 더 추가하려면 기존 것을 삭제하세요.",
|
||||||
|
"presetNotFound": "프리셋을 찾을 수 없습니다",
|
||||||
|
"invalidPreset": "잘못된 프리셋 데이터입니다",
|
||||||
|
"deletePresetFailed": "프리셋 삭제에 실패했습니다",
|
||||||
|
"applyPresetFailed": "프리셋 적용에 실패했습니다"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "예시 이미지 {action}이(가) 완료되었습니다",
|
"imagesCompleted": "예시 이미지 {action}이(가) 완료되었습니다",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "Далее",
|
"next": "Далее",
|
||||||
"backToTop": "Наверх",
|
"backToTop": "Наверх",
|
||||||
"settings": "Настройки",
|
"settings": "Настройки",
|
||||||
"help": "Справка"
|
"help": "Справка",
|
||||||
|
"add": "Добавить"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "Загрузка...",
|
"loading": "Загрузка...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "Фильтр моделей",
|
"title": "Фильтр моделей",
|
||||||
|
"presets": "Пресеты",
|
||||||
|
"savePreset": "Сохранить текущие активные фильтры как новый пресет.",
|
||||||
|
"savePresetDisabledActive": "Невозможно сохранить: Пресет уже активен. Измените фильтры, чтобы сохранить новый пресет",
|
||||||
|
"savePresetDisabledNoFilters": "Сначала выберите фильтры для сохранения как пресет",
|
||||||
|
"savePresetPrompt": "Введите имя пресета:",
|
||||||
|
"presetClickTooltip": "Нажмите чтобы применить пресет \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "Удалить пресет",
|
||||||
|
"presetDeleteConfirm": "Удалить пресет \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "Нажмите еще раз для подтверждения",
|
||||||
|
"presetOverwriteConfirm": "Пресет \"{name}\" уже существует. Перезаписать?",
|
||||||
|
"presetNamePlaceholder": "Имя пресета...",
|
||||||
|
"restoreDefaults": "Восстановить по умолчанию",
|
||||||
|
"noPresets": "Пресеты еще не сохранены. Выберите фильтры ниже и нажмите + для сохранения",
|
||||||
"baseModel": "Базовая модель",
|
"baseModel": "Базовая модель",
|
||||||
"modelTags": "Теги (Топ 20)",
|
"modelTags": "Теги (Топ 20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "Фильтры очищены",
|
"cleared": "Фильтры очищены",
|
||||||
"noCustomFilterToClear": "Нет пользовательского фильтра для очистки"
|
"noCustomFilterToClear": "Нет пользовательского фильтра для очистки",
|
||||||
|
"noActiveFilters": "Нет активных фильтров для сохранения"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "Пресет \"{name}\" создан",
|
||||||
|
"deleted": "Пресет \"{name}\" удален",
|
||||||
|
"applied": "Пресет \"{name}\" применен",
|
||||||
|
"overwritten": "Пресет \"{name}\" перезаписан",
|
||||||
|
"restored": "Пресеты по умолчанию восстановлены"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "Имя пресета не может быть пустым",
|
||||||
|
"presetNameTooLong": "Имя пресета должно содержать не более {max} символов",
|
||||||
|
"presetNameInvalidChars": "Имя пресета содержит недопустимые символы",
|
||||||
|
"presetNameExists": "Пресет с таким именем уже существует",
|
||||||
|
"maxPresetsReached": "Допустимо максимум {max} пресетов. Удалите один, чтобы добавить больше.",
|
||||||
|
"presetNotFound": "Пресет не найден",
|
||||||
|
"invalidPreset": "Недопустимые данные пресета",
|
||||||
|
"deletePresetFailed": "Не удалось удалить пресет",
|
||||||
|
"applyPresetFailed": "Не удалось применить пресет"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "Примеры изображений {action} завершены",
|
"imagesCompleted": "Примеры изображений {action} завершены",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"backToTop": "返回顶部",
|
"backToTop": "返回顶部",
|
||||||
"settings": "设置",
|
"settings": "设置",
|
||||||
"help": "帮助"
|
"help": "帮助",
|
||||||
|
"add": "添加"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "加载中...",
|
"loading": "加载中...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "筛选模型",
|
"title": "筛选模型",
|
||||||
|
"presets": "预设",
|
||||||
|
"savePreset": "将当前激活的筛选器保存为新预设。",
|
||||||
|
"savePresetDisabledActive": "无法保存:已有预设处于激活状态。修改筛选器后可保存新预设",
|
||||||
|
"savePresetDisabledNoFilters": "先选择筛选器,然后保存为预设",
|
||||||
|
"savePresetPrompt": "输入预设名称:",
|
||||||
|
"presetClickTooltip": "点击应用预设 \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "删除预设",
|
||||||
|
"presetDeleteConfirm": "删除预设 \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "再次点击确认",
|
||||||
|
"presetOverwriteConfirm": "预设 \"{name}\" 已存在。是否覆盖?",
|
||||||
|
"presetNamePlaceholder": "预设名称...",
|
||||||
|
"restoreDefaults": "恢复默认",
|
||||||
|
"noPresets": "尚未保存预设。选择下方筛选器并点击 + 保存",
|
||||||
"baseModel": "基础模型",
|
"baseModel": "基础模型",
|
||||||
"modelTags": "标签(前20)",
|
"modelTags": "标签(前20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "筛选已清除",
|
"cleared": "筛选已清除",
|
||||||
"noCustomFilterToClear": "没有自定义筛选可清除"
|
"noCustomFilterToClear": "没有自定义筛选可清除",
|
||||||
|
"noActiveFilters": "没有可保存的激活筛选"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "预设 \"{name}\" 已创建",
|
||||||
|
"deleted": "预设 \"{name}\" 已删除",
|
||||||
|
"applied": "预设 \"{name}\" 已应用",
|
||||||
|
"overwritten": "预设 \"{name}\" 已覆盖",
|
||||||
|
"restored": "默认预设已恢复"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "预设名称不能为空",
|
||||||
|
"presetNameTooLong": "预设名称不能超过 {max} 个字符",
|
||||||
|
"presetNameInvalidChars": "预设名称包含无效字符",
|
||||||
|
"presetNameExists": "已存在同名预设",
|
||||||
|
"maxPresetsReached": "最多允许 {max} 个预设。删除一个以添加更多。",
|
||||||
|
"presetNotFound": "预设未找到",
|
||||||
|
"invalidPreset": "无效的预设数据",
|
||||||
|
"deletePresetFailed": "删除预设失败",
|
||||||
|
"applyPresetFailed": "应用预设失败"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "示例图片{action}完成",
|
"imagesCompleted": "示例图片{action}完成",
|
||||||
|
|||||||
@@ -10,7 +10,8 @@
|
|||||||
"next": "下一步",
|
"next": "下一步",
|
||||||
"backToTop": "回到頂部",
|
"backToTop": "回到頂部",
|
||||||
"settings": "設定",
|
"settings": "設定",
|
||||||
"help": "說明"
|
"help": "說明",
|
||||||
|
"add": "新增"
|
||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"loading": "載入中...",
|
"loading": "載入中...",
|
||||||
@@ -204,6 +205,19 @@
|
|||||||
},
|
},
|
||||||
"filter": {
|
"filter": {
|
||||||
"title": "篩選模型",
|
"title": "篩選模型",
|
||||||
|
"presets": "預設",
|
||||||
|
"savePreset": "將目前啟用的篩選器儲存為新預設。",
|
||||||
|
"savePresetDisabledActive": "無法儲存:已有預設處於啟用狀態。修改篩選器後可儲存新預設",
|
||||||
|
"savePresetDisabledNoFilters": "先選擇篩選器,然後儲存為預設",
|
||||||
|
"savePresetPrompt": "輸入預設名稱:",
|
||||||
|
"presetClickTooltip": "點擊套用預設 \"{name}\"",
|
||||||
|
"presetDeleteTooltip": "刪除預設",
|
||||||
|
"presetDeleteConfirm": "刪除預設 \"{name}\"?",
|
||||||
|
"presetDeleteConfirmClick": "再次點擊確認",
|
||||||
|
"presetOverwriteConfirm": "預設 \"{name}\" 已存在。是否覆蓋?",
|
||||||
|
"presetNamePlaceholder": "預設名稱...",
|
||||||
|
"restoreDefaults": "恢復預設",
|
||||||
|
"noPresets": "尚未儲存預設。選擇下方篩選器並點擊 + 儲存",
|
||||||
"baseModel": "基礎模型",
|
"baseModel": "基礎模型",
|
||||||
"modelTags": "標籤(前 20)",
|
"modelTags": "標籤(前 20)",
|
||||||
"modelTypes": "Model Types",
|
"modelTypes": "Model Types",
|
||||||
@@ -1419,7 +1433,26 @@
|
|||||||
"filters": {
|
"filters": {
|
||||||
"applied": "{message}",
|
"applied": "{message}",
|
||||||
"cleared": "篩選已清除",
|
"cleared": "篩選已清除",
|
||||||
"noCustomFilterToClear": "無自訂篩選可清除"
|
"noCustomFilterToClear": "無自訂篩選可清除",
|
||||||
|
"noActiveFilters": "沒有可儲存的啟用篩選"
|
||||||
|
},
|
||||||
|
"presets": {
|
||||||
|
"created": "預設 \"{name}\" 已建立",
|
||||||
|
"deleted": "預設 \"{name}\" 已刪除",
|
||||||
|
"applied": "預設 \"{name}\" 已套用",
|
||||||
|
"overwritten": "預設 \"{name}\" 已覆蓋",
|
||||||
|
"restored": "預設設定已恢復"
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"presetNameEmpty": "預設名稱不能為空",
|
||||||
|
"presetNameTooLong": "預設名稱不能超過 {max} 個字元",
|
||||||
|
"presetNameInvalidChars": "預設名稱包含無效字元",
|
||||||
|
"presetNameExists": "已存在同名預設",
|
||||||
|
"maxPresetsReached": "最多允許 {max} 個預設。刪除一個以新增更多。",
|
||||||
|
"presetNotFound": "預設未找到",
|
||||||
|
"invalidPreset": "無效的預設資料",
|
||||||
|
"deletePresetFailed": "刪除預設失敗",
|
||||||
|
"applyPresetFailed": "套用預設失敗"
|
||||||
},
|
},
|
||||||
"downloads": {
|
"downloads": {
|
||||||
"imagesCompleted": "範例圖片{action}完成",
|
"imagesCompleted": "範例圖片{action}完成",
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ class SettingsHandler:
|
|||||||
"model_name_display",
|
"model_name_display",
|
||||||
"update_flag_strategy",
|
"update_flag_strategy",
|
||||||
"auto_organize_exclusions",
|
"auto_organize_exclusions",
|
||||||
|
"filter_presets",
|
||||||
)
|
)
|
||||||
|
|
||||||
_PROXY_KEYS = {
|
_PROXY_KEYS = {
|
||||||
|
|||||||
@@ -552,26 +552,112 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-preset-btn {
|
.add-preset-btn {
|
||||||
background-color: transparent !important;
|
background-color: transparent;
|
||||||
border: 1px dashed var(--border-color) !important;
|
border: 1px dashed var(--border-color);
|
||||||
color: var(--text-color);
|
color: var(--text-color);
|
||||||
opacity: 0.7;
|
opacity: 0.85;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
padding: 4px 10px;
|
padding: 4px 10px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.25s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-preset-btn:hover {
|
/* Enabled state - visual cue that button is actionable */
|
||||||
|
.add-preset-btn:not(.disabled) {
|
||||||
|
border-color: var(--lora-accent);
|
||||||
|
border-style: solid;
|
||||||
|
background-color: rgba(66, 153, 225, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-preset-btn:hover:not(.disabled) {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
border-color: var(--lora-accent) !important;
|
background-color: rgba(66, 153, 225, 0.15);
|
||||||
color: var(--lora-accent);
|
color: var(--lora-accent);
|
||||||
background-color: var(--lora-surface-hover) !important;
|
transform: translateY(-1px);
|
||||||
|
box-shadow: 0 2px 6px rgba(66, 153, 225, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Disabled state - clear "unavailable" visual language */
|
||||||
|
.add-preset-btn.disabled {
|
||||||
|
opacity: 0.35;
|
||||||
|
cursor: not-allowed;
|
||||||
|
background-color: rgba(128, 128, 128, 0.05);
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-preset-btn i {
|
.add-preset-btn i {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Inline preset naming input */
|
||||||
|
.preset-inline-input-container {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 2px;
|
||||||
|
background-color: var(--lora-surface);
|
||||||
|
border: 1px solid var(--lora-accent);
|
||||||
|
border-radius: var(--border-radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-input {
|
||||||
|
width: 120px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--text-color);
|
||||||
|
font-size: 13px;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-input::placeholder {
|
||||||
|
color: var(--text-color);
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-btn {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 4px 6px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-btn:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-btn.save:hover {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preset-inline-btn.cancel:hover {
|
||||||
|
color: var(--lora-error, #e74c3c);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Two-step delete confirmation */
|
||||||
|
.preset-delete-btn.confirm {
|
||||||
|
color: var(--lora-accent);
|
||||||
|
opacity: 1;
|
||||||
|
animation: pulse-confirm 0.5s ease-in-out infinite alternate;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse-confirm {
|
||||||
|
from { opacity: 0.7; }
|
||||||
|
to { opacity: 1; }
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-presets {
|
.no-presets {
|
||||||
|
|||||||
@@ -59,6 +59,18 @@ export class BaseModelApiClient {
|
|||||||
sort_by: pageState.sortBy
|
sort_by: pageState.sortBy
|
||||||
}, pageState);
|
}, pageState);
|
||||||
|
|
||||||
|
// If params is null, it means wildcard resolved to no matches - return empty results
|
||||||
|
if (params === null) {
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
currentPage: page,
|
||||||
|
hasMore: false,
|
||||||
|
folders: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.apiConfig.endpoints.list}?${params}`);
|
const response = await fetch(`${this.apiConfig.endpoints.list}?${params}`);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`);
|
throw new Error(`Failed to fetch ${this.apiConfig.config.displayName}s: ${response.statusText}`);
|
||||||
@@ -868,6 +880,13 @@ export class BaseModelApiClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
if (pageState.filters.baseModel && pageState.filters.baseModel.length > 0) {
|
||||||
|
// Check for empty wildcard marker - if present, no models should match
|
||||||
|
const EMPTY_WILDCARD_MARKER = '__EMPTY_WILDCARD_RESULT__';
|
||||||
|
if (pageState.filters.baseModel.length === 1 &&
|
||||||
|
pageState.filters.baseModel[0] === EMPTY_WILDCARD_MARKER) {
|
||||||
|
// Wildcard resolved to no matches - return empty results
|
||||||
|
return null; // Signal to return empty results
|
||||||
|
}
|
||||||
pageState.filters.baseModel.forEach(model => {
|
pageState.filters.baseModel.forEach(model => {
|
||||||
params.append('base_model', model);
|
params.append('base_model', model);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -103,6 +103,19 @@ export async function fetchRecipesPage(page = 1, pageSize = 100) {
|
|||||||
|
|
||||||
// Add base model filters
|
// Add base model filters
|
||||||
if (pageState.filters?.baseModel && pageState.filters.baseModel.length) {
|
if (pageState.filters?.baseModel && pageState.filters.baseModel.length) {
|
||||||
|
// Check for empty wildcard marker - if present, no models should match
|
||||||
|
const EMPTY_WILDCARD_MARKER = '__EMPTY_WILDCARD_RESULT__';
|
||||||
|
if (pageState.filters.baseModel.length === 1 &&
|
||||||
|
pageState.filters.baseModel[0] === EMPTY_WILDCARD_MARKER) {
|
||||||
|
// Wildcard resolved to no matches - return empty results
|
||||||
|
return {
|
||||||
|
items: [],
|
||||||
|
totalItems: 0,
|
||||||
|
totalPages: 0,
|
||||||
|
currentPage: page,
|
||||||
|
hasMore: false
|
||||||
|
};
|
||||||
|
}
|
||||||
params.append('base_models', pageState.filters.baseModel.join(','));
|
params.append('base_models', pageState.filters.baseModel.join(','));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { getModelApiClient } from '../api/modelApiFactory.js';
|
|||||||
import { removeStorageItem, setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
import { removeStorageItem, setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
||||||
import { MODEL_TYPE_DISPLAY_NAMES } from '../utils/constants.js';
|
import { MODEL_TYPE_DISPLAY_NAMES } from '../utils/constants.js';
|
||||||
import { translate } from '../utils/i18nHelpers.js';
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
|
import { FilterPresetManager, EMPTY_WILDCARD_MARKER } from './FilterPresetManager.js';
|
||||||
|
|
||||||
export class FilterManager {
|
export class FilterManager {
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
@@ -20,7 +21,12 @@ export class FilterManager {
|
|||||||
this.filterButton = document.getElementById('filterButton');
|
this.filterButton = document.getElementById('filterButton');
|
||||||
this.activeFiltersCount = document.getElementById('activeFiltersCount');
|
this.activeFiltersCount = document.getElementById('activeFiltersCount');
|
||||||
this.tagsLoaded = false;
|
this.tagsLoaded = false;
|
||||||
this.activePreset = null; // Track currently active preset
|
|
||||||
|
// Initialize preset manager
|
||||||
|
this.presetManager = new FilterPresetManager({
|
||||||
|
page: this.currentPage,
|
||||||
|
filterManager: this
|
||||||
|
});
|
||||||
|
|
||||||
this.initialize();
|
this.initialize();
|
||||||
|
|
||||||
@@ -31,6 +37,17 @@ export class FilterManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Accessor for backward compatibility with activePreset
|
||||||
|
get activePreset() {
|
||||||
|
return this.presetManager?.activePreset ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
set activePreset(value) {
|
||||||
|
if (this.presetManager) {
|
||||||
|
this.presetManager.activePreset = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
initialize() {
|
initialize() {
|
||||||
// Create base model filter tags if they exist
|
// Create base model filter tags if they exist
|
||||||
if (document.getElementById('baseModelTags')) {
|
if (document.getElementById('baseModelTags')) {
|
||||||
@@ -402,7 +419,7 @@ export class FilterManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Render presets
|
// Render presets
|
||||||
this.renderPresets();
|
this.presetManager.renderPresets();
|
||||||
} else {
|
} else {
|
||||||
this.closeFilterPanel();
|
this.closeFilterPanel();
|
||||||
}
|
}
|
||||||
@@ -461,7 +478,9 @@ export class FilterManager {
|
|||||||
const tagFilterCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
const tagFilterCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||||
const licenseFilterCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
const licenseFilterCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||||
const modelTypeFilterCount = this.filters.modelTypes.length;
|
const modelTypeFilterCount = this.filters.modelTypes.length;
|
||||||
const totalActiveFilters = this.filters.baseModel.length + tagFilterCount + licenseFilterCount + modelTypeFilterCount;
|
// Exclude EMPTY_WILDCARD_MARKER from base model count
|
||||||
|
const baseModelCount = this.filters.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER).length;
|
||||||
|
const totalActiveFilters = baseModelCount + tagFilterCount + licenseFilterCount + modelTypeFilterCount;
|
||||||
|
|
||||||
if (this.activeFiltersCount) {
|
if (this.activeFiltersCount) {
|
||||||
if (totalActiveFilters > 0) {
|
if (totalActiveFilters > 0) {
|
||||||
@@ -471,23 +490,31 @@ export class FilterManager {
|
|||||||
this.activeFiltersCount.style.display = 'none';
|
this.activeFiltersCount.style.display = 'none';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update add button state when filters change
|
||||||
|
if (this.presetManager) {
|
||||||
|
this.presetManager.updateAddButtonState();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async applyFilters(showToastNotification = true, isPresetApply = false) {
|
async applyFilters(showToastNotification = true, isPresetApply = false) {
|
||||||
const pageState = getCurrentPageState();
|
const pageState = getCurrentPageState();
|
||||||
const storageKey = `${this.currentPage}_filters`;
|
const storageKey = `${this.currentPage}_filters`;
|
||||||
|
|
||||||
// Save filters to localStorage
|
// Save filters to localStorage (exclude EMPTY_WILDCARD_MARKER)
|
||||||
const filtersSnapshot = this.cloneFilters();
|
const filtersSnapshot = this.cloneFilters();
|
||||||
|
// Don't persist EMPTY_WILDCARD_MARKER - it's a runtime-only marker
|
||||||
|
filtersSnapshot.baseModel = filtersSnapshot.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER);
|
||||||
setStorageItem(storageKey, filtersSnapshot);
|
setStorageItem(storageKey, filtersSnapshot);
|
||||||
|
|
||||||
// Update state with current filters
|
// Update state with current filters
|
||||||
pageState.filters = filtersSnapshot;
|
pageState.filters = this.cloneFilters();
|
||||||
|
|
||||||
// Deactivate preset if this is a manual filter change (not from applying a preset)
|
// Deactivate preset if this is a manual filter change (not from applying a preset)
|
||||||
if (!isPresetApply && this.activePreset) {
|
if (!isPresetApply && this.activePreset) {
|
||||||
this.activePreset = null;
|
this.activePreset = null;
|
||||||
this.renderPresets(); // Re-render to remove active state
|
this.presetManager.saveActivePreset(); // Persist the cleared state
|
||||||
|
this.presetManager.renderPresets(); // Re-render to remove active state
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the appropriate manager's load method based on page type
|
// Call the appropriate manager's load method based on page type
|
||||||
@@ -527,6 +554,7 @@ export class FilterManager {
|
|||||||
async clearFilters() {
|
async clearFilters() {
|
||||||
// Clear active preset
|
// Clear active preset
|
||||||
this.activePreset = null;
|
this.activePreset = null;
|
||||||
|
this.presetManager.saveActivePreset(); // Persist the cleared state
|
||||||
|
|
||||||
// Clear all filters
|
// Clear all filters
|
||||||
this.filters = this.initializeFilters({
|
this.filters = this.initializeFilters({
|
||||||
@@ -544,7 +572,7 @@ export class FilterManager {
|
|||||||
// Update UI
|
// Update UI
|
||||||
this.updateTagSelections();
|
this.updateTagSelections();
|
||||||
this.updateActiveFiltersCount();
|
this.updateActiveFiltersCount();
|
||||||
this.renderPresets(); // Re-render to remove active state
|
this.presetManager.renderPresets(); // Re-render to remove active state
|
||||||
|
|
||||||
// Remove from local Storage
|
// Remove from local Storage
|
||||||
const storageKey = `${this.currentPage}_filters`;
|
const storageKey = `${this.currentPage}_filters`;
|
||||||
@@ -590,14 +618,19 @@ export class FilterManager {
|
|||||||
console.error(`Error loading ${this.currentPage} filters from storage:`, error);
|
console.error(`Error loading ${this.currentPage} filters from storage:`, error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Restore active preset after loading filters
|
||||||
|
this.presetManager.restoreActivePreset();
|
||||||
}
|
}
|
||||||
|
|
||||||
hasActiveFilters() {
|
hasActiveFilters() {
|
||||||
const tagCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
const tagCount = this.filters.tags ? Object.keys(this.filters.tags).length : 0;
|
||||||
const licenseCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
const licenseCount = this.filters.license ? Object.keys(this.filters.license).length : 0;
|
||||||
const modelTypeCount = this.filters.modelTypes.length;
|
const modelTypeCount = this.filters.modelTypes.length;
|
||||||
|
// Exclude EMPTY_WILDCARD_MARKER from base model count
|
||||||
|
const baseModelCount = this.filters.baseModel.filter(m => m !== EMPTY_WILDCARD_MARKER).length;
|
||||||
return (
|
return (
|
||||||
this.filters.baseModel.length > 0 ||
|
baseModelCount > 0 ||
|
||||||
tagCount > 0 ||
|
tagCount > 0 ||
|
||||||
licenseCount > 0 ||
|
licenseCount > 0 ||
|
||||||
modelTypeCount > 0
|
modelTypeCount > 0
|
||||||
@@ -733,219 +766,8 @@ export class FilterManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Preset management methods
|
// Preset management delegation methods for backward compatibility
|
||||||
loadPresets() {
|
hasEmptyWildcardResult() {
|
||||||
const presetsKey = `${this.currentPage}_filter_presets`;
|
return this.presetManager?.hasEmptyWildcardResult() ?? false;
|
||||||
const presets = getStorageItem(presetsKey);
|
|
||||||
|
|
||||||
// If no presets exist and this is the loras page, add default example presets
|
|
||||||
if ((!presets || presets.length === 0) && this.currentPage === 'loras') {
|
|
||||||
return this.getDefaultPresets();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.isArray(presets) ? presets : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
getDefaultPresets() {
|
|
||||||
// Example presets that users can modify or delete
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
name: "WAN Models",
|
|
||||||
filters: {
|
|
||||||
baseModel: [
|
|
||||||
"Wan Video",
|
|
||||||
"Wan Video 1.3B t2v",
|
|
||||||
"Wan Video 14B t2v",
|
|
||||||
"Wan Video 14B i2v 480p",
|
|
||||||
"Wan Video 14B i2v 720p",
|
|
||||||
"Wan Video 2.2 TI2V-5B",
|
|
||||||
"Wan Video 2.2 T2V-A14B",
|
|
||||||
"Wan Video 2.2 I2V-A14B"
|
|
||||||
],
|
|
||||||
tags: {},
|
|
||||||
license: {},
|
|
||||||
modelTypes: []
|
|
||||||
},
|
|
||||||
createdAt: Date.now(),
|
|
||||||
isDefault: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
savePresets(presets) {
|
|
||||||
const presetsKey = `${this.currentPage}_filter_presets`;
|
|
||||||
setStorageItem(presetsKey, presets);
|
|
||||||
}
|
|
||||||
|
|
||||||
createPreset(name) {
|
|
||||||
if (!name || !name.trim()) {
|
|
||||||
showToast('Preset name cannot be empty', {}, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const presets = this.loadPresets();
|
|
||||||
|
|
||||||
// Check for duplicate names
|
|
||||||
if (presets.some(p => p.name === name.trim())) {
|
|
||||||
showToast('A preset with this name already exists', {}, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const preset = {
|
|
||||||
name: name.trim(),
|
|
||||||
filters: this.cloneFilters(),
|
|
||||||
createdAt: Date.now()
|
|
||||||
};
|
|
||||||
|
|
||||||
presets.push(preset);
|
|
||||||
this.savePresets(presets);
|
|
||||||
this.renderPresets();
|
|
||||||
showToast(`Preset "${name}" created`, {}, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
deletePreset(name) {
|
|
||||||
const presets = this.loadPresets();
|
|
||||||
const filtered = presets.filter(p => p.name !== name);
|
|
||||||
|
|
||||||
// If no presets left, remove from storage so defaults can appear
|
|
||||||
if (filtered.length === 0) {
|
|
||||||
const presetsKey = `${this.currentPage}_filter_presets`;
|
|
||||||
removeStorageItem(presetsKey);
|
|
||||||
} else {
|
|
||||||
this.savePresets(filtered);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.renderPresets();
|
|
||||||
showToast(`Preset "${name}" deleted`, {}, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
async applyPreset(name) {
|
|
||||||
const presets = this.loadPresets();
|
|
||||||
const preset = presets.find(p => p.name === name);
|
|
||||||
|
|
||||||
if (!preset) {
|
|
||||||
showToast('Preset not found', {}, 'error');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set active preset
|
|
||||||
this.activePreset = name;
|
|
||||||
|
|
||||||
// Apply the preset filters
|
|
||||||
this.filters = this.initializeFilters(preset.filters);
|
|
||||||
|
|
||||||
// Update state
|
|
||||||
const pageState = getCurrentPageState();
|
|
||||||
pageState.filters = this.cloneFilters();
|
|
||||||
|
|
||||||
// If tags haven't been loaded yet, load them first
|
|
||||||
if (!this.tagsLoaded) {
|
|
||||||
await this.loadTopTags();
|
|
||||||
this.tagsLoaded = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update UI - this will show the blue outlines on active filters
|
|
||||||
// Must be done AFTER tags are loaded
|
|
||||||
this.updateTagSelections();
|
|
||||||
this.updateActiveFiltersCount();
|
|
||||||
this.renderPresets(); // Re-render to show active state
|
|
||||||
|
|
||||||
// Apply filters (pass true for isPresetApply so it doesn't clear activePreset)
|
|
||||||
// Don't show toast here, we'll show it after
|
|
||||||
await this.applyFilters(false, true);
|
|
||||||
|
|
||||||
// Show success toast without closing the panel
|
|
||||||
showToast(`Preset "${name}" applied`, {}, 'success');
|
|
||||||
}
|
|
||||||
|
|
||||||
renderPresets() {
|
|
||||||
const presetsContainer = document.getElementById('filterPresets');
|
|
||||||
if (!presetsContainer) return;
|
|
||||||
|
|
||||||
const presets = this.loadPresets();
|
|
||||||
presetsContainer.innerHTML = '';
|
|
||||||
|
|
||||||
// Render existing presets
|
|
||||||
presets.forEach(preset => {
|
|
||||||
const presetEl = document.createElement('div');
|
|
||||||
presetEl.className = 'filter-preset';
|
|
||||||
|
|
||||||
// Mark as active if this is the active preset
|
|
||||||
const isActive = this.activePreset === preset.name;
|
|
||||||
if (isActive) {
|
|
||||||
presetEl.classList.add('active');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop propagation on the preset element itself
|
|
||||||
presetEl.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
});
|
|
||||||
|
|
||||||
const presetName = document.createElement('span');
|
|
||||||
presetName.className = 'preset-name';
|
|
||||||
|
|
||||||
// Add checkmark icon if active
|
|
||||||
if (isActive) {
|
|
||||||
presetName.innerHTML = `<i class="fas fa-check"></i> ${preset.name}`;
|
|
||||||
} else {
|
|
||||||
presetName.textContent = preset.name;
|
|
||||||
}
|
|
||||||
presetName.title = `Click to apply preset "${preset.name}"`;
|
|
||||||
|
|
||||||
const deleteBtn = document.createElement('button');
|
|
||||||
deleteBtn.className = 'preset-delete-btn';
|
|
||||||
deleteBtn.innerHTML = '<i class="fas fa-times"></i>';
|
|
||||||
deleteBtn.title = 'Delete preset';
|
|
||||||
|
|
||||||
// Apply preset on name click (toggle if already active)
|
|
||||||
presetName.addEventListener('click', async (e) => {
|
|
||||||
e.stopPropagation(); // Prevent click from bubbling to document
|
|
||||||
|
|
||||||
// If this preset is already active, deactivate it (clear filters)
|
|
||||||
if (this.activePreset === preset.name) {
|
|
||||||
await this.clearFilters();
|
|
||||||
} else {
|
|
||||||
await this.applyPreset(preset.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Delete preset on delete button click
|
|
||||||
deleteBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
if (confirm(`Delete preset "${preset.name}"?`)) {
|
|
||||||
this.deletePreset(preset.name);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
presetEl.appendChild(presetName);
|
|
||||||
presetEl.appendChild(deleteBtn);
|
|
||||||
presetsContainer.appendChild(presetEl);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add the "Add new preset" button as the last element
|
|
||||||
const addBtn = document.createElement('div');
|
|
||||||
addBtn.className = 'filter-preset add-preset-btn';
|
|
||||||
addBtn.innerHTML = '<i class="fas fa-plus"></i> Add';
|
|
||||||
addBtn.title = 'Save current filters as a new preset';
|
|
||||||
|
|
||||||
addBtn.addEventListener('click', (e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
this.showSavePresetDialog();
|
|
||||||
});
|
|
||||||
|
|
||||||
presetsContainer.appendChild(addBtn);
|
|
||||||
}
|
|
||||||
|
|
||||||
showSavePresetDialog() {
|
|
||||||
// Check if there are any active filters
|
|
||||||
if (!this.hasActiveFilters()) {
|
|
||||||
showToast('No active filters to save', {}, 'info');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name = prompt('Enter preset name:');
|
|
||||||
if (name !== null) {
|
|
||||||
this.createPreset(name);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
868
static/js/managers/FilterPresetManager.js
Normal file
868
static/js/managers/FilterPresetManager.js
Normal file
@@ -0,0 +1,868 @@
|
|||||||
|
import { showToast } from '../utils/uiHelpers.js';
|
||||||
|
import { removeStorageItem, setStorageItem, getStorageItem } from '../utils/storageHelpers.js';
|
||||||
|
import { translate } from '../utils/i18nHelpers.js';
|
||||||
|
import { state } from '../state/index.js';
|
||||||
|
|
||||||
|
// Constants for preset management
|
||||||
|
const PRESETS_STORAGE_VERSION = 'v1';
|
||||||
|
const MAX_PRESET_NAME_LENGTH = 30;
|
||||||
|
const MAX_PRESETS_COUNT = 10;
|
||||||
|
|
||||||
|
// Marker for when wildcard patterns resolve to no matches
|
||||||
|
// This ensures we return empty results instead of all models
|
||||||
|
export const EMPTY_WILDCARD_MARKER = '__EMPTY_WILDCARD_RESULT__';
|
||||||
|
|
||||||
|
// Timeout for two-step delete confirmation (ms)
|
||||||
|
const DELETE_CONFIRM_TIMEOUT = 3000;
|
||||||
|
|
||||||
|
export class FilterPresetManager {
|
||||||
|
constructor(options = {}) {
|
||||||
|
this.currentPage = options.page || 'loras';
|
||||||
|
this.filterManager = options.filterManager || null;
|
||||||
|
this.activePreset = null;
|
||||||
|
|
||||||
|
// Race condition fix: track pending preset applications
|
||||||
|
this.applyPresetAbortController = null;
|
||||||
|
this.applyPresetRequestId = 0;
|
||||||
|
|
||||||
|
// UI state for two-step delete
|
||||||
|
this.pendingDeletePreset = null;
|
||||||
|
this.pendingDeleteTimeout = null;
|
||||||
|
|
||||||
|
// UI state for inline naming
|
||||||
|
this.isInlineNamingActive = false;
|
||||||
|
|
||||||
|
// Cache for presets to avoid repeated settings lookups
|
||||||
|
this._presetsCache = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Storage key methods (legacy - for migration only)
|
||||||
|
getPresetsStorageKey() {
|
||||||
|
return `${this.currentPage}_filter_presets_${PRESETS_STORAGE_VERSION}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
getActivePresetStorageKey() {
|
||||||
|
return `${this.currentPage}_active_preset`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get settings key for filter presets based on current page
|
||||||
|
*/
|
||||||
|
getSettingsKey() {
|
||||||
|
return `filter_presets`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the filter presets object from settings
|
||||||
|
* Returns an object with page keys (loras, checkpoints, embeddings) containing presets arrays
|
||||||
|
*/
|
||||||
|
getPresetsFromSettings() {
|
||||||
|
const settings = state?.global?.settings;
|
||||||
|
const presets = settings?.filter_presets;
|
||||||
|
if (presets && typeof presets === 'object') {
|
||||||
|
return presets;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save filter presets to backend settings
|
||||||
|
*/
|
||||||
|
async savePresetsToBackend(allPresets) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/lm/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ filter_presets: allPresets })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to save presets to backend');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (data.success === false) {
|
||||||
|
throw new Error(data.error || 'Failed to save presets to backend');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update local cache
|
||||||
|
this._presetsCache = allPresets;
|
||||||
|
|
||||||
|
// Update local settings state
|
||||||
|
if (state?.global?.settings) {
|
||||||
|
state.global.settings.filter_presets = allPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error saving presets to backend:', error);
|
||||||
|
showToast('Failed to save presets to backend', {}, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save active preset name to localStorage
|
||||||
|
* Note: This is UI state only, not persisted to backend
|
||||||
|
*/
|
||||||
|
saveActivePreset() {
|
||||||
|
const key = this.getActivePresetStorageKey();
|
||||||
|
if (this.activePreset) {
|
||||||
|
setStorageItem(key, this.activePreset);
|
||||||
|
} else {
|
||||||
|
removeStorageItem(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore active preset from localStorage
|
||||||
|
* Note: This is UI state only, not synced from backend
|
||||||
|
*/
|
||||||
|
restoreActivePreset() {
|
||||||
|
const key = this.getActivePresetStorageKey();
|
||||||
|
const savedPresetName = getStorageItem(key);
|
||||||
|
|
||||||
|
if (savedPresetName) {
|
||||||
|
// Verify the preset still exists
|
||||||
|
const presets = this.loadPresets();
|
||||||
|
const preset = presets.find(p => p.name === savedPresetName);
|
||||||
|
if (preset) {
|
||||||
|
this.activePreset = savedPresetName;
|
||||||
|
} else {
|
||||||
|
// Preset no longer exists, clear the saved value
|
||||||
|
this.activePreset = null;
|
||||||
|
this.saveActivePreset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migrate presets from localStorage to backend settings
|
||||||
|
*/
|
||||||
|
async migratePresetsFromLocalStorage() {
|
||||||
|
const legacyKey = this.getPresetsStorageKey();
|
||||||
|
const legacyPresets = getStorageItem(legacyKey);
|
||||||
|
|
||||||
|
if (!legacyPresets || !Array.isArray(legacyPresets) || legacyPresets.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we already have presets in backend for this page
|
||||||
|
const allPresets = this.getPresetsFromSettings();
|
||||||
|
if (allPresets[this.currentPage] && allPresets[this.currentPage].length > 0) {
|
||||||
|
// Already migrated, clear localStorage
|
||||||
|
removeStorageItem(legacyKey);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate to backend
|
||||||
|
const validPresets = legacyPresets.filter(preset => {
|
||||||
|
if (!preset || typeof preset !== 'object') return false;
|
||||||
|
if (!preset.name || typeof preset.name !== 'string') return false;
|
||||||
|
if (!preset.filters || typeof preset.filters !== 'object') return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validPresets.length > 0) {
|
||||||
|
allPresets[this.currentPage] = validPresets;
|
||||||
|
const success = await this.savePresetsToBackend(allPresets);
|
||||||
|
if (success) {
|
||||||
|
removeStorageItem(legacyKey);
|
||||||
|
console.log(`Migrated ${validPresets.length} presets from localStorage to backend`);
|
||||||
|
}
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadPresets() {
|
||||||
|
// Get presets from settings
|
||||||
|
const allPresets = this.getPresetsFromSettings();
|
||||||
|
let presets = allPresets[this.currentPage];
|
||||||
|
|
||||||
|
// Fallback to localStorage if no presets in settings (migration)
|
||||||
|
if (!presets) {
|
||||||
|
const legacyKey = this.getPresetsStorageKey();
|
||||||
|
presets = getStorageItem(legacyKey);
|
||||||
|
|
||||||
|
// Trigger async migration
|
||||||
|
if (presets && Array.isArray(presets) && presets.length > 0) {
|
||||||
|
this.migratePresetsFromLocalStorage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!presets) {
|
||||||
|
if (this.currentPage === 'loras') {
|
||||||
|
return this.getDefaultPresets();
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(presets)) {
|
||||||
|
console.warn('Invalid presets data format: expected array');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const validPresets = presets.filter(preset => {
|
||||||
|
if (!preset || typeof preset !== 'object') return false;
|
||||||
|
if (!preset.name || typeof preset.name !== 'string') return false;
|
||||||
|
if (!preset.filters || typeof preset.filters !== 'object') return false;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (validPresets.length === 0 && this.currentPage === 'loras') {
|
||||||
|
return this.getDefaultPresets();
|
||||||
|
}
|
||||||
|
|
||||||
|
return validPresets;
|
||||||
|
}
|
||||||
|
|
||||||
|
getDefaultPresets() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "WAN Models",
|
||||||
|
filters: {
|
||||||
|
baseModel: ["Wan Video*"],
|
||||||
|
tags: {},
|
||||||
|
license: {},
|
||||||
|
modelTypes: []
|
||||||
|
},
|
||||||
|
createdAt: Date.now(),
|
||||||
|
isDefault: true
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve base model patterns to actual available models
|
||||||
|
* Supports exact matches and wildcard patterns (ending with *)
|
||||||
|
*
|
||||||
|
* @param {Array} patterns - Array of base model patterns
|
||||||
|
* @param {AbortSignal} signal - Optional abort signal for cancellation
|
||||||
|
* @returns {Promise<Array>} Resolved base model names
|
||||||
|
*/
|
||||||
|
async resolveBaseModelPatterns(patterns, signal = null) {
|
||||||
|
if (!patterns || patterns.length === 0) return [];
|
||||||
|
|
||||||
|
const hasWildcards = patterns.some(p => p.endsWith('*'));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fetchOptions = signal ? { signal } : {};
|
||||||
|
const response = await fetch(`/api/lm/${this.currentPage}/base-models`, fetchOptions);
|
||||||
|
|
||||||
|
if (!response.ok) throw new Error('Failed to fetch base models');
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
if (!data.success || !Array.isArray(data.base_models)) {
|
||||||
|
const nonWildcards = patterns.filter(p => !p.endsWith('*'));
|
||||||
|
if (hasWildcards && nonWildcards.length === 0) {
|
||||||
|
return [EMPTY_WILDCARD_MARKER];
|
||||||
|
}
|
||||||
|
return nonWildcards;
|
||||||
|
}
|
||||||
|
|
||||||
|
const availableModels = data.base_models.map(m => m.name);
|
||||||
|
const resolvedModels = [];
|
||||||
|
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.endsWith('*')) {
|
||||||
|
const prefix = pattern.slice(0, -1);
|
||||||
|
const matches = availableModels.filter(model =>
|
||||||
|
model.startsWith(prefix)
|
||||||
|
);
|
||||||
|
resolvedModels.push(...matches);
|
||||||
|
} else {
|
||||||
|
if (availableModels.includes(pattern)) {
|
||||||
|
resolvedModels.push(pattern);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueModels = [...new Set(resolvedModels)];
|
||||||
|
|
||||||
|
if (hasWildcards && uniqueModels.length === 0) {
|
||||||
|
return [EMPTY_WILDCARD_MARKER];
|
||||||
|
}
|
||||||
|
|
||||||
|
return uniqueModels;
|
||||||
|
} catch (error) {
|
||||||
|
// Rethrow abort errors so they can be handled properly
|
||||||
|
if (error.name === 'AbortError') {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
console.warn('Error resolving base model patterns:', error);
|
||||||
|
const nonWildcards = patterns.filter(p => !p.endsWith('*'));
|
||||||
|
if (hasWildcards && nonWildcards.length === 0) {
|
||||||
|
return [EMPTY_WILDCARD_MARKER];
|
||||||
|
}
|
||||||
|
return nonWildcards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the base model filter represents an empty wildcard result
|
||||||
|
*/
|
||||||
|
hasEmptyWildcardResult() {
|
||||||
|
const filters = this.filterManager?.filters;
|
||||||
|
return filters?.baseModel?.length === 1 &&
|
||||||
|
filters.baseModel[0] === EMPTY_WILDCARD_MARKER;
|
||||||
|
}
|
||||||
|
|
||||||
|
async savePresets(presets) {
|
||||||
|
const allPresets = this.getPresetsFromSettings();
|
||||||
|
allPresets[this.currentPage] = presets;
|
||||||
|
await this.savePresetsToBackend(allPresets);
|
||||||
|
}
|
||||||
|
|
||||||
|
validatePresetName(name) {
|
||||||
|
if (!name || !name.trim()) {
|
||||||
|
return { valid: false, message: translate('toast.error.presetNameEmpty', {}, 'Preset name cannot be empty') };
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = name.trim();
|
||||||
|
|
||||||
|
if (trimmedName.length > MAX_PRESET_NAME_LENGTH) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: translate('toast.error.presetNameTooLong', { max: MAX_PRESET_NAME_LENGTH }, `Preset name must be ${MAX_PRESET_NAME_LENGTH} characters or less`)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const htmlSpecialChars = /[<>'&]/;
|
||||||
|
if (htmlSpecialChars.test(trimmedName)) {
|
||||||
|
return { valid: false, message: translate('toast.error.presetNameInvalidChars', {}, 'Preset name contains invalid characters') };
|
||||||
|
}
|
||||||
|
|
||||||
|
const controlChars = /[\x00-\x1F\x7F-\x9F]/;
|
||||||
|
if (controlChars.test(trimmedName)) {
|
||||||
|
return { valid: false, message: translate('toast.error.presetNameInvalidChars', {}, 'Preset name contains invalid characters') };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, name: trimmedName };
|
||||||
|
}
|
||||||
|
|
||||||
|
async createPreset(name, options = {}) {
|
||||||
|
const validation = this.validatePresetName(name);
|
||||||
|
if (!validation.valid) {
|
||||||
|
showToast(validation.message, {}, 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trimmedName = validation.name;
|
||||||
|
let presets = this.loadPresets();
|
||||||
|
|
||||||
|
const existingIndex = presets.findIndex(p => p.name.toLowerCase() === trimmedName.toLowerCase());
|
||||||
|
const isDuplicate = existingIndex !== -1;
|
||||||
|
|
||||||
|
if (isDuplicate) {
|
||||||
|
if (options.overwrite) {
|
||||||
|
presets[existingIndex] = {
|
||||||
|
name: trimmedName,
|
||||||
|
filters: this.filterManager.cloneFilters(),
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
await this.savePresets(presets);
|
||||||
|
this.renderPresets();
|
||||||
|
showToast(
|
||||||
|
translate('toast.presets.overwritten', { name: trimmedName }, `Preset "${trimmedName}" overwritten`),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const confirmMsg = translate('header.filter.presetOverwriteConfirm', { name: trimmedName }, `Preset "${trimmedName}" already exists. Overwrite?`);
|
||||||
|
if (confirm(confirmMsg)) {
|
||||||
|
return this.createPreset(name, { overwrite: true });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (presets.length >= MAX_PRESETS_COUNT) {
|
||||||
|
showToast(
|
||||||
|
translate('toast.error.maxPresetsReached', { max: MAX_PRESETS_COUNT }, `Maximum ${MAX_PRESETS_COUNT} presets allowed. Delete one to add more.`),
|
||||||
|
{},
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preset = {
|
||||||
|
name: trimmedName,
|
||||||
|
filters: this.filterManager.cloneFilters(),
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
presets.push(preset);
|
||||||
|
await this.savePresets(presets);
|
||||||
|
|
||||||
|
// Auto-activate the newly created preset
|
||||||
|
this.activePreset = trimmedName;
|
||||||
|
this.saveActivePreset();
|
||||||
|
|
||||||
|
this.renderPresets();
|
||||||
|
showToast(
|
||||||
|
translate('toast.presets.created', { name: trimmedName }, `Preset "${trimmedName}" created`),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async deletePreset(name) {
|
||||||
|
try {
|
||||||
|
let presets = this.loadPresets();
|
||||||
|
const filtered = presets.filter(p => p.name !== name);
|
||||||
|
|
||||||
|
if (filtered.length === 0) {
|
||||||
|
const allPresets = this.getPresetsFromSettings();
|
||||||
|
delete allPresets[this.currentPage];
|
||||||
|
await this.savePresetsToBackend(allPresets);
|
||||||
|
} else {
|
||||||
|
await this.savePresets(filtered);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.activePreset === name) {
|
||||||
|
this.activePreset = null;
|
||||||
|
this.saveActivePreset();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderPresets();
|
||||||
|
showToast(
|
||||||
|
translate('toast.presets.deleted', { name }, `Preset "${name}" deleted`),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting preset:', error);
|
||||||
|
showToast(translate('toast.error.deletePresetFailed', {}, 'Failed to delete preset'), {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply a preset with race condition protection
|
||||||
|
* Cancels any pending preset application before starting a new one
|
||||||
|
*/
|
||||||
|
async applyPreset(name) {
|
||||||
|
// Cancel any pending preset application
|
||||||
|
if (this.applyPresetAbortController) {
|
||||||
|
this.applyPresetAbortController.abort();
|
||||||
|
}
|
||||||
|
this.applyPresetAbortController = new AbortController();
|
||||||
|
const signal = this.applyPresetAbortController.signal;
|
||||||
|
const requestId = ++this.applyPresetRequestId;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const presets = this.loadPresets();
|
||||||
|
const preset = presets.find(p => p.name === name);
|
||||||
|
|
||||||
|
if (!preset) {
|
||||||
|
showToast(translate('toast.error.presetNotFound', {}, 'Preset not found'), {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!preset.filters || typeof preset.filters !== 'object') {
|
||||||
|
showToast(translate('toast.error.invalidPreset', {}, 'Invalid preset data'), {}, 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if aborted before expensive operations
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
// Resolve base model patterns (supports wildcards for default presets)
|
||||||
|
const resolvedBaseModels = await this.resolveBaseModelPatterns(
|
||||||
|
preset.filters.baseModel,
|
||||||
|
signal
|
||||||
|
);
|
||||||
|
|
||||||
|
// Check if request is still valid (another preset may have been selected)
|
||||||
|
if (requestId !== this.applyPresetRequestId) return;
|
||||||
|
if (signal.aborted) return;
|
||||||
|
|
||||||
|
// Set active preset AFTER successful resolution
|
||||||
|
this.activePreset = name;
|
||||||
|
this.saveActivePreset();
|
||||||
|
|
||||||
|
// Apply the preset filters with resolved base models
|
||||||
|
this.filterManager.filters = this.filterManager.initializeFilters({
|
||||||
|
...preset.filters,
|
||||||
|
baseModel: resolvedBaseModels
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update state
|
||||||
|
const { getCurrentPageState } = await import('../state/index.js');
|
||||||
|
const pageState = getCurrentPageState();
|
||||||
|
pageState.filters = this.filterManager.cloneFilters();
|
||||||
|
|
||||||
|
// If tags haven't been loaded yet, load them first
|
||||||
|
if (!this.filterManager.tagsLoaded) {
|
||||||
|
await this.filterManager.loadTopTags();
|
||||||
|
this.filterManager.tagsLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check again after async operation
|
||||||
|
if (requestId !== this.applyPresetRequestId) return;
|
||||||
|
|
||||||
|
// Update UI
|
||||||
|
this.filterManager.updateTagSelections();
|
||||||
|
this.filterManager.updateActiveFiltersCount();
|
||||||
|
this.renderPresets();
|
||||||
|
|
||||||
|
// Apply filters (pass true for isPresetApply so it doesn't clear activePreset)
|
||||||
|
await this.filterManager.applyFilters(false, true);
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
translate('toast.presets.applied', { name }, `Preset "${name}" applied`),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
// Silently handle abort errors
|
||||||
|
if (error.name === 'AbortError') return;
|
||||||
|
|
||||||
|
console.error('Error applying preset:', error);
|
||||||
|
showToast(translate('toast.error.applyPresetFailed', {}, 'Failed to apply preset'), {}, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUserCreatedPresets() {
|
||||||
|
// Check in settings first
|
||||||
|
const allPresets = this.getPresetsFromSettings();
|
||||||
|
const presets = allPresets[this.currentPage];
|
||||||
|
if (presets && Array.isArray(presets) && presets.length > 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback to localStorage
|
||||||
|
const presetsKey = this.getPresetsStorageKey();
|
||||||
|
const localPresets = getStorageItem(presetsKey);
|
||||||
|
return Array.isArray(localPresets) && localPresets.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
async restoreDefaultPresets() {
|
||||||
|
const defaultPresets = this.getDefaultPresets();
|
||||||
|
await this.savePresets(defaultPresets);
|
||||||
|
this.renderPresets();
|
||||||
|
showToast(
|
||||||
|
translate('toast.presets.restored', {}, 'Default presets restored'),
|
||||||
|
{},
|
||||||
|
'success'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the add button should be disabled
|
||||||
|
* Returns true if no filters are active OR a preset is already active
|
||||||
|
*/
|
||||||
|
shouldDisableAddButton() {
|
||||||
|
return !this.filterManager?.hasActiveFilters() || this.activePreset !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the add button's disabled state
|
||||||
|
*/
|
||||||
|
updateAddButtonState() {
|
||||||
|
const addBtn = document.querySelector('.add-preset-btn');
|
||||||
|
if (!addBtn) return;
|
||||||
|
|
||||||
|
const shouldDisable = this.shouldDisableAddButton();
|
||||||
|
|
||||||
|
if (shouldDisable) {
|
||||||
|
addBtn.classList.add('disabled');
|
||||||
|
// Update tooltip to explain why it's disabled
|
||||||
|
if (this.activePreset) {
|
||||||
|
addBtn.title = translate('header.filter.savePresetDisabledActive', {}, 'Cannot save: A preset is already active. Clear filters to save new preset.');
|
||||||
|
} else {
|
||||||
|
addBtn.title = translate('header.filter.savePresetDisabledNoFilters', {}, 'Select filters first to save as preset');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
addBtn.classList.remove('disabled');
|
||||||
|
addBtn.title = translate('header.filter.savePreset', {}, 'Save current filters as a new preset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initiate two-step delete process
|
||||||
|
*/
|
||||||
|
initiateDelete(presetName, deleteBtn) {
|
||||||
|
// If already pending for this preset, execute the delete
|
||||||
|
if (this.pendingDeletePreset === presetName) {
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
this.deletePreset(presetName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel any previous pending delete
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
|
||||||
|
// Set up new pending delete
|
||||||
|
this.pendingDeletePreset = presetName;
|
||||||
|
deleteBtn.classList.add('confirm');
|
||||||
|
deleteBtn.innerHTML = '<i class="fas fa-check"></i>';
|
||||||
|
deleteBtn.title = translate('header.filter.presetDeleteConfirmClick', {}, 'Click again to confirm');
|
||||||
|
|
||||||
|
// Auto-cancel after timeout
|
||||||
|
this.pendingDeleteTimeout = setTimeout(() => {
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
}, DELETE_CONFIRM_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancel pending delete operation
|
||||||
|
*/
|
||||||
|
cancelPendingDelete() {
|
||||||
|
if (this.pendingDeleteTimeout) {
|
||||||
|
clearTimeout(this.pendingDeleteTimeout);
|
||||||
|
this.pendingDeleteTimeout = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.pendingDeletePreset) {
|
||||||
|
// Reset all delete buttons to normal state
|
||||||
|
const deleteBtns = document.querySelectorAll('.preset-delete-btn.confirm');
|
||||||
|
deleteBtns.forEach(btn => {
|
||||||
|
btn.classList.remove('confirm');
|
||||||
|
btn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
btn.title = translate('header.filter.presetDeleteTooltip', {}, 'Delete preset');
|
||||||
|
});
|
||||||
|
this.pendingDeletePreset = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show inline input for preset naming
|
||||||
|
*/
|
||||||
|
showInlineNamingInput() {
|
||||||
|
if (this.isInlineNamingActive) return;
|
||||||
|
|
||||||
|
// Check if there are any active filters
|
||||||
|
if (!this.filterManager?.hasActiveFilters()) {
|
||||||
|
showToast(translate('toast.filters.noActiveFilters', {}, 'No active filters to save'), {}, 'info');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check max presets limit before showing input
|
||||||
|
const presets = this.loadPresets();
|
||||||
|
if (presets.length >= MAX_PRESETS_COUNT) {
|
||||||
|
showToast(
|
||||||
|
translate('toast.error.maxPresetsReached', { max: MAX_PRESETS_COUNT }, `Maximum ${MAX_PRESETS_COUNT} presets allowed. Delete one to add more.`),
|
||||||
|
{},
|
||||||
|
'error'
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.isInlineNamingActive = true;
|
||||||
|
|
||||||
|
const presetsContainer = document.getElementById('filterPresets');
|
||||||
|
if (!presetsContainer) return;
|
||||||
|
|
||||||
|
// Find the add button and hide it
|
||||||
|
const addBtn = presetsContainer.querySelector('.add-preset-btn');
|
||||||
|
if (addBtn) {
|
||||||
|
addBtn.style.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create inline input container
|
||||||
|
const inputContainer = document.createElement('div');
|
||||||
|
inputContainer.className = 'preset-inline-input-container';
|
||||||
|
inputContainer.innerHTML = `
|
||||||
|
<input type="text"
|
||||||
|
class="preset-inline-input"
|
||||||
|
placeholder="${translate('header.filter.presetNamePlaceholder', {}, 'Preset name...')}"
|
||||||
|
maxlength="${MAX_PRESET_NAME_LENGTH}">
|
||||||
|
<button class="preset-inline-btn save" title="${translate('common.actions.save', {}, 'Save')}">
|
||||||
|
<i class="fas fa-check"></i>
|
||||||
|
</button>
|
||||||
|
<button class="preset-inline-btn cancel" title="${translate('common.actions.cancel', {}, 'Cancel')}">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
|
||||||
|
presetsContainer.appendChild(inputContainer);
|
||||||
|
|
||||||
|
const input = inputContainer.querySelector('.preset-inline-input');
|
||||||
|
const saveBtn = inputContainer.querySelector('.preset-inline-btn.save');
|
||||||
|
const cancelBtn = inputContainer.querySelector('.preset-inline-btn.cancel');
|
||||||
|
|
||||||
|
// Focus input
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
// Handle save
|
||||||
|
const handleSave = async () => {
|
||||||
|
const name = input.value;
|
||||||
|
if (await this.createPreset(name)) {
|
||||||
|
this.hideInlineNamingInput();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Handle cancel
|
||||||
|
const handleCancel = () => {
|
||||||
|
this.hideInlineNamingInput();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Event listeners
|
||||||
|
saveBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
cancelBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleCancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keydown', (e) => {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSave();
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault();
|
||||||
|
handleCancel();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prevent clicks inside from bubbling
|
||||||
|
inputContainer.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide inline input and restore add button
|
||||||
|
*/
|
||||||
|
hideInlineNamingInput() {
|
||||||
|
this.isInlineNamingActive = false;
|
||||||
|
|
||||||
|
const presetsContainer = document.getElementById('filterPresets');
|
||||||
|
if (!presetsContainer) return;
|
||||||
|
|
||||||
|
// Remove input container
|
||||||
|
const inputContainer = presetsContainer.querySelector('.preset-inline-input-container');
|
||||||
|
if (inputContainer) {
|
||||||
|
inputContainer.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show add button
|
||||||
|
const addBtn = presetsContainer.querySelector('.add-preset-btn');
|
||||||
|
if (addBtn) {
|
||||||
|
addBtn.style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
renderPresets() {
|
||||||
|
const presetsContainer = document.getElementById('filterPresets');
|
||||||
|
if (!presetsContainer) return;
|
||||||
|
|
||||||
|
// Cancel any pending delete when re-rendering
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
this.isInlineNamingActive = false;
|
||||||
|
|
||||||
|
const presets = this.loadPresets();
|
||||||
|
presetsContainer.innerHTML = '';
|
||||||
|
|
||||||
|
// Show empty state with restore option if no presets
|
||||||
|
if (presets.length === 0) {
|
||||||
|
const emptyState = document.createElement('div');
|
||||||
|
emptyState.className = 'presets-empty-state';
|
||||||
|
emptyState.style.cssText = 'width: 100%; padding: 12px; text-align: center;';
|
||||||
|
|
||||||
|
const noPresetsMsg = document.createElement('div');
|
||||||
|
noPresetsMsg.className = 'no-presets';
|
||||||
|
noPresetsMsg.style.cssText = 'margin-bottom: 8px;';
|
||||||
|
noPresetsMsg.textContent = translate('header.filter.noPresets', {}, 'No presets saved yet. Select filters below and click + to save');
|
||||||
|
|
||||||
|
const restoreLink = document.createElement('button');
|
||||||
|
restoreLink.className = 'restore-defaults-btn';
|
||||||
|
restoreLink.style.cssText = 'background: none; border: none; color: var(--lora-accent); cursor: pointer; font-size: 13px; text-decoration: underline; padding: 4px 8px;';
|
||||||
|
restoreLink.textContent = translate('header.filter.restoreDefaults', {}, 'Restore defaults');
|
||||||
|
restoreLink.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.restoreDefaultPresets();
|
||||||
|
});
|
||||||
|
|
||||||
|
emptyState.appendChild(noPresetsMsg);
|
||||||
|
emptyState.appendChild(restoreLink);
|
||||||
|
presetsContainer.appendChild(emptyState);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render existing presets
|
||||||
|
presets.forEach(preset => {
|
||||||
|
const presetEl = document.createElement('div');
|
||||||
|
presetEl.className = 'filter-preset';
|
||||||
|
|
||||||
|
const isActive = this.activePreset === preset.name;
|
||||||
|
if (isActive) {
|
||||||
|
presetEl.classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
presetEl.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
});
|
||||||
|
|
||||||
|
const presetName = document.createElement('span');
|
||||||
|
presetName.className = 'preset-name';
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
presetName.innerHTML = `<i class="fas fa-check"></i> ${preset.name}`;
|
||||||
|
} else {
|
||||||
|
presetName.textContent = preset.name;
|
||||||
|
}
|
||||||
|
presetName.title = translate('header.filter.presetClickTooltip', { name: preset.name }, `Click to apply preset "${preset.name}"`);
|
||||||
|
|
||||||
|
const deleteBtn = document.createElement('button');
|
||||||
|
deleteBtn.className = 'preset-delete-btn';
|
||||||
|
deleteBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
|
deleteBtn.title = translate('header.filter.presetDeleteTooltip', {}, 'Delete preset');
|
||||||
|
|
||||||
|
// Apply preset on name click (toggle if already active)
|
||||||
|
presetName.addEventListener('click', async (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
|
||||||
|
if (this.activePreset === preset.name) {
|
||||||
|
await this.filterManager.clearFilters();
|
||||||
|
} else {
|
||||||
|
await this.applyPreset(preset.name);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Two-step delete on delete button click
|
||||||
|
deleteBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.initiateDelete(preset.name, deleteBtn);
|
||||||
|
});
|
||||||
|
|
||||||
|
presetEl.appendChild(presetName);
|
||||||
|
presetEl.appendChild(deleteBtn);
|
||||||
|
presetsContainer.appendChild(presetEl);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the "Add new preset" button as the last element
|
||||||
|
const addBtn = document.createElement('div');
|
||||||
|
addBtn.className = 'filter-preset add-preset-btn';
|
||||||
|
addBtn.innerHTML = `<i class="fas fa-plus"></i> ${translate('common.actions.add', {}, 'Add')}`;
|
||||||
|
addBtn.title = translate('header.filter.savePreset', {}, 'Save current filters as a new preset');
|
||||||
|
|
||||||
|
addBtn.addEventListener('click', (e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
this.cancelPendingDelete();
|
||||||
|
this.showInlineNamingInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
presetsContainer.appendChild(addBtn);
|
||||||
|
|
||||||
|
// Update add button state
|
||||||
|
this.updateAddButtonState();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Legacy method for backward compatibility
|
||||||
|
* @deprecated Use showInlineNamingInput instead
|
||||||
|
*/
|
||||||
|
showSavePresetDialog() {
|
||||||
|
this.showInlineNamingInput();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user