diff --git a/public/translations/ar/default.json b/public/translations/ar/default.json index 399db325..7b75de37 100644 --- a/public/translations/ar/default.json +++ b/public/translations/ar/default.json @@ -50,7 +50,6 @@ "gif": "جي آي إف", "total_posts": "إجمالي المشاركات:", "current_users": "المستخدمون الحاليون:", - "boards_tracked": "المنتديات المتابعة:", "challenge_counter": "{{index}} من {{total}}", "embed": "تضمين", "remove": "إزالة", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "فيديو YouTube", - "youtube_thumbnail_link_conversion_notice": "تم اكتشاف رابط YouTube. جارٍ لصق الصورة المصغرة خلال {{count}}..." + "youtube_thumbnail_link_conversion_notice": "تم اكتشاف رابط YouTube. جارٍ لصق الصورة المصغرة خلال {{count}}...", + "stats_scope_all_listed_boards": "جميع اللوحات المُدرجة (بطيء)", + "stats_scope_directory_boards": "لوحات الدليل فقط (سريع)", + "boards_loaded": "اللوحات المحملة:" } diff --git a/public/translations/bn/default.json b/public/translations/bn/default.json index 928c4cf6..ba17fba3 100644 --- a/public/translations/bn/default.json +++ b/public/translations/bn/default.json @@ -50,7 +50,6 @@ "gif": "জিআইএফ", "total_posts": "মোট পোস্টসমূহ:", "current_users": "বর্তমান ব্যবহারকারীরা:", - "boards_tracked": "ট্র্যাক করা বোর্ডসমূহ:", "challenge_counter": "{{index}} থেকে {{total}}", "embed": "এম্বেড", "remove": "অপসারণ", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube ভিডিও", - "youtube_thumbnail_link_conversion_notice": "YouTube লিংক শনাক্ত হয়েছে। {{count}}-এর মধ্যে থাম্বনেইল ছবি পেস্ট করা হচ্ছে..." + "youtube_thumbnail_link_conversion_notice": "YouTube লিংক শনাক্ত হয়েছে। {{count}}-এর মধ্যে থাম্বনেইল ছবি পেস্ট করা হচ্ছে...", + "stats_scope_all_listed_boards": "সব তালিকাভুক্ত বোর্ড (ধীর)", + "stats_scope_directory_boards": "শুধু ডিরেক্টরি বোর্ড (দ্রুত)", + "boards_loaded": "লোড করা বোর্ড:" } diff --git a/public/translations/cs/default.json b/public/translations/cs/default.json index a4b26708..70f68c45 100644 --- a/public/translations/cs/default.json +++ b/public/translations/cs/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Celkem příspěvků:", "current_users": "současní uživatelé:", - "boards_tracked": "sledované nástěnky:", "challenge_counter": "{{index}} z {{total}}", "embed": "vložit", "remove": "odstranit", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "Byl zjištěn odkaz na YouTube. Vkládám náhledový obrázek za {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Byl zjištěn odkaz na YouTube. Vkládám náhledový obrázek za {{count}}...", + "stats_scope_all_listed_boards": "Všechny uvedené nástěnky (pomalé)", + "stats_scope_directory_boards": "Pouze nástěnky v adresáři (rychlé)", + "boards_loaded": "načtené nástěnky:" } diff --git a/public/translations/da/default.json b/public/translations/da/default.json index bdd9eb17..4a03040d 100644 --- a/public/translations/da/default.json +++ b/public/translations/da/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Samlede opslag:", "current_users": "nuværende brugere:", - "boards_tracked": "sporede opslagstavler:", "challenge_counter": "{{index}} af {{total}}", "embed": "integrer", "remove": "fjern", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-video", - "youtube_thumbnail_link_conversion_notice": "YouTube-link registreret. Indsætter miniaturebillede om {{count}}..." + "youtube_thumbnail_link_conversion_notice": "YouTube-link registreret. Indsætter miniaturebillede om {{count}}...", + "stats_scope_all_listed_boards": "Alle viste boards (langsomt)", + "stats_scope_directory_boards": "Kun boards i kataloget (hurtigt)", + "boards_loaded": "indlæste boards:" } diff --git a/public/translations/de/default.json b/public/translations/de/default.json index 90f580e5..43814ca7 100644 --- a/public/translations/de/default.json +++ b/public/translations/de/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Gesamtanzahl der Beiträge:", "current_users": "aktuelle Benutzer:", - "boards_tracked": "verfolgte Boards:", "challenge_counter": "{{index}} von {{total}}", "embed": "einbetten", "remove": "entfernen", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-Video", - "youtube_thumbnail_link_conversion_notice": "YouTube-Link erkannt. Vorschaubild wird in {{count}} eingefügt..." + "youtube_thumbnail_link_conversion_notice": "YouTube-Link erkannt. Vorschaubild wird in {{count}} eingefügt...", + "stats_scope_all_listed_boards": "Alle aufgelisteten Boards (langsam)", + "stats_scope_directory_boards": "Nur Verzeichnis-Boards (schnell)", + "boards_loaded": "geladene Boards:" } diff --git a/public/translations/el/default.json b/public/translations/el/default.json index eda6e509..599edb78 100644 --- a/public/translations/el/default.json +++ b/public/translations/el/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Συνολικές Δημοσιεύσεις:", "current_users": "τρέχοντες χρήστες:", - "boards_tracked": "πίνακες που παρακολουθούνται:", "challenge_counter": "{{index}} από {{total}}", "embed": "ενσωμάτωση", "remove": "αφαίρεση", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "βίντεο YouTube", - "youtube_thumbnail_link_conversion_notice": "Εντοπίστηκε σύνδεσμος YouTube. Επικόλληση μικρογραφίας σε {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Εντοπίστηκε σύνδεσμος YouTube. Επικόλληση μικρογραφίας σε {{count}}...", + "stats_scope_all_listed_boards": "Όλα τα καταχωρισμένα boards (αργό)", + "stats_scope_directory_boards": "Μόνο boards καταλόγου (γρήγορο)", + "boards_loaded": "φορτωμένοι πίνακες:" } diff --git a/public/translations/en/default.json b/public/translations/en/default.json index f2a96db2..d393d476 100644 --- a/public/translations/en/default.json +++ b/public/translations/en/default.json @@ -56,7 +56,6 @@ "gif": "GIF", "total_posts": "Total Posts:", "current_users": "current users:", - "boards_tracked": "boards tracked:", "challenge_counter": "{{index}} of {{total}}", "embed": "embed", "remove": "remove", @@ -420,5 +419,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube video", - "youtube_thumbnail_link_conversion_notice": "YouTube link detected. Pasting thumbnail image in {{count}}..." + "youtube_thumbnail_link_conversion_notice": "YouTube link detected. Pasting thumbnail image in {{count}}...", + "stats_scope_all_listed_boards": "All listed boards (slow)", + "stats_scope_directory_boards": "Directory boards only (fast)", + "boards_loaded": "boards loaded:" } diff --git a/public/translations/es/default.json b/public/translations/es/default.json index 2be29ad9..34c39a07 100644 --- a/public/translations/es/default.json +++ b/public/translations/es/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Total de publicaciones:", "current_users": "usuarios actuales:", - "boards_tracked": "foros seguidos:", "challenge_counter": "{{index}} de {{total}}", "embed": "incrustar", "remove": "eliminar", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video de YouTube", - "youtube_thumbnail_link_conversion_notice": "Se detectó un enlace de YouTube. Pegando imagen en miniatura en {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Se detectó un enlace de YouTube. Pegando imagen en miniatura en {{count}}...", + "stats_scope_all_listed_boards": "Todos los boards listados (lento)", + "stats_scope_directory_boards": "Solo boards del directorio (rápido)", + "boards_loaded": "tableros cargados:" } diff --git a/public/translations/fa/default.json b/public/translations/fa/default.json index a0d9ec18..c710b6e6 100644 --- a/public/translations/fa/default.json +++ b/public/translations/fa/default.json @@ -50,7 +50,6 @@ "gif": "گیف", "total_posts": "کل پست‌ها:", "current_users": "کاربران فعلی:", - "boards_tracked": "تالارهای پیگیری شده:", "challenge_counter": "{{index}} از {{total}}", "embed": "جاسازی", "remove": "حذف", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "ویدیوی YouTube", - "youtube_thumbnail_link_conversion_notice": "پیوند YouTube شناسایی شد. در حال چسباندن تصویر بندانگشتی در {{count}}..." + "youtube_thumbnail_link_conversion_notice": "پیوند YouTube شناسایی شد. در حال چسباندن تصویر بندانگشتی در {{count}}...", + "stats_scope_all_listed_boards": "همهٔ بردهای فهرست‌شده (کند)", + "stats_scope_directory_boards": "فقط بردهای دایرکتوری (سریع)", + "boards_loaded": "بردهای بارگذاری‌شده:" } diff --git a/public/translations/fi/default.json b/public/translations/fi/default.json index 863f6f81..f753c021 100644 --- a/public/translations/fi/default.json +++ b/public/translations/fi/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Viestejä yhteensä:", "current_users": "nykyiset käyttäjät:", - "boards_tracked": "seuratut foorumit:", "challenge_counter": "{{index}} / {{total}}", "embed": "upota", "remove": "poista", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-video", - "youtube_thumbnail_link_conversion_notice": "YouTube-linkki havaittu. Liitetään pikkukuva {{count}} kuluttua..." + "youtube_thumbnail_link_conversion_notice": "YouTube-linkki havaittu. Liitetään pikkukuva {{count}} kuluttua...", + "stats_scope_all_listed_boards": "Kaikki listatut boardit (hidas)", + "stats_scope_directory_boards": "Vain hakemiston boardit (nopea)", + "boards_loaded": "ladatut boardit:" } diff --git a/public/translations/fil/default.json b/public/translations/fil/default.json index 5bc99431..8d5c5318 100644 --- a/public/translations/fil/default.json +++ b/public/translations/fil/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Kabuuang Posts:", "current_users": "kasalukuyang mga gumagamit:", - "boards_tracked": "mga board na sinusubaybayan:", "challenge_counter": "{{index}} ng {{total}}", "embed": "i-embed", "remove": "alisin", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video sa YouTube", - "youtube_thumbnail_link_conversion_notice": "May nakita na link ng YouTube. Ipi-paste ang thumbnail image sa loob ng {{count}}..." + "youtube_thumbnail_link_conversion_notice": "May nakita na link ng YouTube. Ipi-paste ang thumbnail image sa loob ng {{count}}...", + "stats_scope_all_listed_boards": "Lahat ng nakalistang board (mabagal)", + "stats_scope_directory_boards": "Mga board ng directory lang (mabilis)", + "boards_loaded": "mga board na na-load:" } diff --git a/public/translations/fr/default.json b/public/translations/fr/default.json index 2409955e..8ee72bfe 100644 --- a/public/translations/fr/default.json +++ b/public/translations/fr/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Total des publications:", "current_users": "utilisateurs actuels:", - "boards_tracked": "forums suivis:", "challenge_counter": "{{index}} sur {{total}}", "embed": "intégrer", "remove": "supprimer", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "vidéo YouTube", - "youtube_thumbnail_link_conversion_notice": "Lien YouTube détecté. Collage de l'image miniature dans {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Lien YouTube détecté. Collage de l'image miniature dans {{count}}...", + "stats_scope_all_listed_boards": "Tous les boards listés (lent)", + "stats_scope_directory_boards": "Boards de l'annuaire uniquement (rapide)", + "boards_loaded": "boards chargés :" } diff --git a/public/translations/he/default.json b/public/translations/he/default.json index 4d696903..891110df 100644 --- a/public/translations/he/default.json +++ b/public/translations/he/default.json @@ -50,7 +50,6 @@ "gif": "גיף", "total_posts": "סה\"כ פוסטים:", "current_users": "משתמשים נוכחיים:", - "boards_tracked": "פורומים שנמצאים במעקב:", "challenge_counter": "{{index}} מתוך {{total}}", "embed": "הטמע", "remove": "הסר", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "סרטון YouTube", - "youtube_thumbnail_link_conversion_notice": "זוהה קישור YouTube. מדביק תמונת ממוזערת בעוד {{count}}..." + "youtube_thumbnail_link_conversion_notice": "זוהה קישור YouTube. מדביק תמונת ממוזערת בעוד {{count}}...", + "stats_scope_all_listed_boards": "כל הלוחות הרשומים (איטי)", + "stats_scope_directory_boards": "רק לוחות מהספרייה (מהיר)", + "boards_loaded": "לוחות שנטענו:" } diff --git a/public/translations/hi/default.json b/public/translations/hi/default.json index a4a2bcd9..ea562343 100644 --- a/public/translations/hi/default.json +++ b/public/translations/hi/default.json @@ -50,7 +50,6 @@ "gif": "जीआईएफ", "total_posts": "कुल पोस्ट:", "current_users": "वर्तमान उपयोगकर्ता:", - "boards_tracked": "ट्रैक किए गए बोर्ड:", "challenge_counter": "{{index}} में से {{total}}", "embed": "एम्बेड", "remove": "हटाएं", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube वीडियो", - "youtube_thumbnail_link_conversion_notice": "YouTube लिंक मिला। {{count}} में थंबनेल इमेज पेस्ट की जा रही है..." + "youtube_thumbnail_link_conversion_notice": "YouTube लिंक मिला। {{count}} में थंबनेल इमेज पेस्ट की जा रही है...", + "stats_scope_all_listed_boards": "सभी सूचीबद्ध बोर्ड (धीमा)", + "stats_scope_directory_boards": "सिर्फ डायरेक्टरी बोर्ड (तेज़)", + "boards_loaded": "लोड किए गए बोर्ड:" } diff --git a/public/translations/hu/default.json b/public/translations/hu/default.json index 699cabc1..6b5feaac 100644 --- a/public/translations/hu/default.json +++ b/public/translations/hu/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Összes bejegyzés:", "current_users": "jelenlegi felhasználók:", - "boards_tracked": "követett fórumok:", "challenge_counter": "{{index}} a(z) {{total}}-ból/ből", "embed": "beágyaz", "remove": "eltávolít", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-videó", - "youtube_thumbnail_link_conversion_notice": "YouTube-link észlelve. Bélyegkép beillesztése {{count}} múlva..." + "youtube_thumbnail_link_conversion_notice": "YouTube-link észlelve. Bélyegkép beillesztése {{count}} múlva...", + "stats_scope_all_listed_boards": "Az összes listázott board (lassú)", + "stats_scope_directory_boards": "Csak a könyvtár boardjai (gyors)", + "boards_loaded": "betöltött boardok:" } diff --git a/public/translations/id/default.json b/public/translations/id/default.json index a007633a..b6d9a0fc 100644 --- a/public/translations/id/default.json +++ b/public/translations/id/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Total Posting:", "current_users": "pengguna saat ini:", - "boards_tracked": "papan yang dilacak:", "challenge_counter": "{{index}} dari {{total}}", "embed": "sematkan", "remove": "hapus", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "Tautan YouTube terdeteksi. Menempelkan gambar thumbnail dalam {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Tautan YouTube terdeteksi. Menempelkan gambar thumbnail dalam {{count}}...", + "stats_scope_all_listed_boards": "Semua board yang terdaftar (lambat)", + "stats_scope_directory_boards": "Hanya board direktori (cepat)", + "boards_loaded": "board dimuat:" } diff --git a/public/translations/it/default.json b/public/translations/it/default.json index d24494ff..d60fb11a 100644 --- a/public/translations/it/default.json +++ b/public/translations/it/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Post totali:", "current_users": "utenti attuali:", - "boards_tracked": "board tracciate:", "challenge_counter": "{{index}} di {{total}}", "embed": "incorpora", "remove": "rimuovi", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "Link YouTube rilevato. Incollando l'immagine di anteprima tra {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Link YouTube rilevato. Incollando l'immagine di anteprima tra {{count}}...", + "stats_scope_all_listed_boards": "Tutte le board elencate (lento)", + "stats_scope_directory_boards": "Solo board della directory (veloce)", + "boards_loaded": "board caricati:" } diff --git a/public/translations/ja/default.json b/public/translations/ja/default.json index 57b06b4c..88c2855d 100644 --- a/public/translations/ja/default.json +++ b/public/translations/ja/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "総投稿数:", "current_users": "現在のユーザー:", - "boards_tracked": "追跡されたボード:", "challenge_counter": "{{index}} / {{total}}", "embed": "埋め込み", "remove": "削除", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube動画", - "youtube_thumbnail_link_conversion_notice": "YouTubeリンクを検出しました。{{count}}後にサムネイル画像を貼り付けます..." + "youtube_thumbnail_link_conversion_notice": "YouTubeリンクを検出しました。{{count}}後にサムネイル画像を貼り付けます...", + "stats_scope_all_listed_boards": "掲載されているすべての掲示板(低速)", + "stats_scope_directory_boards": "ディレクトリの掲示板のみ(高速)", + "boards_loaded": "読み込まれた板:" } diff --git a/public/translations/ko/default.json b/public/translations/ko/default.json index 72dae021..178c0ef8 100644 --- a/public/translations/ko/default.json +++ b/public/translations/ko/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "총 게시물:", "current_users": "현재 사용자:", - "boards_tracked": "추적된 게시판:", "challenge_counter": "{{index}} 중 {{total}}", "embed": "임베드", "remove": "제거", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube 동영상", - "youtube_thumbnail_link_conversion_notice": "YouTube 링크가 감지되었습니다. {{count}} 후에 썸네일 이미지를 붙여넣는 중..." + "youtube_thumbnail_link_conversion_notice": "YouTube 링크가 감지되었습니다. {{count}} 후에 썸네일 이미지를 붙여넣는 중...", + "stats_scope_all_listed_boards": "등록된 모든 게시판 (느림)", + "stats_scope_directory_boards": "디렉터리 게시판만 (빠름)", + "boards_loaded": "불러온 게시판:" } diff --git a/public/translations/mr/default.json b/public/translations/mr/default.json index bb01a9b6..d0f1a36a 100644 --- a/public/translations/mr/default.json +++ b/public/translations/mr/default.json @@ -50,7 +50,6 @@ "gif": "जीआईएफ", "total_posts": "एकूण पोस्ट:", "current_users": "सध्याचे वापरकर्ते:", - "boards_tracked": "ट्रॅक केलेले बोर्ड:", "challenge_counter": "{{index}} पैकी {{total}}", "embed": "एम्बेड", "remove": "काढा", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube व्हिडिओ", - "youtube_thumbnail_link_conversion_notice": "YouTube लिंक आढळली. {{count}} मध्ये थंबनेल प्रतिमा पेस्ट करत आहे..." + "youtube_thumbnail_link_conversion_notice": "YouTube लिंक आढळली. {{count}} मध्ये थंबनेल प्रतिमा पेस्ट करत आहे...", + "stats_scope_all_listed_boards": "सर्व सूचीबद्ध बोर्ड (मंद)", + "stats_scope_directory_boards": "फक्त निर्देशिका बोर्ड (जलद)", + "boards_loaded": "लोड केलेले बोर्ड:" } diff --git a/public/translations/nl/default.json b/public/translations/nl/default.json index 4791f927..2fe388d2 100644 --- a/public/translations/nl/default.json +++ b/public/translations/nl/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Totaal aantal berichten:", "current_users": "huidige gebruikers:", - "boards_tracked": "gevolgde borden:", "challenge_counter": "{{index}} van {{total}}", "embed": "insluiten", "remove": "verwijderen", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-video", - "youtube_thumbnail_link_conversion_notice": "YouTube-link gedetecteerd. Miniatuurafbeelding wordt over {{count}} geplakt..." + "youtube_thumbnail_link_conversion_notice": "YouTube-link gedetecteerd. Miniatuurafbeelding wordt over {{count}} geplakt...", + "stats_scope_all_listed_boards": "Alle vermelde boards (traag)", + "stats_scope_directory_boards": "Alleen directory-boards (snel)", + "boards_loaded": "geladen boards:" } diff --git a/public/translations/no/default.json b/public/translations/no/default.json index 21310230..3d0d2a76 100644 --- a/public/translations/no/default.json +++ b/public/translations/no/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Totalt antall innlegg:", "current_users": "nåværende brukere:", - "boards_tracked": "sporede fora:", "challenge_counter": "{{index}} av {{total}}", "embed": "bygg inn", "remove": "fjern", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-video", - "youtube_thumbnail_link_conversion_notice": "YouTube-lenke oppdaget. Limer inn miniatyrbilde om {{count}}..." + "youtube_thumbnail_link_conversion_notice": "YouTube-lenke oppdaget. Limer inn miniatyrbilde om {{count}}...", + "stats_scope_all_listed_boards": "Alle oppførte boards (tregt)", + "stats_scope_directory_boards": "Bare katalog-boards (raskt)", + "boards_loaded": "lastede boards:" } diff --git a/public/translations/pl/default.json b/public/translations/pl/default.json index d81aec32..b31d354a 100644 --- a/public/translations/pl/default.json +++ b/public/translations/pl/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Łączna liczba postów:", "current_users": "obecni użytkownicy:", - "boards_tracked": "śledzone fora:", "challenge_counter": "{{index}} z {{total}}", "embed": "osadź", "remove": "usuń", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "wideo YouTube", - "youtube_thumbnail_link_conversion_notice": "Wykryto link YouTube. Wklejam miniaturę za {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Wykryto link YouTube. Wklejam miniaturę za {{count}}...", + "stats_scope_all_listed_boards": "Wszystkie wymienione boardy (wolno)", + "stats_scope_directory_boards": "Tylko boardy z katalogu (szybko)", + "boards_loaded": "załadowane boardy:" } diff --git a/public/translations/pt/default.json b/public/translations/pt/default.json index 660fda77..2ceb1a3a 100644 --- a/public/translations/pt/default.json +++ b/public/translations/pt/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Total de postagens:", "current_users": "usuários atuais:", - "boards_tracked": "fóruns rastreados:", "challenge_counter": "{{index}} de {{total}}", "embed": "embutir", "remove": "remover", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "vídeo do YouTube", - "youtube_thumbnail_link_conversion_notice": "Link do YouTube detectado. Colando imagem em miniatura em {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Link do YouTube detectado. Colando imagem em miniatura em {{count}}...", + "stats_scope_all_listed_boards": "Todos os boards listados (lento)", + "stats_scope_directory_boards": "Somente boards do diretório (rápido)", + "boards_loaded": "boards carregados:" } diff --git a/public/translations/ro/default.json b/public/translations/ro/default.json index 555d2a50..5d2098b7 100644 --- a/public/translations/ro/default.json +++ b/public/translations/ro/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Total postări:", "current_users": "utilizatori actuali:", - "boards_tracked": "forumuri urmărite:", "challenge_counter": "{{index}} din {{total}}", "embed": "încorpora", "remove": "elimina", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "Link YouTube detectat. Lipesc imaginea miniatură în {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Link YouTube detectat. Lipesc imaginea miniatură în {{count}}...", + "stats_scope_all_listed_boards": "Toate boardurile listate (lent)", + "stats_scope_directory_boards": "Doar boardurile din director (rapid)", + "boards_loaded": "boarduri încărcate:" } diff --git a/public/translations/ru/default.json b/public/translations/ru/default.json index 8f6ec977..77e0097b 100644 --- a/public/translations/ru/default.json +++ b/public/translations/ru/default.json @@ -50,7 +50,6 @@ "gif": "ГИФ", "total_posts": "Всего сообщений:", "current_users": "текущие пользователи:", - "boards_tracked": "отслеживаемые форумы:", "challenge_counter": "{{index}} из {{total}}", "embed": "встроить", "remove": "удалить", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "видео YouTube", - "youtube_thumbnail_link_conversion_notice": "Обнаружена ссылка YouTube. Вставка миниатюры через {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Обнаружена ссылка YouTube. Вставка миниатюры через {{count}}...", + "stats_scope_all_listed_boards": "Все перечисленные доски (медленно)", + "stats_scope_directory_boards": "Только доски каталога (быстро)", + "boards_loaded": "загруженные доски:" } diff --git a/public/translations/sq/default.json b/public/translations/sq/default.json index 64e66eb5..777dbc19 100644 --- a/public/translations/sq/default.json +++ b/public/translations/sq/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Postime totale:", "current_users": "përdoruesit aktualë:", - "boards_tracked": "forume të gjurmueshme:", "challenge_counter": "{{index}} nga {{total}}", "embed": "integro", "remove": "hiq", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "U zbulua lidhje YouTube. Po ngjis imazhin miniaturë pas {{count}}..." + "youtube_thumbnail_link_conversion_notice": "U zbulua lidhje YouTube. Po ngjis imazhin miniaturë pas {{count}}...", + "stats_scope_all_listed_boards": "Të gjitha bordet e listuara (ngadalë)", + "stats_scope_directory_boards": "Vetëm bordet e drejtorisë (shpejt)", + "boards_loaded": "bordet e ngarkuara:" } diff --git a/public/translations/sv/default.json b/public/translations/sv/default.json index 9d7c4819..a22c816a 100644 --- a/public/translations/sv/default.json +++ b/public/translations/sv/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Totalt antal inlägg:", "current_users": "aktuella användare:", - "boards_tracked": "spårade forum:", "challenge_counter": "{{index}} av {{total}}", "embed": "bädda in", "remove": "ta bort", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube-video", - "youtube_thumbnail_link_conversion_notice": "YouTube-länk upptäckt. Klistrar in miniatyrbild om {{count}}..." + "youtube_thumbnail_link_conversion_notice": "YouTube-länk upptäckt. Klistrar in miniatyrbild om {{count}}...", + "stats_scope_all_listed_boards": "Alla listade boards (långsamt)", + "stats_scope_directory_boards": "Endast katalogens boards (snabbt)", + "boards_loaded": "laddade boards:" } diff --git a/public/translations/te/default.json b/public/translations/te/default.json index 2d63affa..d07b5d19 100644 --- a/public/translations/te/default.json +++ b/public/translations/te/default.json @@ -50,7 +50,6 @@ "gif": "జీఐఎఫ్", "total_posts": "మొత్తం పోస్ట్‌లు:", "current_users": "ప్రస్తుత వాడుకరులు:", - "boards_tracked": "ట్రాక్ చేయబడిన బోర్డులు:", "challenge_counter": "{{index}} లో {{total}}", "embed": "ఎంబెడ్", "remove": "తొలగించు", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube వీడియో", - "youtube_thumbnail_link_conversion_notice": "YouTube లింక్ గుర్తించబడింది. {{count}}లో థంబ్‌నెయిల్ చిత్రాన్ని పేస్ట్ చేస్తున్నాము..." + "youtube_thumbnail_link_conversion_notice": "YouTube లింక్ గుర్తించబడింది. {{count}}లో థంబ్‌నెయిల్ చిత్రాన్ని పేస్ట్ చేస్తున్నాము...", + "stats_scope_all_listed_boards": "జాబితాలో ఉన్న అన్ని బోర్డులు (నెమ్మదిగా)", + "stats_scope_directory_boards": "డైరెక్టరీ బోర్డులు మాత్రమే (వేగంగా)", + "boards_loaded": "లోడ్ చేసిన బోర్డులు:" } diff --git a/public/translations/th/default.json b/public/translations/th/default.json index 4e9fd5f1..d746051c 100644 --- a/public/translations/th/default.json +++ b/public/translations/th/default.json @@ -50,7 +50,6 @@ "gif": "จีไอเอฟ", "total_posts": "โพสต์ทั้งหมด:", "current_users": "ผู้ใช้งานปัจจุบัน:", - "boards_tracked": "บอร์ดที่ติดตาม:", "challenge_counter": "{{index}} จาก {{total}}", "embed": "ฝัง", "remove": "ลบ", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "วิดีโอ YouTube", - "youtube_thumbnail_link_conversion_notice": "ตรวจพบลิงก์ YouTube กำลังวางรูปภาพขนาดย่อในอีก {{count}}..." + "youtube_thumbnail_link_conversion_notice": "ตรวจพบลิงก์ YouTube กำลังวางรูปภาพขนาดย่อในอีก {{count}}...", + "stats_scope_all_listed_boards": "บอร์ดที่แสดงทั้งหมด (ช้า)", + "stats_scope_directory_boards": "เฉพาะบอร์ดในไดเรกทอรี (เร็ว)", + "boards_loaded": "บอร์ดที่โหลดแล้ว:" } diff --git a/public/translations/tr/default.json b/public/translations/tr/default.json index 9dfb080c..843700f0 100644 --- a/public/translations/tr/default.json +++ b/public/translations/tr/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Toplam Gönderi:", "current_users": "mevcut kullanıcılar:", - "boards_tracked": "takip edilen panolar:", "challenge_counter": "{{index}} / {{total}}", "embed": "göm", "remove": "kaldır", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube videosu", - "youtube_thumbnail_link_conversion_notice": "YouTube bağlantısı algılandı. Küçük resim {{count}} içinde yapıştırılıyor..." + "youtube_thumbnail_link_conversion_notice": "YouTube bağlantısı algılandı. Küçük resim {{count}} içinde yapıştırılıyor...", + "stats_scope_all_listed_boards": "Listelenen tüm panolar (yavaş)", + "stats_scope_directory_boards": "Yalnızca dizin panolari (hızlı)", + "boards_loaded": "yüklenen panolar:" } diff --git a/public/translations/uk/default.json b/public/translations/uk/default.json index 2e2080b0..1c5c37b4 100644 --- a/public/translations/uk/default.json +++ b/public/translations/uk/default.json @@ -50,7 +50,6 @@ "gif": "ГІФ", "total_posts": "Всього повідомлень:", "current_users": "поточні користувачі:", - "boards_tracked": "відстежувані форуми:", "challenge_counter": "{{index}} з {{total}}", "embed": "вбудувати", "remove": "видалити", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "відео YouTube", - "youtube_thumbnail_link_conversion_notice": "Виявлено посилання YouTube. Вставляю мініатюру через {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Виявлено посилання YouTube. Вставляю мініатюру через {{count}}...", + "stats_scope_all_listed_boards": "Усі перелічені дошки (повільно)", + "stats_scope_directory_boards": "Лише дошки каталогу (швидко)", + "boards_loaded": "завантажені дошки:" } diff --git a/public/translations/ur/default.json b/public/translations/ur/default.json index 12762325..1bab8998 100644 --- a/public/translations/ur/default.json +++ b/public/translations/ur/default.json @@ -50,7 +50,6 @@ "gif": "جی آئی ایف", "total_posts": "کل پوسٹس:", "current_users": "موجودہ صارفین:", - "boards_tracked": "ٹریک کیے گئے بورڈز:", "challenge_counter": "{{index}} میں سے {{total}}", "embed": "ایمبیڈ", "remove": "ہٹائیں", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube ویڈیو", - "youtube_thumbnail_link_conversion_notice": "YouTube لنک مل گیا۔ {{count}} میں تھمب نیل تصویر پیسٹ کی جا رہی ہے..." + "youtube_thumbnail_link_conversion_notice": "YouTube لنک مل گیا۔ {{count}} میں تھمب نیل تصویر پیسٹ کی جا رہی ہے...", + "stats_scope_all_listed_boards": "تمام فہرست شدہ بورڈز (سست)", + "stats_scope_directory_boards": "صرف ڈائریکٹری بورڈز (تیز)", + "boards_loaded": "لوڈ شدہ بورڈز:" } diff --git a/public/translations/vi/default.json b/public/translations/vi/default.json index 02b39b89..8cfa60e7 100644 --- a/public/translations/vi/default.json +++ b/public/translations/vi/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "Tổng số bài viết:", "current_users": "người dùng hiện tại:", - "boards_tracked": "bảng được theo dõi:", "challenge_counter": "{{index}} trong {{total}}", "embed": "nhúng", "remove": "xóa", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "video YouTube", - "youtube_thumbnail_link_conversion_notice": "Đã phát hiện liên kết YouTube. Đang dán ảnh thu nhỏ sau {{count}}..." + "youtube_thumbnail_link_conversion_notice": "Đã phát hiện liên kết YouTube. Đang dán ảnh thu nhỏ sau {{count}}...", + "stats_scope_all_listed_boards": "Tất cả bảng được liệt kê (chậm)", + "stats_scope_directory_boards": "Chỉ bảng trong thư mục (nhanh)", + "boards_loaded": "bảng đã tải:" } diff --git a/public/translations/zh/default.json b/public/translations/zh/default.json index 6e332675..559c07da 100644 --- a/public/translations/zh/default.json +++ b/public/translations/zh/default.json @@ -50,7 +50,6 @@ "gif": "GIF", "total_posts": "总帖数:", "current_users": "当前用户:", - "boards_tracked": "跟踪的板块:", "challenge_counter": "{{index}} / {{total}}", "embed": "嵌入", "remove": "移除", @@ -401,5 +400,8 @@ "tag": "tag", "post_form_flash_upload_prompt": "Recommended SWF host: Catbox. Upload a .swf, then paste the direct https://files.catbox.moe/...swf link in Link. Other hosts must provide a direct HTTPS .swf URL with CORS enabled.", "youtube_video": "YouTube 视频", - "youtube_thumbnail_link_conversion_notice": "检测到 YouTube 链接。将在 {{count}} 后粘贴缩略图..." + "youtube_thumbnail_link_conversion_notice": "检测到 YouTube 链接。将在 {{count}} 后粘贴缩略图...", + "stats_scope_all_listed_boards": "所有已列出的版块(较慢)", + "stats_scope_directory_boards": "仅目录板块(快速)", + "boards_loaded": "已加载的版块:" } diff --git a/src/stores/__tests__/persisted-extra-stores.test.ts b/src/stores/__tests__/persisted-extra-stores.test.ts index 089b09f9..9f0c49d9 100644 --- a/src/stores/__tests__/persisted-extra-stores.test.ts +++ b/src/stores/__tests__/persisted-extra-stores.test.ts @@ -6,6 +6,7 @@ const flushMicrotasks = async () => { }; const loadBlotterVisibilityStore = async () => (await import('../use-blotter-visibility-store')).default; +const loadHomepageStatsOptionsStore = async () => (await import('../use-homepage-stats-options-store')).default; const loadModQueueStore = async () => (await import('../use-mod-queue-store')).default; const loadPopularThreadsOptionsStore = async () => (await import('../use-popular-threads-options-store')).default; @@ -56,6 +57,17 @@ describe('persisted extra stores', () => { expect(localStorage.getItem('showNsfwContentOnly')).toBe('true'); }); + it('loads homepage stats scope from localStorage defaults and persists changes', async () => { + const useHomepageStatsOptionsStore = await loadHomepageStatsOptionsStore(); + + expect(useHomepageStatsOptionsStore.getState().statsScope).toBe('directory'); + + useHomepageStatsOptionsStore.getState().setStatsScope('all'); + + expect(useHomepageStatsOptionsStore.getState().statsScope).toBe('all'); + expect(localStorage.getItem('5chan-homepage-stats-scope')).toBe('all'); + }); + it('migrates legacy mod queue storage and computes threshold seconds from the active unit', async () => { localStorage.setItem( 'mod-queue-storage', diff --git a/src/stores/use-homepage-stats-options-store.ts b/src/stores/use-homepage-stats-options-store.ts new file mode 100644 index 00000000..60ff6ff4 --- /dev/null +++ b/src/stores/use-homepage-stats-options-store.ts @@ -0,0 +1,22 @@ +import { create } from 'zustand'; + +export type HomepageStatsScope = 'directory' | 'all'; + +const LOCALSTORAGE_KEY = '5chan-homepage-stats-scope'; + +const readInitialStatsScope = (): HomepageStatsScope => (localStorage.getItem(LOCALSTORAGE_KEY) === 'all' ? 'all' : 'directory'); + +interface HomepageStatsOptionsStore { + statsScope: HomepageStatsScope; + setStatsScope: (value: HomepageStatsScope) => void; +} + +const useHomepageStatsOptionsStore = create((set) => ({ + statsScope: readInitialStatsScope(), + setStatsScope: (value) => { + set({ statsScope: value }); + localStorage.setItem(LOCALSTORAGE_KEY, value); + }, +})); + +export default useHomepageStatsOptionsStore; diff --git a/src/views/home/__tests__/home.test.tsx b/src/views/home/__tests__/home.test.tsx index 0d9addb0..1c221813 100644 --- a/src/views/home/__tests__/home.test.tsx +++ b/src/views/home/__tests__/home.test.tsx @@ -15,9 +15,9 @@ const testState = vi.hoisted(() => ({ directoryAddresses: [] as string[], directoryListCodes: [] as string[], directoryListsByCode: {} as Record }>, - loadingStartTimestamps: [] as number[], - nowSeconds: 1_704_067_210, navigateMock: vi.fn(), + setStatsScopeMock: vi.fn(), + statsScope: 'directory' as 'directory' | 'all', communities: {} as Record, communityStats: {} as Record, feedStateString: 'Downloading boards', @@ -70,12 +70,12 @@ vi.mock('../../../hooks/use-communities-stats', () => ({ useCommunitiesStatsStore: (selector: (state: { communityStats: typeof testState.communityStats }) => unknown) => selector({ communityStats: testState.communityStats }), })); -vi.mock('../../../stores/use-communities-loading-start-timestamps-store', () => ({ - default: () => testState.loadingStartTimestamps, -})); - -vi.mock('../../../hooks/use-now-seconds', () => ({ - useNowSeconds: () => testState.nowSeconds, +vi.mock('../../../stores/use-homepage-stats-options-store', () => ({ + default: (selector: (state: { statsScope: typeof testState.statsScope; setStatsScope: typeof testState.setStatsScopeMock }) => unknown) => + selector({ + statsScope: testState.statsScope, + setStatsScope: testState.setStatsScopeMock, + }), })); vi.mock('../../../hooks/use-state-string', () => ({ @@ -86,6 +86,11 @@ vi.mock('../../../components/loading-ellipsis', () => ({ default: ({ string }: { string: string }) => createElement('span', { 'data-testid': 'loading-ellipsis' }, string), })); +vi.mock('../../../components/tooltip', () => ({ + default: ({ content, children }: { content: React.ReactNode; children: React.ReactNode }) => + createElement('span', { 'data-testid': 'tooltip', 'data-content': typeof content === 'string' ? content : undefined }, children), +})); + vi.mock('../../../stores/use-directory-modal-store', () => ({ default: () => ({ closeDirectoryModal: testState.closeDirectoryModalMock, @@ -135,8 +140,6 @@ describe('Home', () => { testState.directoryAddresses = ['music-posting.eth', 'tech-posting.eth']; testState.directoryListCodes = []; testState.directoryListsByCode = {}; - testState.loadingStartTimestamps = []; - testState.nowSeconds = 1_704_067_210; testState.communities = { 'music-posting.eth': { address: 'music-posting.eth' }, 'tech-posting.eth': { address: 'tech-posting.eth' }, @@ -146,6 +149,10 @@ describe('Home', () => { 'tech-posting.eth': { allPostCount: 7, weekActiveUserCount: 5 }, }; testState.feedStateString = 'Downloading boards'; + testState.statsScope = 'directory'; + testState.setStatsScopeMock.mockImplementation((value: 'directory' | 'all') => { + testState.statsScope = value; + }); container = document.createElement('div'); document.body.appendChild(container); @@ -160,47 +167,62 @@ describe('Home', () => { it('renders the home view chrome, child sections, collectors, and aggregated stats', () => { renderHome(); - expect(vi.mocked(useFeedStateString)).not.toHaveBeenCalled(); + expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith([]); expect(document.title).toBe('5chan'); expect(container.querySelector('[data-testid="disclaimer-modal"]')?.textContent).toBe('disclaimer-modal'); expect(container.querySelector('[data-testid="directory-modal"]')?.textContent).toBe('directory-modal'); expect(container.querySelector('[data-testid="boards-list"]')?.textContent).toBe('boards:2'); expect(container.querySelector('[data-testid="popular-threads-box"]')?.textContent).toBe('popular:2:2'); expect(container.querySelectorAll('[data-testid="stats-collector"]')).toHaveLength(2); + expect(testState.directoryListCodes).toEqual([]); + expect(container.textContent).toContain('stats'); + expect(container.textContent).not.toContain('loaded 2/2 boards p2p'); + expect(container.querySelector('.yellowOfflineIcon')).toBeNull(); expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0); expect(container.textContent).toContain('total_posts 12'); expect(container.textContent).toContain('current_users 7'); - expect(container.textContent).toContain('boards_tracked 2'); + expect(container.textContent).toContain('boards_loaded 2'); expect(container.querySelector('[data-testid="site-legal-meta"]')?.textContent).toBe('site-legal-meta'); expect(container.querySelector('a[href="/pass"]')?.textContent).toBe('support_5chan'); }); - it('keeps the stats values loading until every directory has loaded stats', () => { + it('shows the stats state string while no board stats have loaded yet', () => { + testState.communityStats = {}; + + renderHome(); + + expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith(['music-posting.eth', 'tech-posting.eth']); + expect(container.querySelector('.yellowOfflineIcon')).not.toBeNull(); + expect(container.querySelector('[data-testid="tooltip"]')?.getAttribute('data-content')).toBe('Downloading boards'); + expect(container.querySelector('[data-testid="loading-ellipsis"]')?.textContent).toBe('Downloading boards'); + expect(container.textContent).not.toContain('total_posts'); + expect(container.textContent).not.toContain('boards_loaded'); + }); + + it('shows partial stats while some boards are still loading', () => { testState.communityStats = { 'music-posting.eth': { allPostCount: 5, weekActiveUserCount: 2 }, }; renderHome(); - const loadingValues = Array.from(container.querySelectorAll('[data-testid="loading-ellipsis"]')); - expect(loadingValues).toHaveLength(1); - expect(loadingValues[0]?.textContent).toBe('Downloading boards'); expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith(['music-posting.eth', 'tech-posting.eth']); - expect(container.textContent).not.toContain('total_posts'); - expect(container.textContent).not.toContain('current_users'); - expect(container.textContent).not.toContain('boards_tracked'); - expect(container.textContent).not.toContain('total_posts 5'); - expect(container.textContent).not.toContain('current_users 2'); - expect(container.textContent).not.toContain('boards_tracked 1'); + expect(container.querySelector('.yellowOfflineIcon')).not.toBeNull(); + expect(container.querySelector('[data-testid="tooltip"]')?.getAttribute('data-content')).toBe('Downloading boards'); + expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0); + expect(container.textContent).not.toContain('loaded 1/2 boards p2p'); + expect(container.textContent).toContain('total_posts 5'); + expect(container.textContent).toContain('current_users 2'); + expect(container.textContent).toContain('boards_loaded 1'); }); - it('uses a ranked directory fallback board when the default board stats stay unresolved', () => { + it('loads every listed board when the slow stats scope is selected', () => { + testState.statsScope = 'all'; testState.directories = [ { address: 'business-and-finance.bso', title: '/biz/ - Business & Finance', directoryCode: 'biz' }, { address: 'tech-posting.eth', title: '/g/ - Technology', directoryCode: 'g' }, ]; testState.directoryAddresses = ['business-and-finance.bso', 'tech-posting.eth']; - testState.loadingStartTimestamps = [1_704_067_170, 1_704_067_170]; testState.directoryListsByCode = { biz: { boards: [ @@ -217,12 +239,15 @@ describe('Home', () => { renderHome(); const collectorAddresses = Array.from(container.querySelectorAll('[data-testid="stats-collector"]')).map((collector) => collector.getAttribute('data-address')); - expect(testState.directoryListCodes).toEqual(['biz']); - expect(collectorAddresses).toEqual(['business-and-finance.bso', 'tech-posting.eth', 'backup-business.bso']); + expect(testState.directoryListCodes).toEqual(['biz', 'g']); + expect(collectorAddresses).toEqual(['business-and-finance.bso', 'backup-business.bso', 'tech-posting.eth']); + expect(vi.mocked(useFeedStateString)).toHaveBeenCalledWith(['business-and-finance.bso', 'backup-business.bso', 'tech-posting.eth']); + expect(container.querySelector('.yellowOfflineIcon')).not.toBeNull(); expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0); + expect(container.textContent).not.toContain('loaded 2/3 boards p2p'); expect(container.textContent).toContain('total_posts 18'); expect(container.textContent).toContain('current_users 7'); - expect(container.textContent).toContain('boards_tracked 2'); + expect(container.textContent).toContain('boards_loaded 2'); }); it('shows zero totals after every directory has loaded zero-count stats', () => { @@ -234,9 +259,11 @@ describe('Home', () => { renderHome(); expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0); + expect(container.querySelector('.yellowOfflineIcon')).toBeNull(); + expect(container.textContent).not.toContain('loaded 2/2 boards p2p'); expect(container.textContent).toContain('total_posts 0'); expect(container.textContent).toContain('current_users 0'); - expect(container.textContent).toContain('boards_tracked 2'); + expect(container.textContent).toContain('boards_loaded 2'); }); it('does not keep aggregate stats loading after a directory stats fetch fails', () => { @@ -248,9 +275,31 @@ describe('Home', () => { renderHome(); expect(container.querySelectorAll('[data-testid="loading-ellipsis"]')).toHaveLength(0); + expect(container.querySelector('.yellowOfflineIcon')).toBeNull(); + expect(container.textContent).not.toContain('loaded 2/2 boards p2p'); expect(container.textContent).toContain('total_posts 5'); expect(container.textContent).toContain('current_users 2'); - expect(container.textContent).toContain('boards_tracked 2'); + expect(container.textContent).toContain('boards_loaded 2'); + }); + + it('switches the stats scope from the options menu', () => { + renderHome(); + + const optionsButton = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'options ▼'); + expect(optionsButton).toBeTruthy(); + + act(() => { + optionsButton?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + }); + + const option = Array.from(container.querySelectorAll('button')).find((button) => button.textContent === 'stats_scope_all_listed_boards'); + expect(option).toBeTruthy(); + + act(() => { + option?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + }); + + expect(testState.setStatsScopeMock).toHaveBeenCalledWith('all'); }); it('navigates to the canonical board path when the search form is submitted', async () => { diff --git a/src/views/home/home.module.css b/src/views/home/home.module.css index e1c7f2bb..d0310e8a 100644 --- a/src/views/home/home.module.css +++ b/src/views/home/home.module.css @@ -57,7 +57,7 @@ font-weight: 700; } -.boxBar span, .boxBar > button { +.boxBar > span, .boxBar > button { all: unset; position: absolute; top: 0; @@ -69,7 +69,7 @@ line-height: inherit; } -.boxBar span:hover, .boxBar > button:hover { +.boxBar > span:hover, .boxBar > button:hover { cursor: pointer; color: var(--homepage-box-bar-text-color-hover); } @@ -135,13 +135,14 @@ top: 30px; right: 5px; display: inline-block; - position: absolute; text-align: right; background: #fff; border: 1px solid gray; box-shadow: 2px 2px 0 1px rgba(0, 0, 0, .1); padding: 4px 0; line-height: 1.7em; + min-width: max-content; + white-space: nowrap; z-index: 100; } @@ -155,12 +156,13 @@ all: unset; box-sizing: border-box; display: block; - width: 100%; + min-width: 100%; padding: 0 10px; color: inherit; font: inherit; line-height: inherit; text-align: right; + white-space: nowrap; } .filterModal .option:hover { @@ -259,6 +261,31 @@ color: #800; } +.statsTitle { + padding-right: 6em; + text-transform: none; +} + +.statsLoadingIconWrapper { + display: inline-flex; + align-items: center; + margin-left: 6px; + line-height: 1; + vertical-align: middle; +} + +.statsLoadingIconWrapper > span { + display: inline-flex; + align-items: center; +} + +.statsLoadingIcon { + box-sizing: border-box; + display: inline-block; + width: 14px; + height: 14px; +} + .stats .stat { display: inline-block; width: 33%; diff --git a/src/views/home/home.tsx b/src/views/home/home.tsx index d3da1120..df4cddbf 100644 --- a/src/views/home/home.tsx +++ b/src/views/home/home.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useRef, FormEvent } from 'react'; +import { useEffect, useMemo, useRef, useState, type FormEvent, type KeyboardEvent as ReactKeyboardEvent } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; import styles from './home.module.css'; @@ -9,15 +9,15 @@ import PopularThreadsBox from './popular-threads-box'; import BoardsList from './boards-list'; import SiteLegalMeta from '../../components/site-legal-meta'; import LoadingEllipsis from '../../components/loading-ellipsis'; -import { useFeedStateString } from '../../hooks/use-state-string'; +import Tooltip from '../../components/tooltip'; import useDirectoryModalStore from '../../stores/use-directory-modal-store'; +import useHomepageStatsOptionsStore, { type HomepageStatsScope } from '../../stores/use-homepage-stats-options-store'; import DisclaimerModal from '../../components/disclaimer-modal'; import DirectoryModal from '../../components/directory-modal'; import { extractDirectoryFromTitle, getBoardPath } from '../../lib/utils/route-utils'; import { isWebRuntime } from '../../lib/media-hosting/show-upload-controls'; +import { useFeedStateString } from '../../hooks/use-state-string'; import lowerCase from 'lodash/lowerCase'; -import useCommunitiesLoadingStartTimestamps from '../../stores/use-communities-loading-start-timestamps-store'; -import { useNowSeconds } from '../../hooks/use-now-seconds'; // https://github.com/bitsocialnet/lists/tree/master/5chan-directories @@ -106,103 +106,152 @@ type HomepageStats = { state?: string; }; -const StatsLoading = ({ communityAddresses }: { communityAddresses: string[] }) => { - const { t } = useTranslation(); - const loadingStateString = useFeedStateString(communityAddresses) || t('loading'); - - return ; -}; - -const STATS_DIRECTORY_FALLBACK_DELAY_SECONDS = 30; - const getDirectoryCode = (directory: DirectoryCommunity): string | null => directory.directoryCode ?? extractDirectoryFromTitle(directory.title ?? ''); const getUniqueAddresses = (addresses: string[]): string[] => [...new Set(addresses.filter((address) => address.length > 0))]; const hasLoadedStats = (stat: HomepageStats | undefined): stat is HomepageStats & { allPostCount: number } => stat?.allPostCount !== undefined; const hasFailedStats = (stat: HomepageStats | undefined): boolean => stat?.state === 'failed'; +const hasResolvedStats = (stat: HomepageStats | undefined): boolean => hasLoadedStats(stat) || hasFailedStats(stat); + +const STATS_SCOPE_OPTIONS: Array<{ scope: HomepageStatsScope; labelKey: string }> = [ + { scope: 'directory', labelKey: 'stats_scope_directory_boards' }, + { scope: 'all', labelKey: 'stats_scope_all_listed_boards' }, +]; +const EMPTY_STATS_LIST: string[] = []; + +const StatsOptionsModal = () => { + const { t } = useTranslation(); + const [showFilterModal, setShowFilterModal] = useState(false); + const modalRef = useRef(null); + const buttonRef = useRef(null); + const statsScope = useHomepageStatsOptionsStore((state) => state.statsScope); + const setStatsScope = useHomepageStatsOptionsStore((state) => state.setStatsScope); + + useEffect(() => { + if (!showFilterModal) return; + + const handleClickOutside = (event: globalThis.MouseEvent) => { + if (modalRef.current && !modalRef.current.contains(event.target as Node) && buttonRef.current && !buttonRef.current.contains(event.target as Node)) { + setShowFilterModal(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, [showFilterModal]); + + const selectScope = (scope: HomepageStatsScope) => { + setStatsScope(scope); + setShowFilterModal(false); + }; + + const handleScopeKey = (event: ReactKeyboardEvent, scope: HomepageStatsScope) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + selectScope(scope); + } + }; + + return ( + <> + + {showFilterModal && ( +
+ {STATS_SCOPE_OPTIONS.map((option) => ( + + ))} +
+ )} + + ); +}; const Stats = ({ directories }: { directories: DirectoryCommunity[] }) => { const { t } = useTranslation(); + const statsScope = useHomepageStatsOptionsStore((state) => state.statsScope); const communitiesStats = useCommunitiesStatsStore((state) => state.communityStats); const defaultDirectoryAddresses = useMemo(() => directories.map((directory) => directory.address), [directories]); - const loadingStartTimestamps = useCommunitiesLoadingStartTimestamps(defaultDirectoryAddresses); - const nowSeconds = useNowSeconds(defaultDirectoryAddresses.length > 0); - - const fallbackDirectoryCodes = useMemo( + const allDirectoryCodes = useMemo( () => - directories.flatMap((directory, index) => { - const defaultStats = communitiesStats[directory.address]; - if (hasLoadedStats(defaultStats)) { - return []; - } - - const loadingStartTimestamp = loadingStartTimestamps[index]; - if (!loadingStartTimestamp || nowSeconds - loadingStartTimestamp < STATS_DIRECTORY_FALLBACK_DELAY_SECONDS) { - return []; - } - - const directoryCode = getDirectoryCode(directory); - return directoryCode ? [directoryCode] : []; - }), - [communitiesStats, directories, loadingStartTimestamps, nowSeconds], + getUniqueAddresses( + directories.flatMap((directory) => { + const directoryCode = getDirectoryCode(directory); + return directoryCode ? [directoryCode] : []; + }), + ), + [directories], ); - const { listsByCode } = useDirectoryLists(fallbackDirectoryCodes); + const { listsByCode } = useDirectoryLists(statsScope === 'all' ? allDirectoryCodes : EMPTY_STATS_LIST); - const { collectorAddresses, totalPosts, currentUsers, boardsTracked, allDirectoryStatsLoaded } = useMemo(() => { - const collectorAddressSet = new Set(defaultDirectoryAddresses); + const collectorAddresses = useMemo(() => { + if (statsScope === 'directory') { + return defaultDirectoryAddresses; + } + + return getUniqueAddresses( + directories.flatMap((directory) => { + const directoryCode = getDirectoryCode(directory); + const directoryList = directoryCode ? listsByCode[directoryCode] : null; + const listAddresses = directoryList ? sortDirectoryBoardsByRank(directoryList.boards).map((board) => board.address) : []; + return listAddresses.length > 0 ? listAddresses : [directory.address]; + }), + ); + }, [defaultDirectoryAddresses, directories, listsByCode, statsScope]); + + const { totalPosts, currentUsers, boardsLoaded, boardsWithStats } = useMemo(() => { let totalPosts = 0; let currentUsers = 0; - let boardsTracked = 0; - let allDirectoryStatsLoaded = directories.length > 0; + let boardsLoaded = 0; + let boardsWithStats = 0; - for (const directory of directories) { - const directoryCode = getDirectoryCode(directory); - const directoryList = directoryCode ? listsByCode[directoryCode] : null; - const rankedAddresses = directoryList ? sortDirectoryBoardsByRank(directoryList.boards).map((board) => board.address) : [directory.address]; - const candidateAddresses = getUniqueAddresses(rankedAddresses.length > 0 ? rankedAddresses : [directory.address]); - - candidateAddresses.forEach((address) => collectorAddressSet.add(address)); - - let stat: HomepageStats | undefined; - let failedStat: HomepageStats | undefined; - let allCandidateStatsResolved = candidateAddresses.length > 0; - for (const address of candidateAddresses) { - const candidateStats = communitiesStats[address]; - if (hasLoadedStats(candidateStats)) { - stat = candidateStats; - break; - } - if (hasFailedStats(candidateStats)) { - failedStat ??= candidateStats; - continue; - } - allCandidateStatsResolved = false; - } - - if (!stat && allCandidateStatsResolved) { - stat = failedStat; - } - - if (!stat || (!hasLoadedStats(stat) && !hasFailedStats(stat))) { - allDirectoryStatsLoaded = false; + for (const address of collectorAddresses) { + const stats = communitiesStats[address]; + if (!hasResolvedStats(stats)) { continue; } - totalPosts += stat.allPostCount || 0; - currentUsers += stat.weekActiveUserCount || 0; - boardsTracked++; + boardsLoaded++; + if (hasLoadedStats(stats)) { + boardsWithStats++; + totalPosts += stats.allPostCount || 0; + currentUsers += stats.weekActiveUserCount || 0; + } } return { - collectorAddresses: [...collectorAddressSet], totalPosts, currentUsers, - boardsTracked, - allDirectoryStatsLoaded, + boardsLoaded, + boardsWithStats, }; - }, [communitiesStats, defaultDirectoryAddresses, directories, listsByCode]); + }, [collectorAddresses, communitiesStats]); + const hasDisplayableStats = boardsWithStats > 0 || (collectorAddresses.length > 0 && boardsLoaded === collectorAddresses.length); + const isStatsLoading = !hasDisplayableStats || boardsLoaded < collectorAddresses.length; + const loadingStateString = useFeedStateString(isStatsLoading ? collectorAddresses : EMPTY_STATS_LIST) || t('loading'); return ( <> @@ -212,10 +261,20 @@ const Stats = ({ directories }: { directories: DirectoryCommunity[] }) => { ))}
-

{t('stats')}

+

+ {t('stats')} + {isStatsLoading && ( + + + + + + )} +

+
- {allDirectoryStatsLoaded ? ( + {hasDisplayableStats ? ( <>
{t('total_posts')} {totalPosts} @@ -224,11 +283,11 @@ const Stats = ({ directories }: { directories: DirectoryCommunity[] }) => { {t('current_users')} {currentUsers}
- {t('boards_tracked')} {boardsTracked} + {t('boards_loaded')} {boardsLoaded}
) : ( - + )}