Deprecate legacy loader and migrate all callsites to action-loader system with informative toasts (#5326) * Refactor loader.js to use action-loader system and move overlay management into action-loader module - Deprecate showLoader() and hideLoader() in favor of action-loader API - Implement legacy functions as thin wrappers around ActionLoaderHandle - Move overlay management (showOverlay, hideOverlay, isOverlayDisplayed) into action-loader.js - Move Popup-based loader implementation and preloader cleanup to action-loader - Add loader.isBlocking() method to check for active blocking overlays * Migrate from legacy loader functions to action-loader API throughout codebase - Replace showLoader()/hideLoader() imports with loader from action-loader.js - Update firstLoadInit() to use loader.show() with title, message, and ToastMode.STATIC - Pass initLoaderHandle to getSettings() for early hide during onboarding flow - Refactor renameGroupOrCharacterChat() to use loader.show() instead of boolean flag - Wrap handleDeleteChat() with loader.show() and proper error handling - Update BulkEditOver... * Update loader titles and remove redundant reload notification - Change bookmark loader title from "Bookmark" to "Chat History" for clarity - Remove loader notification before extensions reload (redundant with browser reload) * lint fix * Add splash screen support to action loader with custom overlay content - Add `overlayContent` option to ActionLoaderOptions for custom HTML in overlay - Implement splash screen styles with centered logo, spinner, and message - Update firstLoadInit() to use custom splash screen instead of static toast - Pass custom content through showOverlay() to replace default spinner - Adjust non-blocking loader warning to account for custom overlay content * Refactor loader overlay to use DOM elements instead of HTML strings - Add createDefaultLoaderOverlay() function to generate fresh loader overlay elements - Export createOverlay() method on loader utility API for external use - Change overlayContent parameter type from string-only to string|HTMLElement|null - Add getOverlayContent() helper to normalize custom content for Popup - Update firstLoadInit() to build splash screen using DOM manipulation instead of template literals - Add splash-logo class and * Use a true ellipsis * Adjust sizing for desktop * Even truer ellipsis * Add transition to splash screen and fix blur animation on hideOverlay (#5338) * Initial plan * Blur entire splash screen on hideOverlay, not just spinner Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/eee6c06d-7c9d-4363-bc8f-2647ed390368 * Add transition to splash-screen and fix transition detection Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/9368bc36-31a0-4a58-aebd-7b569696ff2e --------- Co-authored-by: anthropic-code-agent[bot] <242468646+Claude@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Add translations to supported locales * Localize logo alt on welcome screen --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com> Co-authored-by: Claude <242468646+Claude@users.noreply.github.com>
Signed| @@ -53,3 +53,36 @@ | ||
| 53 | 53 | opacity: 1; |
| 54 | 54 | color: color-mix(in srgb, currentColor 40%, #e74c3c 60%); |
| 55 | 55 | } |
| 56 | + | |
| 57 | +/* Splash screen styles for branded loading */ | |
| 58 | +#loader.splash-screen { | |
| 59 | + display: flex; | |
| 60 | + flex-direction: column; | |
| 61 | + align-items: center; | |
| 62 | + justify-content: center; | |
| 63 | + gap: 4rem; | |
| 64 | + width: 100%; | |
| 65 | + height: 100%; | |
| 66 | + transition: all var(--animation-duration-2x) ease-out; | |
| 67 | +} | |
| 68 | + | |
| 69 | +#loader.splash-screen #load-spinner { | |
| 70 | + position: static; | |
| 71 | + top: unset; | |
| 72 | + left: unset; | |
| 73 | +} | |
| 74 | + | |
| 75 | +.splash-logo { | |
| 76 | + /* max original px, or 50% on mobile */ | |
| 77 | + width: min(150px, 50%); | |
| 78 | + height: auto; | |
| 79 | + filter: drop-shadow(0 4px 8px rgba(0, 0, 0, 0.3)); | |
| 80 | +} | |
| 81 | + | |
| 82 | +.splash-message { | |
| 83 | + margin: 0; | |
| 84 | + font-size: 1.25rem; | |
| 85 | + font-weight: 500; | |
| 86 | + opacity: 0.9; | |
| 87 | + letter-spacing: 0.02em; | |
| 88 | +} | |
| @@ -1444,5 +1444,15 @@ | ||
| 1444 | 1444 | "Interleaved Thinking": "التفكير المتداخل", |
| 1445 | 1445 | "Since Last User Message": "منذ آخر رسالة من المستخدم", |
| 1446 | 1446 | "Active Tool Chain": "سلسلة الأدوات النشطة", |
| 1447 | 1447 | "openrouter_interleaved_thinking_hint": "يرسل الاستدلال من رسائل المساعد السابقة مع طلبات استدعاء الأدوات للحفاظ على سياق التفكير المتداخل.", |
| 1448 | + "SillyTavern Logo": "شعار SillyTavern", | |
| 1449 | + "Initializing…": "جارٍ التهيئة…", | |
| 1450 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "الاسم غير مقبول، لأنه نفسه كما كان من قبل (تجاهل حالة الأحرف والنبرات).", | |
| 1451 | + "Rename Chat": "إعادة تسمية المحادثة", | |
| 1452 | + "Renaming chat…": "جارٍ إعادة تسمية المحادثة…", | |
| 1453 | + "Delete Chat": "حذف المحادثة", | |
| 1454 | + "Deleting chat…": "جارٍ حذف المحادثة…", | |
| 1455 | + "Loading chat…": "جارٍ تحميل المحادثة…", | |
| 1456 | + "Bulk Delete": "حذف جماعي", | |
| 1457 | + "Deleting ${0} character(s)…": "جارٍ حذف ${0} شخصية (شخصيات)…" | |
| 1448 | 1458 | } |
| @@ -1444,5 +1444,15 @@ | ||
| 1444 | 1444 | "Interleaved Thinking": "Verschränktes Denken", |
| 1445 | 1445 | "Since Last User Message": "Seit letzter Nutzernachricht", |
| 1446 | 1446 | "Active Tool Chain": "Aktive Tool-Kette", |
| 1447 | 1447 | "openrouter_interleaved_thinking_hint": "Sendet Begründungen aus vorherigen Assistentennachrichten mit Tool-Aufrufen, um den Kontext für verschränktes Denken zu erhalten.", |
| 1448 | + "SillyTavern Logo": "SillyTavern-Logo", | |
| 1449 | + "Initializing…": "Initialisierung…", | |
| 1450 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Name nicht akzeptiert, da er derselbe wie zuvor ist (Groß-/Kleinschreibung und Akzente ignoriert).", | |
| 1451 | + "Rename Chat": "Chat umbenennen", | |
| 1452 | + "Renaming chat…": "Chat wird umbenannt…", | |
| 1453 | + "Delete Chat": "Chat löschen", | |
| 1454 | + "Deleting chat…": "Chat wird gelöscht…", | |
| 1455 | + "Loading chat…": "Chat wird geladen…", | |
| 1456 | + "Bulk Delete": "Massenlöschung", | |
| 1457 | + "Deleting ${0} character(s)…": "${0} Charakter(e) werden gelöscht…" | |
| 1448 | 1458 | } |
| @@ -1551,5 +1551,15 @@ | ||
| 1551 | 1551 | "Interleaved Thinking": "Razonamiento intercalado", |
| 1552 | 1552 | "Since Last User Message": "Desde el último mensaje del usuario", |
| 1553 | 1553 | "Active Tool Chain": "Cadena de herramientas activa", |
| 1554 | 1554 | "openrouter_interleaved_thinking_hint": "Envía razonamiento de turnos anteriores del asistente junto con solicitudes de herramientas para mantener el contexto del razonamiento intercalado.", |
| 1555 | + "SillyTavern Logo": "Logo de SillyTavern", | |
| 1556 | + "Initializing…": "Inicializando…", | |
| 1557 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nombre no aceptado, ya que es el mismo que antes (ignorando mayúsculas y acentos).", | |
| 1558 | + "Rename Chat": "Renombrar chat", | |
| 1559 | + "Renaming chat…": "Renombrando chat…", | |
| 1560 | + "Delete Chat": "Eliminar chat", | |
| 1561 | + "Deleting chat…": "Eliminando chat…", | |
| 1562 | + "Loading chat…": "Cargando chat…", | |
| 1563 | + "Bulk Delete": "Eliminación masiva", | |
| 1564 | + "Deleting ${0} character(s)…": "Eliminando ${0} personaje(s)…" | |
| 1555 | 1565 | } |
| @@ -2052,5 +2052,15 @@ | ||
| 2052 | 2052 | "Interleaved Thinking": "Raisonnement intercalé", |
| 2053 | 2053 | "Since Last User Message": "Depuis le dernier message utilisateur", |
| 2054 | 2054 | "Active Tool Chain": "Chaîne d'outils active", |
| 2055 | 2055 | "openrouter_interleaved_thinking_hint": "Envoie le raisonnement des tours précédents de l'assistant avec les requêtes d'outils pour conserver le contexte du raisonnement intercalé.", |
| 2056 | + "SillyTavern Logo": "Logo SillyTavern", | |
| 2057 | + "Initializing…": "Initialisation…", | |
| 2058 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nom non accepté, car il est identique à l'ancien (en ignorant la casse et les accents).", | |
| 2059 | + "Rename Chat": "Renommer la discussion", | |
| 2060 | + "Renaming chat…": "Renommage de la discussion…", | |
| 2061 | + "Delete Chat": "Supprimer la discussion", | |
| 2062 | + "Deleting chat…": "Suppression de la discussion…", | |
| 2063 | + "Loading chat…": "Chargement de la discussion…", | |
| 2064 | + "Bulk Delete": "Suppression groupée", | |
| 2065 | + "Deleting ${0} character(s)…": "Suppression de ${0} personnage(s)…" | |
| 2056 | 2066 | } |
| @@ -1442,5 +1442,15 @@ | ||
| 1442 | 1442 | "Interleaved Thinking": "Fléttuð hugsun", |
| 1443 | 1443 | "Since Last User Message": "Frá síðustu notandaskilaboðum", |
| 1444 | 1444 | "Active Tool Chain": "Virk verkfærakeðja", |
| 1445 | 1445 | "openrouter_interleaved_thinking_hint": "Sendir röksemdir úr fyrri aðstoðarskilaboðum með verkfæraköllum til að viðhalda samhengi fléttaðrar hugsunar.", |
| 1446 | + "SillyTavern Logo": "SillyTavern merki", | |
| 1447 | + "Initializing…": "Ræsir…", | |
| 1448 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nafn ekki samþykkt, því það er það sama og áður (hunsar há-/lágstafi og brottmerki).", | |
| 1449 | + "Rename Chat": "Endurnefna spjall", | |
| 1450 | + "Renaming chat…": "Endurnefni spjall…", | |
| 1451 | + "Delete Chat": "Eyða spjalli", | |
| 1452 | + "Deleting chat…": "Eyði spjalli…", | |
| 1453 | + "Loading chat…": "Hleð spjalli…", | |
| 1454 | + "Bulk Delete": "Fjöldaeyðing", | |
| 1455 | + "Deleting ${0} character(s)…": "Eyði ${0} persónu(m)…" | |
| 1446 | 1456 | } |
| @@ -1444,5 +1444,15 @@ | ||
| 1444 | 1444 | "Interleaved Thinking": "Ragionamento intercalato", |
| 1445 | 1445 | "Since Last User Message": "Dall'ultimo messaggio utente", |
| 1446 | 1446 | "Active Tool Chain": "Catena di strumenti attiva", |
| 1447 | 1447 | "openrouter_interleaved_thinking_hint": "Invia il ragionamento dai turni precedenti dell'assistente con le richieste di strumenti per mantenere il contesto del ragionamento intercalato.", |
| 1448 | + "SillyTavern Logo": "Logo SillyTavern", | |
| 1449 | + "Initializing…": "Inizializzazione…", | |
| 1450 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome non accettato, poiché è lo stesso di prima (ignorando maiuscole e accenti).", | |
| 1451 | + "Rename Chat": "Rinomina chat", | |
| 1452 | + "Renaming chat…": "Rinominazione chat…", | |
| 1453 | + "Delete Chat": "Elimina chat", | |
| 1454 | + "Deleting chat…": "Eliminazione chat…", | |
| 1455 | + "Loading chat…": "Caricamento chat…", | |
| 1456 | + "Bulk Delete": "Eliminazione multipla", | |
| 1457 | + "Deleting ${0} character(s)…": "Eliminazione di ${0} personaggio/i…" | |
| 1448 | 1458 | } |
| @@ -1449,5 +1449,15 @@ | ||
| 1449 | 1449 | "Interleaved Thinking": "インターリーブ思考", |
| 1450 | 1450 | "Since Last User Message": "直前のユーザーメッセージ以降", |
| 1451 | 1451 | "Active Tool Chain": "アクティブなツールチェーン", |
| 1452 | 1452 | "openrouter_interleaved_thinking_hint": "インターリーブ思考の文脈を維持するため、直前のアシスタントターンの推論をツール呼び出しリクエストとともに送信します。", |
| 1453 | + "SillyTavern Logo": "SillyTavernロゴ", | |
| 1454 | + "Initializing…": "初期化中…", | |
| 1455 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "名前が受け入れられませんでした。以前と同じです(大文字小文字とアクセントを無視)。", | |
| 1456 | + "Rename Chat": "チャット名を変更", | |
| 1457 | + "Renaming chat…": "チャット名を変更中…", | |
| 1458 | + "Delete Chat": "チャットを削除", | |
| 1459 | + "Deleting chat…": "チャットを削除中…", | |
| 1460 | + "Loading chat…": "チャットを読み込み中…", | |
| 1461 | + "Bulk Delete": "一括削除", | |
| 1462 | + "Deleting ${0} character(s)…": "${0}人のキャラクターを削除中…" | |
| 1453 | 1463 | } |
| @@ -1623,5 +1623,15 @@ | ||
| 1623 | 1623 | "Interleaved Thinking": "인터리브드 사고", |
| 1624 | 1624 | "Since Last User Message": "마지막 사용자 메시지 이후", |
| 1625 | 1625 | "Active Tool Chain": "활성 도구 체인", |
| 1626 | 1626 | "openrouter_interleaved_thinking_hint": "인터리브드 사고 컨텍스트를 유지하기 위해 이전 어시스턴트 턴의 추론을 도구 호출 요청과 함께 전송합니다.", |
| 1627 | + "SillyTavern Logo": "SillyTavern 로고", | |
| 1628 | + "Initializing…": "초기화 중…", | |
| 1629 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "이름이 승인되지 않았습니다. 이전과 동일합니다(대소문자 및 악센트 무시).", | |
| 1630 | + "Rename Chat": "채팅 이름 변경", | |
| 1631 | + "Renaming chat…": "채팅 이름 변경 중…", | |
| 1632 | + "Delete Chat": "채팅 삭제", | |
| 1633 | + "Deleting chat…": "채팅 삭제 중…", | |
| 1634 | + "Loading chat…": "채팅 불러오는 중…", | |
| 1635 | + "Bulk Delete": "대량 삭제", | |
| 1636 | + "Deleting ${0} character(s)…": "${0}개 캐릭터 삭제 중…" | |
| 1627 | 1637 | } |
| @@ -1440,5 +1440,15 @@ | ||
| 1440 | 1440 | "Interleaved Thinking": "Verweven redenering", |
| 1441 | 1441 | "Since Last User Message": "Sinds laatste gebruikersbericht", |
| 1442 | 1442 | "Active Tool Chain": "Actieve toolketen", |
| 1443 | 1443 | "openrouter_interleaved_thinking_hint": "Verstuurt redenering uit eerdere assistentbeurten met tool-aanvragen om de context van verweven redenering te behouden.", |
| 1444 | + "SillyTavern Logo": "SillyTavern-logo", | |
| 1445 | + "Initializing…": "Initialiseren…", | |
| 1446 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Naam niet geaccepteerd, omdat deze hetzelfde is als voorheen (hoofdletters en accenten genegeerd).", | |
| 1447 | + "Rename Chat": "Chat hernoemen", | |
| 1448 | + "Renaming chat…": "Chat hernoemen…", | |
| 1449 | + "Delete Chat": "Chat verwijderen", | |
| 1450 | + "Deleting chat…": "Chat verwijderen…", | |
| 1451 | + "Loading chat…": "Chat laden…", | |
| 1452 | + "Bulk Delete": "Bulkverwijdering", | |
| 1453 | + "Deleting ${0} character(s)…": "${0} personage(s) verwijderen…" | |
| 1444 | 1454 | } |
| @@ -1442,5 +1442,15 @@ | ||
| 1442 | 1442 | "Interleaved Thinking": "Raciocínio intercalado", |
| 1443 | 1443 | "Since Last User Message": "Desde a última mensagem do utilizador", |
| 1444 | 1444 | "Active Tool Chain": "Cadeia de ferramentas ativa", |
| 1445 | 1445 | "openrouter_interleaved_thinking_hint": "Envia o raciocínio de turnos anteriores do assistente com pedidos de ferramentas para manter o contexto de raciocínio intercalado.", |
| 1446 | + "SillyTavern Logo": "Logótipo do SillyTavern", | |
| 1447 | + "Initializing…": "A inicializar…", | |
| 1448 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Nome não aceite, pois é o mesmo que antes (ignorando maiúsculas e acentos).", | |
| 1449 | + "Rename Chat": "Renomear chat", | |
| 1450 | + "Renaming chat…": "A renomear chat…", | |
| 1451 | + "Delete Chat": "Eliminar chat", | |
| 1452 | + "Deleting chat…": "A eliminar chat…", | |
| 1453 | + "Loading chat…": "A carregar chat…", | |
| 1454 | + "Bulk Delete": "Eliminação em massa", | |
| 1455 | + "Deleting ${0} character(s)…": "A eliminar ${0} personagem(ns)…" | |
| 1446 | 1456 | } |
| @@ -2744,5 +2744,15 @@ | ||
| 2744 | 2744 | "Interleaved Thinking": "Чередующееся рассуждение", |
| 2745 | 2745 | "Since Last User Message": "С момента последнего сообщения пользователя", |
| 2746 | 2746 | "Active Tool Chain": "Активная цепочка инструментов", |
| 2747 | 2747 | "openrouter_interleaved_thinking_hint": "Отправляет рассуждение из предыдущих ходов ассистента вместе с запросами вызова инструментов, чтобы сохранять контекст чередующегося рассуждения.", |
| 2748 | + "SillyTavern Logo": "Логотип SillyTavern", | |
| 2749 | + "Initializing…": "Инициализация…", | |
| 2750 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Имя не принято, так как оно совпадает с предыдущим (игнорируя регистр и акценты).", | |
| 2751 | + "Rename Chat": "Переименовать чат", | |
| 2752 | + "Renaming chat…": "Переименование чата…", | |
| 2753 | + "Delete Chat": "Удалить чат", | |
| 2754 | + "Deleting chat…": "Удаление чата…", | |
| 2755 | + "Loading chat…": "Загрузка чата…", | |
| 2756 | + "Bulk Delete": "Массовое удаление", | |
| 2757 | + "Deleting ${0} character(s)…": "Удаление ${0} персонажа(ей)…" | |
| 2748 | 2758 | } |
| @@ -1461,5 +1461,15 @@ | ||
| 1461 | 1461 | "Interleaved Thinking": "การคิดแบบสลับขั้น", |
| 1462 | 1462 | "Since Last User Message": "ตั้งแต่ข้อความล่าสุดของผู้ใช้", |
| 1463 | 1463 | "Active Tool Chain": "สายโซ่เครื่องมือที่ใช้งานอยู่", |
| 1464 | 1464 | "openrouter_interleaved_thinking_hint": "ส่งเหตุผลจากเทิร์นก่อนหน้าของผู้ช่วยไปพร้อมกับคำขอเรียกใช้เครื่องมือ เพื่อคงบริบทการคิดแบบสลับขั้นไว้", |
| 1465 | + "SillyTavern Logo": "โลโก้ SillyTavern", | |
| 1466 | + "Initializing…": "กำลังเริ่มต้น…", | |
| 1467 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "ชื่อไม่ได้รับการยอมรับ เนื่องจากเหมือนกับก่อนหน้านี้ (ไม่สนใจตัวพิมพ์ใหญ่เล็กและเครื่องหมายเสียง)", | |
| 1468 | + "Rename Chat": "เปลี่ยนชื่อแชท", | |
| 1469 | + "Renaming chat…": "กำลังเปลี่ยนชื่อแชท…", | |
| 1470 | + "Delete Chat": "ลบแชท", | |
| 1471 | + "Deleting chat…": "กำลังลบแชท…", | |
| 1472 | + "Loading chat…": "กำลังโหลดแชท…", | |
| 1473 | + "Bulk Delete": "ลบจำนวนมาก", | |
| 1474 | + "Deleting ${0} character(s)…": "กำลังลบ ${0} ตัวละคร…" | |
| 1465 | 1475 | } |
| @@ -1442,5 +1442,15 @@ | ||
| 1442 | 1442 | "Interleaved Thinking": "Черговане мислення", |
| 1443 | 1443 | "Since Last User Message": "Від останнього повідомлення користувача", |
| 1444 | 1444 | "Active Tool Chain": "Активний ланцюжок інструментів", |
| 1445 | 1445 | "openrouter_interleaved_thinking_hint": "Надсилає міркування з попередніх ходів асистента разом із запитами виклику інструментів, щоб зберегти контекст чергованого мислення.", |
| 1446 | + "SillyTavern Logo": "Логотип SillyTavern", | |
| 1447 | + "Initializing…": "Ініціалізація…", | |
| 1448 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Ім'я не прийнято, оскільки воно збігається з попереднім (ігноруючи регістр та акценти).", | |
| 1449 | + "Rename Chat": "Перейменувати чат", | |
| 1450 | + "Renaming chat…": "Перейменування чату…", | |
| 1451 | + "Delete Chat": "Видалити чат", | |
| 1452 | + "Deleting chat…": "Видалення чату…", | |
| 1453 | + "Loading chat…": "Завантаження чату…", | |
| 1454 | + "Bulk Delete": "Масове видалення", | |
| 1455 | + "Deleting ${0} character(s)…": "Видалення ${0} персонажа(ів)…" | |
| 1446 | 1456 | } |
| @@ -1442,5 +1442,15 @@ | ||
| 1442 | 1442 | "Interleaved Thinking": "Lập luận xen kẽ", |
| 1443 | 1443 | "Since Last User Message": "Kể từ tin nhắn người dùng gần nhất", |
| 1444 | 1444 | "Active Tool Chain": "Chuỗi công cụ đang hoạt động", |
| 1445 | 1445 | "openrouter_interleaved_thinking_hint": "Gửi lập luận từ các lượt trợ lý trước đó cùng với yêu cầu gọi công cụ để duy trì ngữ cảnh lập luận xen kẽ.", |
| 1446 | + "SillyTavern Logo": "Logo SillyTavern", | |
| 1447 | + "Initializing…": "Đang khởi tạo…", | |
| 1448 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "Tên không được chấp nhận vì giống như trước đó (bỏ qua chữ hoa/thường và dấu).", | |
| 1449 | + "Rename Chat": "Đổi tên cuộc trò chuyện", | |
| 1450 | + "Renaming chat…": "Đang đổi tên cuộc trò chuyện…", | |
| 1451 | + "Delete Chat": "Xóa cuộc trò chuyện", | |
| 1452 | + "Deleting chat…": "Đang xóa cuộc trò chuyện…", | |
| 1453 | + "Loading chat…": "Đang tải cuộc trò chuyện…", | |
| 1454 | + "Bulk Delete": "Xóa hàng loạt", | |
| 1455 | + "Deleting ${0} character(s)…": "Đang xóa ${0} nhân vật…" | |
| 1446 | 1456 | } |
| @@ -3635,5 +3635,13 @@ | ||
| 3635 | 3635 | "sd_prompt_9": "你的人设 (多模态模式)", |
| 3636 | 3636 | "Show only favorites": "仅显示收藏", |
| 3637 | 3637 | "Show only groups": "仅显示群聊", |
| 3638 | 3638 | "Show Tag List": "显示标签列表", |
| 3639 | + "SillyTavern Logo": "SillyTavern 徽标", | |
| 3640 | + "Initializing…": "正在初始化…", | |
| 3641 | + "Renaming chat…": "正在重命名聊天…", | |
| 3642 | + "Delete Chat": "删除聊天", | |
| 3643 | + "Deleting chat…": "正在删除聊天…", | |
| 3644 | + "Loading chat…": "正在加载聊天…", | |
| 3645 | + "Bulk Delete": "批量删除", | |
| 3646 | + "Deleting ${0} character(s)…": "正在删除 ${0} 个角色…" | |
| 3639 | 3647 | } |
| @@ -2764,5 +2764,15 @@ | ||
| 2764 | 2764 | "Interleaved Thinking": "交錯思維", |
| 2765 | 2765 | "Since Last User Message": "自上一則使用者訊息起", |
| 2766 | 2766 | "Active Tool Chain": "使用中的工具鏈", |
| 2767 | 2767 | "openrouter_interleaved_thinking_hint": "會在工具呼叫請求中附帶先前助理回合的推理,以維持交錯思維的上下文。", |
| 2768 | + "SillyTavern Logo": "SillyTavern 標誌", | |
| 2769 | + "Initializing…": "正在初始化…", | |
| 2770 | + "Name not accepted, as it is the same as before (ignoring case and accents).": "名稱不被接受,因為它與之前相同(忽略大小寫和重音)。", | |
| 2771 | + "Rename Chat": "重新命名聊天", | |
| 2772 | + "Renaming chat…": "正在重新命名聊天…", | |
| 2773 | + "Delete Chat": "刪除聊天", | |
| 2774 | + "Deleting chat…": "正在刪除聊天…", | |
| 2775 | + "Loading chat…": "正在載入聊天…", | |
| 2776 | + "Bulk Delete": "批次刪除", | |
| 2777 | + "Deleting ${0} character(s)…": "正在刪除 ${0} 個角色…" | |
| 2768 | 2778 | } |
| @@ -244,7 +244,7 @@ import { | ||
| 244 | 244 | isPersonaPanelOpen, |
| 245 | 245 | } from './scripts/personas.js'; |
| 246 | 246 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 247 | 247 | import { hideLoader, showLoaderloader } from './scripts/action-loader.js'; |
| 248 | 248 | import { BulkEditOverlay } from './scripts/BulkEditOverlay.js'; |
| 249 | 249 | import { initTextGenModels } from './scripts/textgen-models.js'; |
| 250 | 250 | import { appendFileContent, hasPendingFileAttachment, populateFileAttachment, decodeStyleTags, encodeStyleTags, isExternalMediaAllowed, preserveNeutralChat, restoreNeutralChat, formatCreatorNotes, initChatUtilities, addDOMPurifyHooks } from './scripts/chats.js'; |
| @@ -698,7 +698,28 @@ async function firstLoadInit() { | ||
| 698 | 698 | throw new Error('Initialization failed'); |
| 699 | 699 | } |
| 700 | 700 | |
| 701 | - showLoader(); | |
| 701 | + const initLoaderOverlay = loader.createOverlay(); | |
| 702 | + initLoaderOverlay.classList.add('splash-screen'); | |
| 703 | + | |
| 704 | + const splashLogo = document.createElement('img'); | |
| 705 | + splashLogo.src = '/img/logo.png'; | |
| 706 | + splashLogo.alt = 'SillyTavern'; | |
| 707 | + splashLogo.className = 'splash-logo'; | |
| 708 | + splashLogo.ariaLabel = t`SillyTavern Logo`; | |
| 709 | + | |
| 710 | + const splashMessage = document.createElement('h2'); | |
| 711 | + splashMessage.className = 'splash-message'; | |
| 712 | + splashMessage.textContent = t`Initializing…`; | |
| 713 | + splashMessage.dataset.i18n = 'Initializing…'; | |
| 714 | + | |
| 715 | + initLoaderOverlay.prepend(splashLogo); | |
| 716 | + initLoaderOverlay.appendChild(splashMessage); | |
| 717 | + | |
| 718 | + const initLoaderHandle = loader.show({ | |
| 719 | + toastMode: loader.ToastMode.NONE, | |
| 720 | + overlayContent: initLoaderOverlay, | |
| 721 | + }); | |
| 722 | + | |
| 702 | 723 | registerPromptManagerMigration(); |
| 703 | 724 | initDomHandlers(); |
| 704 | 725 | initStandaloneMode(); |
| @@ -724,7 +745,7 @@ async function firstLoadInit() { | ||
| 724 | 745 | ToolManager.initToolSlashCommands(); |
| 725 | 746 | await initPresetManager(); |
| 726 | 747 | await initSystemMessages(); |
| 727 | 748 | await getSettings(initLoaderHandle); |
| 728 | 749 | initKeyboard(); |
| 729 | 750 | initDynamicStyles(); |
| 730 | 751 | initTags(); |
| @@ -758,7 +779,7 @@ async function firstLoadInit() { | ||
| 758 | 779 | addDebugFunctions(); |
| 759 | 780 | doDailyExtensionUpdatesCheck(); |
| 760 | 781 | await eventSource.emit(event_types.APP_INITIALIZED); |
| 761 | 782 | await hideLoaderinitLoaderHandle.hide(); |
| 762 | 783 | await fixViewport(); |
| 763 | 784 | await eventSource.emit(event_types.APP_READY); |
| 764 | 785 | } |
| @@ -7763,7 +7784,7 @@ function reloadLoop() { | ||
| 7763 | 7784 | |
| 7764 | 7785 | //MARK: getSettings() |
| 7765 | 7786 | /////////////////////////////////////////// |
| 7766 | 7787 | export async function getSettings(initLoaderHandle = null) { |
| 7767 | 7788 | const response = await fetch('/api/settings/get', { |
| 7768 | 7789 | method: 'POST', |
| 7769 | 7790 | headers: getRequestHeaders(), |
| @@ -7881,7 +7902,7 @@ export async function getSettings() { | ||
| 7881 | 7902 | firstRun = !!settings.firstRun; |
| 7882 | 7903 | |
| 7883 | 7904 | if (firstRun) { |
| 7884 | 7905 | hideLoaderawait initLoaderHandle?.hide(); |
| 7885 | 7906 | await doOnboarding(user_avatar); |
| 7886 | 7907 | firstRun = false; |
| 7887 | 7908 | } |
| @@ -10457,7 +10478,7 @@ export async function doNewChat({ deleteCurrentChat = false } = {}) { | ||
| 10457 | 10478 | * @param {string} param.newFileName New name for the chat (no JSONL extension) |
| 10458 | 10479 | * @param {boolean} [param.loader=true] Whether to show loader during the operation |
| 10459 | 10480 | */ |
| 10460 | 10481 | export async function renameGroupOrCharacterChat({ characterId, groupId, oldFileName, newFileName, loader: showLoader }) { |
| 10461 | 10482 | const currentChatId = getCurrentChatId(); |
| 10462 | 10483 | const body = { |
| 10463 | 10484 | is_group: !!groupId, |
| @@ -10475,9 +10496,13 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile | ||
| 10475 | 10496 | return; |
| 10476 | 10497 | } |
| 10477 | 10498 | |
| 10478 | - try { | |
| 10499 | + const loaderHandle = showLoader ? loader.show({ | |
| 10479 | - loader && showLoader(); | |
| 10500 | + title: t`Rename Chat`, | |
| 10501 | + message: t`Renaming chat…`, | |
| 10502 | + toastMode: loader.ToastMode.STATIC, | |
| 10503 | + }) : null; | |
| 10480 | 10504 | |
| 10505 | + try { | |
| 10481 | 10506 | const response = await fetch('/api/chats/rename', { |
| 10482 | 10507 | method: 'POST', |
| 10483 | 10508 | body: JSON.stringify(body), |
| @@ -10510,11 +10535,10 @@ export async function renameGroupOrCharacterChat({ characterId, groupId, oldFile | ||
| 10510 | 10535 | await reloadCurrentChat(); |
| 10511 | 10536 | } |
| 10512 | 10537 | } catch { |
| 10513 | - loader && hideLoader(); | |
| 10514 | 10538 | await delay(500); |
| 10515 | 10539 | await callGenericPopup('An error has occurred. Chat was not renamed.', POPUP_TYPE.TEXT); |
| 10516 | 10540 | } finally { |
| 10517 | 10541 | loader &&await hideLoaderloaderHandle?.hide(); |
| 10518 | 10542 | } |
| 10519 | 10543 | } |
| 10520 | 10544 | |
| @@ -11072,21 +11096,32 @@ jQuery(async function () { | ||
| 11072 | 11096 | async function handleDeleteChat(chatFile, group, fromSlashCommand = false) { |
| 11073 | 11097 | // Close past chat popup. |
| 11074 | 11098 | $('#select_chat_cross').trigger('click'); |
| 11075 | - showLoader(); | |
| 11099 | + | |
| 11076 | - if (group) { | |
| 11100 | + const loaderHandle = loader.show({ | |
| 11077 | - await deleteGroupChat(group, chatFile); | |
| 11101 | + title: t`Delete Chat`, | |
| 11078 | - } else { | |
| 11102 | + message: t`Deleting chat…`, | |
| 11079 | - await delChat(`${chatFile}.jsonl`); | |
| 11103 | + toastMode: loader.ToastMode.STATIC, | |
| 11104 | + }); | |
| 11105 | + | |
| 11106 | + try { | |
| 11107 | + if (group) { | |
| 11108 | + await deleteGroupChat(group, chatFile); | |
| 11109 | + } else { | |
| 11110 | + await delChat(`${chatFile}.jsonl`); | |
| 11111 | + } | |
| 11112 | + } catch (error) { | |
| 11113 | + loaderHandle.hide(); | |
| 11114 | + throw error; | |
| 11080 | 11115 | } |
| 11081 | 11116 | |
| 11082 | 11117 | if (fromSlashCommand) { // When called from `/delchat` command, don't re-open the history view. |
| 11083 | 11118 | $('#options').hide(); // Hide option popup menu. |
| 11084 | 11119 | hideLoaderawait loaderHandle.hide(); |
| 11085 | 11120 | } else { // Open the history view again after 2 seconds (delay to avoid edge cases for deleting last chat). |
| 11086 | 11121 | setTimeout(async function () { |
| 11087 | 11122 | $('#option_select_chat').trigger('click'); |
| 11088 | 11123 | $('#options').hide(); // Hide option popup menu. |
| 11089 | 11124 | hideLoaderawait loaderHandle.hide(); |
| 11090 | 11125 | }, 2000); |
| 11091 | 11126 | } |
| 11092 | 11127 | } |
| @@ -14,10 +14,11 @@ import { | ||
| 14 | 14 | } from '../script.js'; |
| 15 | 15 | |
| 16 | 16 | import { favsToHotswap } from './RossAscends-mods.js'; |
| 17 | 17 | import { hideLoader, showLoaderloader } from './action-loader.js'; |
| 18 | 18 | import { convertCharacterToPersona } from './personas.js'; |
| 19 | 19 | import { callGenericPopup, POPUP_TYPE } from './popup.js'; |
| 20 | 20 | import { createTagInput, getTagKeyForEntity, getTagsList, printTagList, tag_map, compareTagsForSort, removeTagFromMap, importTags, tag_import_setting } from './tags.js'; |
| 21 | +import { t } from './i18n.js'; | |
| 21 | 22 | |
| 22 | 23 | /** |
| 23 | 24 | * Static object representing the actions of the |
| @@ -846,15 +847,15 @@ class BulkEditOverlay { | ||
| 846 | 847 | |
| 847 | 848 | const deleteChats = checkbox.prop('checked') ?? false; |
| 848 | 849 | |
| 849 | - showLoader(); | |
| 850 | + const loaderHandle = loader.show({ | |
| 850 | - const toast = toastr.info('We\'re deleting your characters, please wait...', 'Working on it'); | |
| 851 | + title: t`Bulk Delete`, | |
| 852 | + message: t`Deleting ${characterIds.length} character(s)…`, | |
| 853 | + toastMode: loader.ToastMode.STATIC, | |
| 854 | + }); | |
| 851 | 855 | const avatarList = characterIds.map(id => characters[id]?.avatar).filter(a => a); |
| 852 | 856 | return CharacterContextMenu.delete(avatarList, deleteChats) |
| 853 | 857 | .then(() => this.browseState()) |
| 854 | 858 | .finally(() => {loaderHandle.hide()); |
| 855 | - toastr.clear(toast); | |
| 856 | - hideLoader(); | |
| 857 | - }); | |
| 858 | 859 | }); |
| 859 | 860 | |
| 860 | 861 | // At this moment the popup is already changed in the dom, but not yet closed/resolved. We build the avatar list here |
| @@ -1,16 +1,19 @@ | ||
| 1 | 1 | /** |
| 2 | 2 | * ActionUnified action loader utilitysystem - shows loader overlay with stoppableoptional toast notificationnotifications. |
| 3 | 3 | * Designed to be flexible and reusable for various long-running operations. |
| 4 | - * Supports stacking multiple loaders - overlay stays single, but toasts can stack. | |
| 5 | 4 | * |
| 6 | - * With default arguments, will function as a generation loader / wrapper. | |
| 5 | + * Features: | |
| 6 | + * - Stacking multiple loaders - overlay stays single, but toasts can stack | |
| 7 | + * - Blocking and non-blocking modes | |
| 8 | + * - Stoppable or static toasts | |
| 9 | + * - Class-based handle system for fine-grained control | |
| 7 | 10 | * |
| 8 | 11 | * @module action-loader |
| 9 | 12 | */ |
| 10 | 13 | |
| 11 | 14 | import { t } from './i18n.js'; |
| 12 | 15 | import { stopGeneration } from '../script.js'; |
| 13 | 16 | import { showLoaderPopup, hideLoaderPOPUP_RESULT, isLoaderDisplayedPOPUP_TYPE } from './loaderpopup.js'; |
| 14 | 17 | |
| 15 | 18 | /** |
| 16 | 19 | * Enum representing the toast display mode for the action loader. |
| @@ -33,6 +36,7 @@ export const ActionLoaderToastMode = { | ||
| 33 | 36 | * @property {string} [message='Generating...'] - The message to display in the toast |
| 34 | 37 | * @property {string} [title] - Optional title for the toast notification |
| 35 | 38 | * @property {string} [stopTooltip='Stop'] - Tooltip text for the stop button |
| 39 | + * @property {HTMLElement|string|null} [overlayContent=null] - Custom content for the overlay (replaces default spinner) | |
| 36 | 40 | * @property {(() => void)|null} [onStop=null] - Custom stop handler. If null, calls `stopGeneration()` |
| 37 | 41 | * @property {(() => void)|null} [onHide=null] - Custom hide handler. Called when the loader is hidden (not stopped). |
| 38 | 42 | */ |
| @@ -95,6 +99,7 @@ export class ActionLoaderHandle { | ||
| 95 | 99 | * @param {string} [options.message='Generating...'] - Message to display in the toast |
| 96 | 100 | * @param {string} [options.title] - Title for the toast notification |
| 97 | 101 | * @param {string} [options.stopTooltip='Stop'] - Tooltip for the stop button |
| 102 | + * @param {HTMLElement|string|null} [options.overlayContent] - Custom content for the overlay (replaces default spinner) | |
| 98 | 103 | * @param {(() => void)|null} [options.onStop] - Custom stop handler |
| 99 | 104 | * @param {(() => void)|null} [options.onHide] - Custom hide handler |
| 100 | 105 | */ |
| @@ -104,6 +109,7 @@ export class ActionLoaderHandle { | ||
| 104 | 109 | message = t`Generating...`, |
| 105 | 110 | title = '', |
| 106 | 111 | stopTooltip = t`Stop`, |
| 112 | + overlayContent = null, | |
| 107 | 113 | onStop = null, |
| 108 | 114 | onHide = null, |
| 109 | 115 | } = {}) { |
| @@ -113,13 +119,13 @@ export class ActionLoaderHandle { | ||
| 113 | 119 | this.#onHide = onHide; |
| 114 | 120 | |
| 115 | 121 | // Warn if non-blocking loader has no toast - it won't be visible to the user |
| 116 | 122 | if (!blocking && toastMode === ActionLoaderToastMode.NONE && !overlayContent) { |
| 117 | 123 | console.warn('[ActionLoader] Non-blocking loader created without a toast. This loader will not be visible to the user.'); |
| 118 | 124 | } |
| 119 | 125 | |
| 120 | 126 | // Show the blocking loader overlay if this is the first blocking handle |
| 121 | 127 | if (blocking && !hasBlockingLoaders() && !isLoaderDisplayedisOverlayDisplayed()) { |
| 122 | 128 | showLoadershowOverlay(overlayContent); |
| 123 | 129 | } |
| 124 | 130 | |
| 125 | 131 | // Register this handle |
| @@ -191,7 +197,7 @@ export class ActionLoaderHandle { | ||
| 191 | 197 | |
| 192 | 198 | // Hide the overlay if this was the last blocking handle |
| 193 | 199 | if (this.#blocking && !hasBlockingLoaders()) { |
| 194 | 200 | await hideLoaderhideOverlay(); |
| 195 | 201 | } |
| 196 | 202 | } |
| 197 | 203 | |
| @@ -301,6 +307,12 @@ export const loader = { | ||
| 301 | 307 | get: getLoaderHandleById, |
| 302 | 308 | |
| 303 | 309 | /** |
| 310 | + * Checks if any blocking loader overlay is currently displayed. | |
| 311 | + * @returns {boolean} True if a blocking overlay is shown | |
| 312 | + */ | |
| 313 | + isBlocking: isOverlayDisplayed, | |
| 314 | + | |
| 315 | + /** | |
| 304 | 316 | * Toast display mode constants. |
| 305 | 317 | * @type {typeof ActionLoaderToastMode} |
| 306 | 318 | */ |
| @@ -311,6 +323,12 @@ export const loader = { | ||
| 311 | 323 | * @type {typeof ActionLoaderHandle} |
| 312 | 324 | */ |
| 313 | 325 | Handle: ActionLoaderHandle, |
| 326 | + | |
| 327 | + /** | |
| 328 | + * Creates a fresh default loader overlay element. | |
| 329 | + * @type {typeof createDefaultLoaderOverlay} | |
| 330 | + */ | |
| 331 | + createOverlay: createDefaultLoaderOverlay, | |
| 314 | 332 | }; |
| 315 | 333 | |
| 316 | 334 | /** |
| @@ -404,3 +422,145 @@ export function getLoaderHandleById(id) { | ||
| 404 | 422 | } |
| 405 | 423 | return undefined; |
| 406 | 424 | } |
| 425 | + | |
| 426 | +// ============================================================================ | |
| 427 | +// Internal overlay management | |
| 428 | +// ============================================================================ | |
| 429 | + | |
| 430 | +/** @type {Popup|null} The current loader overlay popup */ | |
| 431 | +let loaderPopup = null; | |
| 432 | + | |
| 433 | +/** Whether the initial HTML preloader has been removed */ | |
| 434 | +let preloaderYoinked = false; | |
| 435 | + | |
| 436 | +/** | |
| 437 | + * Creates the default loader overlay element. | |
| 438 | + * Always returns a fresh element instance. | |
| 439 | + * | |
| 440 | + * @returns {HTMLDivElement} A new loader overlay element | |
| 441 | + */ | |
| 442 | +export function createDefaultLoaderOverlay() { | |
| 443 | + const loaderElement = document.createElement('div'); | |
| 444 | + loaderElement.id = 'loader'; | |
| 445 | + | |
| 446 | + const spinnerElement = document.createElement('div'); | |
| 447 | + spinnerElement.id = 'load-spinner'; | |
| 448 | + spinnerElement.className = 'fa-solid fa-gear fa-spin fa-3x'; | |
| 449 | + | |
| 450 | + loaderElement.appendChild(spinnerElement); | |
| 451 | + | |
| 452 | + return loaderElement; | |
| 453 | +} | |
| 454 | + | |
| 455 | +/** | |
| 456 | + * Normalizes custom overlay content into a value supported by Popup. | |
| 457 | + * @param {string|HTMLElement|null} customContent - Custom overlay content | |
| 458 | + * @returns {string|HTMLElement} Content for Popup | |
| 459 | + */ | |
| 460 | +function getOverlayContent(customContent) { | |
| 461 | + if (typeof customContent === 'string') { | |
| 462 | + return customContent; | |
| 463 | + } | |
| 464 | + | |
| 465 | + if (customContent instanceof HTMLElement) { | |
| 466 | + return customContent; | |
| 467 | + } | |
| 468 | + | |
| 469 | + return createDefaultLoaderOverlay(); | |
| 470 | +} | |
| 471 | + | |
| 472 | +/** | |
| 473 | + * Checks if the loader overlay is currently displayed. | |
| 474 | + * @returns {boolean} True if overlay is shown | |
| 475 | + */ | |
| 476 | +function isOverlayDisplayed() { | |
| 477 | + return !!loaderPopup; | |
| 478 | +} | |
| 479 | + | |
| 480 | +/** | |
| 481 | + * Shows the blocking loader overlay. | |
| 482 | + * Internal function - use showActionLoader() instead. | |
| 483 | + * @param {HTMLElement|string|null} [customContent] - Custom content for the overlay | |
| 484 | + */ | |
| 485 | +function showOverlay(customContent = null) { | |
| 486 | + // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes | |
| 487 | + if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); | |
| 488 | + | |
| 489 | + const content = getOverlayContent(customContent); | |
| 490 | + | |
| 491 | + loaderPopup = new Popup(content, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); | |
| 492 | + | |
| 493 | + // No close button, loaders are not closable | |
| 494 | + loaderPopup.closeButton.style.display = 'none'; | |
| 495 | + | |
| 496 | + loaderPopup.show(); | |
| 497 | +} | |
| 498 | + | |
| 499 | +/** | |
| 500 | + * Hides the blocking loader overlay with animation. | |
| 501 | + * Internal function - use hideActionLoader() instead. | |
| 502 | + * @returns {Promise<void>} | |
| 503 | + */ | |
| 504 | +async function hideOverlay() { | |
| 505 | + if (!loaderPopup) { | |
| 506 | + return Promise.resolve(); | |
| 507 | + } | |
| 508 | + | |
| 509 | + return new Promise((resolve) => { | |
| 510 | + const loaderElement = $('#loader'); | |
| 511 | + const spinner = $('#load-spinner'); | |
| 512 | + | |
| 513 | + if (!loaderElement.length) { | |
| 514 | + console.warn('Loader element not found, skipping animation'); | |
| 515 | + cleanup(); | |
| 516 | + return; | |
| 517 | + } | |
| 518 | + | |
| 519 | + // Check if transitions are enabled on spinner (which has the transition property) | |
| 520 | + const transitionDuration = spinner.length && spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; | |
| 521 | + const hasTransitions = parseFloat(transitionDuration) > 0; | |
| 522 | + | |
| 523 | + if (hasTransitions) { | |
| 524 | + Promise.race([ | |
| 525 | + new Promise((r) => setTimeout(r, 500)), // Fallback timeout | |
| 526 | + new Promise((r) => loaderElement.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), | |
| 527 | + ]).finally(cleanup); | |
| 528 | + } else { | |
| 529 | + cleanup(); | |
| 530 | + } | |
| 531 | + | |
| 532 | + function cleanup() { | |
| 533 | + loaderElement.remove(); | |
| 534 | + // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS | |
| 535 | + // If it's present, we remove it once and then it's gone. | |
| 536 | + yoinkPreloader(); | |
| 537 | + | |
| 538 | + loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) | |
| 539 | + .catch((err) => console.error('Error completing loaderPopup:', err)) | |
| 540 | + .finally(() => { | |
| 541 | + loaderPopup = null; | |
| 542 | + resolve(); | |
| 543 | + }); | |
| 544 | + } | |
| 545 | + | |
| 546 | + // Apply the blur styles to the entire loader element | |
| 547 | + loaderElement.css({ | |
| 548 | + 'filter': 'blur(15px)', | |
| 549 | + 'opacity': '0', | |
| 550 | + }); | |
| 551 | + }); | |
| 552 | +} | |
| 553 | + | |
| 554 | +/** | |
| 555 | + * Removes the initial HTML preloader element. | |
| 556 | + * Called once after the first loader hide. | |
| 557 | + */ | |
| 558 | +function yoinkPreloader() { | |
| 559 | + if (preloaderYoinked) return; | |
| 560 | + document.getElementById('preloader')?.remove(); | |
| 561 | + preloaderYoinked = true; | |
| 562 | +} | |
| 563 | + | |
| 564 | +// ============================================================================ | |
| 565 | +// End internal overlay management | |
| 566 | +// ============================================================================ | |
| @@ -25,7 +25,7 @@ import { | ||
| 25 | 25 | saveGroupBookmarkChat, |
| 26 | 26 | selected_group, |
| 27 | 27 | } from './group-chats.js'; |
| 28 | 28 | import { hideLoader, showLoaderloader } from './action-loader.js'; |
| 29 | 29 | import { getLastMessageId } from './macros.js'; |
| 30 | 30 | import { Popup } from './popup.js'; |
| 31 | 31 | import { SlashCommand } from './slash-commands/SlashCommand.js'; |
| @@ -661,15 +661,20 @@ export function initBookmarks() { | ||
| 661 | 661 | return; |
| 662 | 662 | } |
| 663 | 663 | |
| 664 | + const loaderHandle = loader.show({ | |
| 665 | + title: t`Chat History`, | |
| 666 | + message: t`Loading chat…`, | |
| 667 | + toastMode: loader.ToastMode.STATIC, | |
| 668 | + }); | |
| 669 | + | |
| 664 | 670 | try { |
| 665 | - showLoader(); | |
| 666 | 671 | if (selected_group) { |
| 667 | 672 | await openGroupChat(selected_group, fileName); |
| 668 | 673 | } else { |
| 669 | 674 | await openCharacterChat(fileName); |
| 670 | 675 | } |
| 671 | 676 | } finally { |
| 672 | 677 | await hideLoaderloaderHandle.hide(); |
| 673 | 678 | } |
| 674 | 679 | |
| 675 | 680 | $('#shadow_select_chat_popup').css('display', 'none'); |
| @@ -1,7 +1,6 @@ | ||
| 1 | 1 | import { DOMPurify, Popper } from '../lib.js'; |
| 2 | 2 | |
| 3 | 3 | import { eventSource, event_types, saveSettings, saveSettingsDebounced, getRequestHeaders, animation_duration, CLIENT_VERSION } from '../script.js'; |
| 4 | -import { showLoader } from './loader.js'; | |
| 5 | 4 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 6 | 5 | import { renderTemplate, renderTemplateAsync } from './templates.js'; |
| 7 | 6 | import { delay, equalsIgnoreCaseAndAccents, isSubsetOf, sanitizeSelector, setValueByPath, versionCompare } from './utils.js'; |
| @@ -1158,7 +1157,6 @@ async function showExtensionsDetails() { | ||
| 1158 | 1157 | abortController.abort(); |
| 1159 | 1158 | } |
| 1160 | 1159 | if (requiresReload) { |
| 1161 | - showLoader(); | |
| 1162 | 1160 | location.reload(); |
| 1163 | 1161 | } |
| 1164 | 1162 | } |
| @@ -1,80 +1,59 @@ | ||
| 1 | 1 | import { POPUP_RESULT, POPUP_TYPE, Popuploader } from './popupaction-loader.js'; |
| 2 | 2 | |
| 3 | -/** @type {Popup} */ | |
| 3 | +/** | |
| 4 | -let loaderPopup; | |
| 4 | + * Handle for the legacy loader created by showLoader(). | |
| 5 | - | |
| 5 | + * @type {import('./action-loader.js').ActionLoaderHandle|null} | |
| 6 | -let preloaderYoinked = false; | |
| 6 | + */ | |
| 7 | - | |
| 7 | +let legacyLoaderHandle = null; | |
| 8 | -export function isLoaderDisplayed() { | |
| 8 | + | |
| 9 | - return !!loaderPopup; | |
| 9 | +/** | |
| 10 | -} | |
| 10 | + * Shows the loader overlay. | |
| 11 | - | |
| 11 | + * | |
| 12 | + * @deprecated Use `showActionLoader()` from action-loader.js instead. | |
| 13 | + * This function now creates a blocking action loader with no toast. | |
| 14 | + * The new system supports stacking multiple loaders and provides better control. | |
| 15 | + * | |
| 16 | + * @example | |
| 17 | + * // New recommended approach: | |
| 18 | + * import { showActionLoader } from './action-loader.js'; | |
| 19 | + * const handle = showActionLoader({ message: 'Loading...' }); | |
| 20 | + * // ... do work ... | |
| 21 | + * handle.hide(); | |
| 22 | + */ | |
| 12 | 23 | export function showLoader() { |
| 13 | - // Two loaders don't make sense. Don't await, we can overlay the old loader while it closes | |
| 24 | + // Hide any existing legacy loader first to maintain old behavior | |
| 14 | - if (loaderPopup) loaderPopup.complete(POPUP_RESULT.CANCELLED); | |
| 25 | + if (legacyLoaderHandle && legacyLoaderHandle.isActive) { | |
| 15 | - | |
| 26 | + legacyLoaderHandle.hide(); | |
| 16 | - loaderPopup = new Popup(` | |
| 27 | + } | |
| 17 | - <div id="loader"> | |
| 18 | - <div id="load-spinner" class="fa-solid fa-gear fa-spin fa-3x"></div> | |
| 19 | - </div>`, POPUP_TYPE.DISPLAY, null, { transparent: true, animation: 'none', wide: true, large: true }); | |
| 20 | - | |
| 21 | - // No close button, loaders are not closable | |
| 22 | - loaderPopup.closeButton.style.display = 'none'; | |
| 23 | 28 | |
| 24 | - loaderPopup.show(); | |
| 29 | + // Create a blocking loader with no toast (matches old behavior) | |
| 30 | + legacyLoaderHandle = loader.show({ | |
| 31 | + blocking: true, | |
| 32 | + toastMode: loader.ToastMode.NONE, | |
| 33 | + }); | |
| 25 | 34 | } |
| 26 | 35 | |
| 36 | +/** | |
| 37 | + * Hides the loader overlay. | |
| 38 | + * | |
| 39 | + * @deprecated Use `hideActionLoader()` or `handle.hide()` from action-loader.js instead. | |
| 40 | + * This function now hides the legacy loader created by showLoader(). | |
| 41 | + * | |
| 42 | + * @example | |
| 43 | + * // New recommended approach: | |
| 44 | + * import { showActionLoader } from './action-loader.js'; | |
| 45 | + * const handle = showActionLoader({ message: 'Loading...' }); | |
| 46 | + * // ... do work ... | |
| 47 | + * await handle.hide(); | |
| 48 | + * | |
| 49 | + * @returns {Promise<void>} | |
| 50 | + */ | |
| 27 | 51 | export async function hideLoader() { |
| 28 | 52 | if (!loaderPopuplegacyLoaderHandle || !legacyLoaderHandle.isActive) { |
| 29 | 53 | console.warn('There is no loader showing to hide'); |
| 30 | 54 | return Promise.resolve(); |
| 31 | 55 | } |
| 32 | 56 | |
| 33 | - return new Promise((resolve) => { | |
| 57 | + await legacyLoaderHandle.hide(); | |
| 34 | - const spinner = $('#load-spinner'); | |
| 58 | + legacyLoaderHandle = null; | |
| 35 | - if (!spinner.length) { | |
| 36 | - console.warn('Spinner element not found, skipping animation'); | |
| 37 | - cleanup(); | |
| 38 | - return; | |
| 39 | - } | |
| 40 | - | |
| 41 | - // Check if transitions are enabled | |
| 42 | - const transitionDuration = spinner[0] ? getComputedStyle(spinner[0]).transitionDuration : '0s'; | |
| 43 | - const hasTransitions = parseFloat(transitionDuration) > 0; | |
| 44 | - | |
| 45 | - if (hasTransitions) { | |
| 46 | - Promise.race([ | |
| 47 | - new Promise((r) => setTimeout(r, 500)), // Fallback timeout | |
| 48 | - new Promise((r) => spinner.one('transitionend webkitTransitionEnd oTransitionEnd MSTransitionEnd', r)), | |
| 49 | - ]).finally(cleanup); | |
| 50 | - } else { | |
| 51 | - cleanup(); | |
| 52 | - } | |
| 53 | - | |
| 54 | - function cleanup() { | |
| 55 | - $('#loader').remove(); | |
| 56 | - // Yoink preloader entirely; it only exists to cover up unstyled content while loading JS | |
| 57 | - // If it's present, we remove it once and then it's gone. | |
| 58 | - yoinkPreloader(); | |
| 59 | - | |
| 60 | - loaderPopup.complete(POPUP_RESULT.AFFIRMATIVE) | |
| 61 | - .catch((err) => console.error('Error completing loaderPopup:', err)) | |
| 62 | - .finally(() => { | |
| 63 | - loaderPopup = null; | |
| 64 | - resolve(); | |
| 65 | - }); | |
| 66 | - } | |
| 67 | - | |
| 68 | - // Apply the styles | |
| 69 | - spinner.css({ | |
| 70 | - 'filter': 'blur(15px)', | |
| 71 | - 'opacity': '0', | |
| 72 | - }); | |
| 73 | - }); | |
| 74 | -} | |
| 75 | - | |
| 76 | -function yoinkPreloader() { | |
| 77 | - if (preloaderYoinked) return; | |
| 78 | - document.getElementById('preloader').remove(); | |
| 79 | - preloaderYoinked = true; | |
| 80 | 59 | } |
| @@ -189,7 +189,9 @@ export function getContext() { | ||
| 189 | 189 | /** @deprecated Use callGenericPopup or Popup instead. */ |
| 190 | 190 | callPopup, |
| 191 | 191 | callGenericPopup, |
| 192 | + /** @deprecated Use loader.show instead. */ | |
| 192 | 193 | showLoader, |
| 194 | + /** @deprecated Use loader.hide instead. */ | |
| 193 | 195 | hideLoader, |
| 194 | 196 | mainApi: main_api, |
| 195 | 197 | extensionSettings: extension_settings, |
| @@ -1,6 +1,6 @@ | ||
| 1 | 1 | <div class="welcomePanel"> |
| 2 | 2 | <div class="welcomeHeaderTitle"> |
| 3 | 3 | <img src="img/logo.png" alt="SillyTavern Logo" data-i18n="[alt]SillyTavern Logo" class="welcomeHeaderLogo"> |
| 4 | 4 | <span class="welcomeHeaderVersionDisplay">{{version}}</span> |
| 5 | 5 | <div class="mes_button showRecentChats" title="Show recent chats" data-i18n="[title]Show recent chats"> |
| 6 | 6 | <i class="fa-solid fa-circle-chevron-down fa-fw fa-lg"></i> |