diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/BooksCatalogAsset.java b/controller/app/src/main/java/org/iiab/controller/redesign/BooksCatalogAsset.java index 23a9a4d49..d2491e006 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/BooksCatalogAsset.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/BooksCatalogAsset.java @@ -46,24 +46,27 @@ private BooksCatalogAsset() {} // Parsed once, kept in memory (already popularity-ordered by the generator). private static volatile List CACHE; - /** Search the offline catalog. lang: ""=all or an ISO code; q empty => popularity order. */ - public static void search(Context ctx, String q, String lang, int limit, BooksClient.ArrayCb cb) { + /** Search the offline catalog. lang: ""=all or an ISO code; q empty => popularity order. + * ADFA-5329: offset skips earlier batches so the caller can page through with "Load more". */ + public static void search(Context ctx, String q, String lang, int offset, int limit, BooksClient.ArrayCb cb) { final Context app = ctx.getApplicationContext(); AppExecutors.get().io().execute(() -> { try { List all = ensureLoaded(app); String term = q == null ? "" : q.trim().toLowerCase(Locale.ROOT); String lc = lang == null ? "" : lang.trim(); + int skip = Math.max(0, offset); JSONArray out = new JSONArray(); - int n = 0; + int matched = 0, taken = 0; for (JSONObject b : all) { if (!lc.isEmpty() && !lc.equalsIgnoreCase(b.optString("language"))) continue; if (!term.isEmpty()) { String hay = (b.optString("title") + " " + b.optString("author")).toLowerCase(Locale.ROOT); if (!hay.contains(term)) continue; } + if (matched++ < skip) continue; // already shown in an earlier batch out.put(b); - if (++n >= Math.max(1, limit)) break; + if (++taken >= Math.max(1, limit)) break; } MAIN.post(() -> cb.onOk(out)); } catch (Exception e) { diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/BooksClient.java b/controller/app/src/main/java/org/iiab/controller/redesign/BooksClient.java index a9f15d551..8831831e6 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/BooksClient.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/BooksClient.java @@ -4,7 +4,7 @@ * Author : AppDevForAll * Copyright : Copyright (c) 2026 AppDevForAll * Description : ADFA-4850. App-side client for the dashboard's Books REST endpoints: - * GET /api/books/search?q=&filter=&limit= -> catalog rows (offline FTS) + * GET /api/books/search?q=&filter=&lang=&offset=&limit= -> catalog rows (offline FTS) * GET /api/books/library -> Calibre-Web library rows * POST /api/books/library/:id/remove -> delete * (The actual download is a durable job driven by BooksDownloadService.) @@ -38,11 +38,13 @@ public interface ArrayCb { void onOk(JSONArray rows); void onErr(String message) public interface OkCb { void onOk(); void onErr(String message); } /** Search the offline Gutenberg catalog. filter: ""|"educational"; lang: ""=all or an ISO code; - * q empty => top-by-downloads. */ - public static void search(String q, String filter, String lang, int limit, ArrayCb cb) { + * q empty => top-by-downloads. ADFA-5329: offset pages through the results ("Load more"). + * The server must apply a stable order (popularity, then id) so batches don't overlap. */ + public static void search(String q, String filter, String lang, int offset, int limit, ArrayCb cb) { AppExecutors.get().io().execute(() -> { try { String url = BASE + "/search?limit=" + limit + + "&offset=" + Math.max(0, offset) + "&filter=" + enc(filter == null ? "" : filter) + "&lang=" + enc(lang == null ? "" : lang) + "&q=" + enc(q == null ? "" : q); diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/BooksLandingFragment.java b/controller/app/src/main/java/org/iiab/controller/redesign/BooksLandingFragment.java index da6a48e25..a12f2cb03 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/BooksLandingFragment.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/BooksLandingFragment.java @@ -68,6 +68,11 @@ public class BooksLandingFragment extends Fragment { // live download service. "In your books" here means "already in your setup order". private boolean wizard = false; + // ADFA-5329: incremental "Load more" — BATCH titles per tap; hasMore/loading drive the footer. + private static final int BATCH = 40; + private boolean hasMore = false; + private boolean loading = false; + /** Open the Books screen in wizard (pre-install, offline) mode. */ public static BooksLandingFragment newInstance(boolean wizard) { BooksLandingFragment f = new BooksLandingFragment(); @@ -229,27 +234,51 @@ private void loadLibrary() { }); } + /** (Re)load the first batch — called on open and whenever the filter, search or language changes. */ private void loadBooks() { - android.util.Log.d("K2Go-Books", "loadBooks wizard=" + wizard + " filter=" + filter + " lang=" + lang + " q=" + query); - status.setVisibility(View.VISIBLE); - status.setText(getString(R.string.k2go_books_loading)); - grid.removeAllViews(); + hasMore = true; + fetchBatch(0, false); + } + + /** ADFA-5329: append the next batch on demand ("Load more"). */ + private void loadMore() { + if (loading || !hasMore) return; + fetchBatch(books.size(), true); + } + + private void fetchBatch(int offset, boolean append) { + android.util.Log.d("K2Go-Books", "fetchBatch off=" + offset + " append=" + append + + " wizard=" + wizard + " filter=" + filter + " lang=" + lang + " q=" + query); + loading = true; + if (!append) { + books.clear(); + status.setVisibility(View.VISIBLE); + status.setText(getString(R.string.k2go_books_loading)); + grid.removeAllViews(); + } else { + render(); // show the "Loading…" footer while the next batch arrives + } BooksClient.ArrayCb cb = new BooksClient.ArrayCb() { @Override public void onOk(JSONArray rows) { if (!isAdded()) return; - books.clear(); + loading = false; + if (!append) books.clear(); for (int i = 0; i < rows.length(); i++) { JSONObject b = rows.optJSONObject(i); if (b != null) books.add(b); } + // The local library returns everything at once; a short batch means we hit the end. + hasMore = !isLocal() && rows.length() >= BATCH; render(); } @Override public void onErr(String m) { if (!isAdded()) return; + loading = false; + if (append) { render(); return; } // keep what we have; the Load more stays for a retry status.setVisibility(View.VISIBLE); status.setText(getString(wizard ? R.string.k2go_books_offline_error : R.string.k2go_books_unavailable)); } }; - if (wizard) BooksCatalogAsset.search(requireContext(), query, lang, 40, cb); + if (wizard) BooksCatalogAsset.search(requireContext(), query, lang, offset, BATCH, cb); else if (isLocal()) BooksClient.library(cb); - else BooksClient.search(query, filter, lang, 40, cb); + else BooksClient.search(query, filter, lang, offset, BATCH, cb); } private boolean inLibrary(JSONObject b) { @@ -279,9 +308,51 @@ private void render() { row.addView(pad, new LinearLayout.LayoutParams(0, 1, 1f)); } } + appendPaginationFooter(); // ADFA-5329: Load more / loading / end-of-list refreshFooter(); } + /** ADFA-5329: the "Load more" / loading / end-of-list footer, appended under the grid (inside the + * scroll, above the fixed Add bar). Not shown for the local library, which returns all rows. */ + private void appendPaginationFooter() { + if (isLocal() || books.isEmpty()) return; + if (loading) { + grid.addView(footerText(getString(R.string.k2go_books_loading), false)); + return; + } + if (hasMore) { + grid.addView(footerText(getString(R.string.k2go_books_showing_fmt, books.size()), false)); + TextView more = footerText(getString(R.string.k2go_books_load_more), true); + more.setOnClickListener(v -> loadMore()); + grid.addView(more); + } else { + grid.addView(footerText(getString(R.string.k2go_books_all_fmt, books.size()), false)); + } + } + + private TextView footerText(String text, boolean asButton) { + TextView t = new TextView(requireContext()); + t.setText(text); + t.setGravity(Gravity.CENTER); + LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT); + lp.setMargins(px(8), px(10), px(8), px(6)); + t.setLayoutParams(lp); + if (asButton) { + t.setPadding(px(10), px(12), px(10), px(12)); + t.setBackgroundResource(R.drawable.k2go_getmore_bg); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge); + t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_teal)); // after appearance + t.setClickable(true); + t.setFocusable(true); + } else { + t.setPadding(px(10), px(6), px(10), px(6)); + t.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall); + t.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted)); // after appearance + } + return t; + } + /** Cover colors chosen so a card never repeats its left neighbor's or the one above it (2-col * grid). Seeded from the title hash for variety, then nudged off any collision — no meaning, * just fewer same-color blocks. */ diff --git a/controller/app/src/main/res/values-ar/strings.xml b/controller/app/src/main/res/values-ar/strings.xml index f8067e845..23eee32c1 100644 --- a/controller/app/src/main/res/values-ar/strings.xml +++ b/controller/app/src/main/res/values-ar/strings.xml @@ -1286,4 +1286,7 @@ %1$d دورة لم تكتمل. أعد محاولة ما فشل قبل المغادرة — المغادرة تمسح هذه القائمة. تم تحديث الكتالوج في %1$s + تحميل المزيد + عرض %1$d كتاب + هذا كل شيء · %1$d كتاب diff --git a/controller/app/src/main/res/values-az/strings.xml b/controller/app/src/main/res/values-az/strings.xml index 8280a692e..2fa246848 100644 --- a/controller/app/src/main/res/values-az/strings.xml +++ b/controller/app/src/main/res/values-az/strings.xml @@ -1306,4 +1306,7 @@ %1$d kurs tamamlanmadı. Getməzdən əvvəl uğursuzları yenidən yoxlayın — çıxış bu siyahını silir. Kataloq %1$s tarixində yeniləndi + Daha çox yüklə + %1$d kitab göstərilir + Hamısı budur · %1$d kitab diff --git a/controller/app/src/main/res/values-bg/strings.xml b/controller/app/src/main/res/values-bg/strings.xml index aef55b869..cadf2159c 100644 --- a/controller/app/src/main/res/values-bg/strings.xml +++ b/controller/app/src/main/res/values-bg/strings.xml @@ -1293,4 +1293,7 @@ %1$d курса не завършиха. Опитайте отново неуспешните, преди да излезете — излизането изчиства този списък. Каталогът е обновен на %1$s + Зареди още + Показани %1$d книги + Това е всичко · %1$d книги diff --git a/controller/app/src/main/res/values-bn/strings.xml b/controller/app/src/main/res/values-bn/strings.xml index 3c5b35069..7e0c31ba3 100644 --- a/controller/app/src/main/res/values-bn/strings.xml +++ b/controller/app/src/main/res/values-bn/strings.xml @@ -1299,4 +1299,7 @@ %1$dটি কোর্স শেষ হয়নি। চলে যাওয়ার আগে ব্যর্থগুলো আবার চেষ্টা করুন — চলে গেলে এই তালিকা মুছে যাবে। ক্যাটালগ %1$s তারিখে আপডেট হয়েছে + আরও লোড করুন + %1$d টি বই দেখানো হচ্ছে + এটুকুই · %1$d টি বই diff --git a/controller/app/src/main/res/values-cs/strings.xml b/controller/app/src/main/res/values-cs/strings.xml index 2adb84883..f4ab07730 100644 --- a/controller/app/src/main/res/values-cs/strings.xml +++ b/controller/app/src/main/res/values-cs/strings.xml @@ -1293,4 +1293,7 @@ %1$d kurzů se nedokončilo. Než odejdete, zkuste neúspěšné znovu — odchodem se tento seznam smaže. Katalog aktualizován %1$s + Načíst další + Zobrazeno %1$d knih + To je vše · %1$d knih diff --git a/controller/app/src/main/res/values-de/strings.xml b/controller/app/src/main/res/values-de/strings.xml index 2510e2136..30ef6a600 100644 --- a/controller/app/src/main/res/values-de/strings.xml +++ b/controller/app/src/main/res/values-de/strings.xml @@ -1286,4 +1286,7 @@ %1$d Kurse wurden nicht fertig. Wiederholen Sie die fehlgeschlagenen, bevor Sie gehen — beim Verlassen wird diese Liste gelöscht. Katalog aktualisiert am %1$s + Mehr laden + %1$d Bücher angezeigt + Das ist alles · %1$d Bücher diff --git a/controller/app/src/main/res/values-el/strings.xml b/controller/app/src/main/res/values-el/strings.xml index 2a61b1199..3cb1081e5 100644 --- a/controller/app/src/main/res/values-el/strings.xml +++ b/controller/app/src/main/res/values-el/strings.xml @@ -1293,4 +1293,7 @@ %1$d μαθήματα δεν ολοκληρώθηκαν. Δοκιμάστε ξανά όσα απέτυχαν πριν φύγετε — η έξοδος διαγράφει αυτή τη λίστα. Ο κατάλογος ενημερώθηκε στις %1$s + Φόρτωση περισσότερων + Εμφανίζονται %1$d βιβλία + Αυτά ήταν όλα · %1$d βιβλία diff --git a/controller/app/src/main/res/values-es/strings.xml b/controller/app/src/main/res/values-es/strings.xml index e66d5e9fa..81b276521 100644 --- a/controller/app/src/main/res/values-es/strings.xml +++ b/controller/app/src/main/res/values-es/strings.xml @@ -1362,4 +1362,7 @@ %1$d curso(s) no terminaron. Reintenta los que fallaron antes de salir: al salir se borra esta lista. Catálogo actualizado el %1$s + Cargar más + Mostrando %1$d libros + Eso es todo · %1$d libros diff --git a/controller/app/src/main/res/values-fa/strings.xml b/controller/app/src/main/res/values-fa/strings.xml index bf29379e0..040b0806a 100644 --- a/controller/app/src/main/res/values-fa/strings.xml +++ b/controller/app/src/main/res/values-fa/strings.xml @@ -1286,4 +1286,7 @@ %1$d دوره کامل نشد. پیش از خروج، موارد ناموفق را دوباره تلاش کنید — خروج این فهرست را پاک می‌کند. کاتالوگ در %1$s به‌روزرسانی شد + بارگذاری بیشتر + نمایش %1$d کتاب + همین · %1$d کتاب diff --git a/controller/app/src/main/res/values-fr/strings.xml b/controller/app/src/main/res/values-fr/strings.xml index ec7b5e489..d6bb61a2c 100644 --- a/controller/app/src/main/res/values-fr/strings.xml +++ b/controller/app/src/main/res/values-fr/strings.xml @@ -1373,4 +1373,7 @@ %1$d cours ne se sont pas terminés. Réessayez ceux qui ont échoué avant de partir : quitter efface cette liste. Catalogue mis à jour le %1$s + Charger plus + %1$d livres affichés + C\'est tout · %1$d livres diff --git a/controller/app/src/main/res/values-gu/strings.xml b/controller/app/src/main/res/values-gu/strings.xml index 8d594244b..088052da1 100644 --- a/controller/app/src/main/res/values-gu/strings.xml +++ b/controller/app/src/main/res/values-gu/strings.xml @@ -1299,4 +1299,7 @@ %1$d કોર્સ પૂરા થયા નથી. જતાં પહેલાં નિષ્ફળ થયેલા ફરી પ્રયાસ કરો — જવાથી આ યાદી ભૂંસાઈ જશે. કૅટલૉગ %1$s ના રોજ અપડેટ થયું + વધુ લોડ કરો + %1$d પુસ્તકો બતાવ્યાં + એટલું જ · %1$d પુસ્તકો diff --git a/controller/app/src/main/res/values-hi/strings.xml b/controller/app/src/main/res/values-hi/strings.xml index 5a086a900..f71c70174 100644 --- a/controller/app/src/main/res/values-hi/strings.xml +++ b/controller/app/src/main/res/values-hi/strings.xml @@ -1363,4 +1363,7 @@ %1$d कोर्स पूरे नहीं हुए। जाने से पहले विफल हुए फिर से आज़माएँ — जाने पर यह सूची मिट जाएगी। कैटलॉग %1$s को अपडेट हुआ + और लोड करें + %1$d किताबें दिखाई जा रही हैं + बस इतना ही · %1$d किताबें diff --git a/controller/app/src/main/res/values-hu/strings.xml b/controller/app/src/main/res/values-hu/strings.xml index 16f779336..bed04773e 100644 --- a/controller/app/src/main/res/values-hu/strings.xml +++ b/controller/app/src/main/res/values-hu/strings.xml @@ -1286,4 +1286,7 @@ %1$d tanfolyam nem fejeződött be. Távozás előtt próbálja újra a sikerteleneket — a kilépés törli ezt a listát. Katalógus frissítve: %1$s + Továbbiak betöltése + %1$d könyv megjelenítve + Ennyi · %1$d könyv diff --git a/controller/app/src/main/res/values-in/strings.xml b/controller/app/src/main/res/values-in/strings.xml index 26111e8a5..580c98f49 100644 --- a/controller/app/src/main/res/values-in/strings.xml +++ b/controller/app/src/main/res/values-in/strings.xml @@ -1293,4 +1293,7 @@ %1$d kursus tidak selesai. Coba lagi yang gagal sebelum keluar — keluar akan menghapus daftar ini. Katalog diperbarui pada %1$s + Muat lebih banyak + Menampilkan %1$d buku + Itu saja · %1$d buku diff --git a/controller/app/src/main/res/values-it/strings.xml b/controller/app/src/main/res/values-it/strings.xml index aa9f4a619..bba8137ae 100644 --- a/controller/app/src/main/res/values-it/strings.xml +++ b/controller/app/src/main/res/values-it/strings.xml @@ -1286,4 +1286,7 @@ %1$d corsi non sono stati completati. Riprova quelli falliti prima di uscire: uscendo questa lista viene cancellata. Catalogo aggiornato il %1$s + Carica altro + %1$d libri mostrati + È tutto · %1$d libri diff --git a/controller/app/src/main/res/values-ja/strings.xml b/controller/app/src/main/res/values-ja/strings.xml index a0ac87d4a..381dba6fc 100644 --- a/controller/app/src/main/res/values-ja/strings.xml +++ b/controller/app/src/main/res/values-ja/strings.xml @@ -1287,4 +1287,7 @@ %1$d 件のコースが完了しませんでした。 退出する前に失敗したものを再試行してください。退出するとこの一覧は消えます。 カタログの更新日: %1$s + もっと読み込む + %1$d 冊を表示中 + 以上です · %1$d 冊 diff --git a/controller/app/src/main/res/values-ko/strings.xml b/controller/app/src/main/res/values-ko/strings.xml index 1fa8cebdb..8c49048cb 100644 --- a/controller/app/src/main/res/values-ko/strings.xml +++ b/controller/app/src/main/res/values-ko/strings.xml @@ -1287,4 +1287,7 @@ %1$d개 강의가 완료되지 않았습니다. 나가기 전에 실패한 항목을 다시 시도하세요. 나가면 이 목록이 지워집니다. 카탈로그 업데이트: %1$s + 더 보기 + %1$d권 표시 중 + 여기까지입니다 · %1$d권 diff --git a/controller/app/src/main/res/values-lt/strings.xml b/controller/app/src/main/res/values-lt/strings.xml index 64e3767f7..ee9ab8c89 100644 --- a/controller/app/src/main/res/values-lt/strings.xml +++ b/controller/app/src/main/res/values-lt/strings.xml @@ -1303,4 +1303,7 @@ %1$d kursų nebaigta. Prieš išeidami pakartokite nepavykusius — išėjus šis sąrašas išvalomas. Katalogas atnaujintas %1$s + Įkelti daugiau + Rodoma knygų: %1$d + Tai viskas · %1$d knygų diff --git a/controller/app/src/main/res/values-nl/strings.xml b/controller/app/src/main/res/values-nl/strings.xml index ebb16c263..1f275af64 100644 --- a/controller/app/src/main/res/values-nl/strings.xml +++ b/controller/app/src/main/res/values-nl/strings.xml @@ -1286,4 +1286,7 @@ %1$d cursussen zijn niet afgerond. Probeer de mislukte opnieuw voordat u weggaat — weggaan wist deze lijst. Catalogus bijgewerkt op %1$s + Meer laden + %1$d boeken weergegeven + Dat is alles · %1$d boeken diff --git a/controller/app/src/main/res/values-no/strings.xml b/controller/app/src/main/res/values-no/strings.xml index 480ee893b..ef3d1f024 100644 --- a/controller/app/src/main/res/values-no/strings.xml +++ b/controller/app/src/main/res/values-no/strings.xml @@ -1296,4 +1296,7 @@ %1$d kurs ble ikke ferdige. Prøv de mislykkede på nytt før du går — å gå tømmer denne listen. Katalog oppdatert %1$s + Last inn mer + Viser %1$d bøker + Det var alt · %1$d bøker diff --git a/controller/app/src/main/res/values-pl/strings.xml b/controller/app/src/main/res/values-pl/strings.xml index 7b8750ed6..97a2c097f 100644 --- a/controller/app/src/main/res/values-pl/strings.xml +++ b/controller/app/src/main/res/values-pl/strings.xml @@ -1296,4 +1296,7 @@ %1$d kursów nie ukończono. Ponów nieudane przed wyjściem — wyjście czyści tę listę. Katalog zaktualizowano %1$s + Załaduj więcej + Wyświetlono %1$d książek + To wszystko · %1$d książek diff --git a/controller/app/src/main/res/values-pt/strings.xml b/controller/app/src/main/res/values-pt/strings.xml index a9c776a40..4476739af 100644 --- a/controller/app/src/main/res/values-pt/strings.xml +++ b/controller/app/src/main/res/values-pt/strings.xml @@ -1365,4 +1365,7 @@ %1$d curso(s) não terminaram. Tente de novo os que falharam antes de sair: sair apaga esta lista. Catálogo atualizado em %1$s + Carregar mais + A mostrar %1$d livros + É tudo · %1$d livros diff --git a/controller/app/src/main/res/values-ro/strings.xml b/controller/app/src/main/res/values-ro/strings.xml index 3b72ed08f..68331b4a4 100644 --- a/controller/app/src/main/res/values-ro/strings.xml +++ b/controller/app/src/main/res/values-ro/strings.xml @@ -1286,4 +1286,7 @@ %1$d cursuri nu s-au terminat. Reîncearcă-le pe cele eșuate înainte să pleci — plecarea șterge lista. Catalog actualizat pe %1$s + Încarcă mai multe + %1$d cărți afișate + Asta e tot · %1$d cărți diff --git a/controller/app/src/main/res/values-ru-rRU/strings.xml b/controller/app/src/main/res/values-ru-rRU/strings.xml index b60eda272..8693c79bb 100644 --- a/controller/app/src/main/res/values-ru-rRU/strings.xml +++ b/controller/app/src/main/res/values-ru-rRU/strings.xml @@ -1362,4 +1362,7 @@ %1$d курс(ов) не завершились. Повторите неудавшиеся перед выходом — выход очищает этот список. Каталог обновлён %1$s + Загрузить ещё + Показано книг: %1$d + Это всё · %1$d книг diff --git a/controller/app/src/main/res/values-sk/strings.xml b/controller/app/src/main/res/values-sk/strings.xml index 966d9aa35..6e6812883 100644 --- a/controller/app/src/main/res/values-sk/strings.xml +++ b/controller/app/src/main/res/values-sk/strings.xml @@ -1293,4 +1293,7 @@ %1$d kurzov sa nedokončilo. Pred odchodom skúste neúspešné znova — odchodom sa tento zoznam vymaže. Katalóg aktualizovaný %1$s + Načítať ďalšie + Zobrazených %1$d kníh + To je všetko · %1$d kníh diff --git a/controller/app/src/main/res/values-sr/strings.xml b/controller/app/src/main/res/values-sr/strings.xml index bead64b73..c48190a65 100644 --- a/controller/app/src/main/res/values-sr/strings.xml +++ b/controller/app/src/main/res/values-sr/strings.xml @@ -1299,4 +1299,7 @@ %1$d курсева није завршено. Покушајте поново неуспеле пре него што одете — одлазак брише овај списак. Каталог ажуриран %1$s + Учитај још + Приказано %1$d књига + То је све · %1$d књига diff --git a/controller/app/src/main/res/values-sw/strings.xml b/controller/app/src/main/res/values-sw/strings.xml index c1cd56741..7b9866730 100644 --- a/controller/app/src/main/res/values-sw/strings.xml +++ b/controller/app/src/main/res/values-sw/strings.xml @@ -1306,4 +1306,7 @@ Kozi %1$d hazikukamilika. Jaribu tena zilizoshindwa kabla ya kuondoka — kuondoka kunafuta orodha hii. Katalogi ilisasishwa tarehe %1$s + Pakia zaidi + Inaonyesha vitabu %1$d + Ndio hivyo · vitabu %1$d diff --git a/controller/app/src/main/res/values-ta/strings.xml b/controller/app/src/main/res/values-ta/strings.xml index 23bf36d5c..35fd447d1 100644 --- a/controller/app/src/main/res/values-ta/strings.xml +++ b/controller/app/src/main/res/values-ta/strings.xml @@ -1306,4 +1306,7 @@ %1$d பாடநெறிகள் முடியவில்லை. வெளியேறும் முன் தோல்வியடைந்தவற்றை மீண்டும் முயற்சிக்கவும் — வெளியேறினால் இந்தப் பட்டியல் அழிக்கப்படும். பட்டியல் %1$s அன்று புதுப்பிக்கப்பட்டது + மேலும் ஏற்று + %1$d புத்தகங்கள் காட்டப்படுகின்றன + இவ்வளவுதான் · %1$d புத்தகங்கள் diff --git a/controller/app/src/main/res/values-tr/strings.xml b/controller/app/src/main/res/values-tr/strings.xml index 81fc06abb..6d679154a 100644 --- a/controller/app/src/main/res/values-tr/strings.xml +++ b/controller/app/src/main/res/values-tr/strings.xml @@ -1286,4 +1286,7 @@ %1$d kurs tamamlanmadı. Ayrılmadan önce başarısız olanları yeniden deneyin — ayrılmak bu listeyi siler. Katalog %1$s tarihinde güncellendi + Daha fazla yükle + %1$d kitap gösteriliyor + Hepsi bu · %1$d kitap diff --git a/controller/app/src/main/res/values-uk/strings.xml b/controller/app/src/main/res/values-uk/strings.xml index c050cc630..ea1c13bb1 100644 --- a/controller/app/src/main/res/values-uk/strings.xml +++ b/controller/app/src/main/res/values-uk/strings.xml @@ -1286,4 +1286,7 @@ %1$d курс(ів) не завершено. Повторіть невдалі перед виходом — вихід очищає цей список. Каталог оновлено %1$s + Завантажити ще + Показано книг: %1$d + Це все · %1$d книг diff --git a/controller/app/src/main/res/values-vi/strings.xml b/controller/app/src/main/res/values-vi/strings.xml index 873a5c6d7..e4534b4e2 100644 --- a/controller/app/src/main/res/values-vi/strings.xml +++ b/controller/app/src/main/res/values-vi/strings.xml @@ -1286,4 +1286,7 @@ %1$d khóa học chưa hoàn tất. Hãy thử lại những mục lỗi trước khi rời đi — rời đi sẽ xóa danh sách này. Danh mục cập nhật ngày %1$s + Tải thêm + Đang hiển thị %1$d cuốn sách + Hết rồi · %1$d cuốn sách diff --git a/controller/app/src/main/res/values-yo/strings.xml b/controller/app/src/main/res/values-yo/strings.xml index 3ef256eda..9237c7cec 100644 --- a/controller/app/src/main/res/values-yo/strings.xml +++ b/controller/app/src/main/res/values-yo/strings.xml @@ -1306,4 +1306,7 @@ %1$d ìdánilẹ́kọ̀ọ́ kò parí. Tún gbìyànjú àwọn tí ó kùnà kí o tó lọ — lílọ máa pa àtòjọ yìí rẹ́. A ṣe àfikún àtòjọ ní %1$s + Gbé afikun wọlé + Ń fi ìwé %1$d hàn + Ìyẹn ni gbogbo rẹ̀ · ìwé %1$d diff --git a/controller/app/src/main/res/values-zh-rCN/strings.xml b/controller/app/src/main/res/values-zh-rCN/strings.xml index 0f924e3da..e8f33ce0d 100644 --- a/controller/app/src/main/res/values-zh-rCN/strings.xml +++ b/controller/app/src/main/res/values-zh-rCN/strings.xml @@ -1287,4 +1287,7 @@ %1$d 门课程未完成。 离开前请重试失败的项目——离开会清空此列表。 目录更新于 %1$s + 加载更多 + 正在显示 %1$d 本书 + 就这些 · %1$d 本书 diff --git a/controller/app/src/main/res/values/strings.xml b/controller/app/src/main/res/values/strings.xml index f3e25ccd7..80435d30e 100644 --- a/controller/app/src/main/res/values/strings.xml +++ b/controller/app/src/main/res/values/strings.xml @@ -1240,6 +1240,9 @@ Your selection. One at a time %1$d books + Load more + Showing %1$d books + That\'s all · %1$d books Downloads Added one at a time — kind to Project Gutenberg. %1$d of %2$d books diff --git a/static/dashboard/routes.ts b/static/dashboard/routes.ts index c84bfdd43..16d2ad9db 100644 --- a/static/dashboard/routes.ts +++ b/static/dashboard/routes.ts @@ -65,13 +65,15 @@ export const apiRouter: Router = express.Router(); // (POST /books/download). These paths don't collide with the generic /:type/* routes below. // Search the offline Gutenberg catalog. ?q= (FTS) | ?filter=educational | (default) top-by-downloads. +// ADFA-5329: ?offset= pages through the results for the client's "Load more". apiRouter.get('/books/search', (req: Request, res: Response): void => { try { const q = String(req.query.q ?? ''); const filter = String(req.query.filter ?? ''); const lang = String(req.query.lang ?? ''); + const offset = parseInt(String(req.query.offset ?? '0'), 10); const limit = parseInt(String(req.query.limit ?? '40'), 10); - res.json(searchCatalog(q, filter, lang, limit)); + res.json(searchCatalog(q, filter, lang, offset, limit)); } catch (e: any) { res.status(500).json({ error: e?.message || 'search failed' }); } diff --git a/static/dashboard/sockets/books.query.ts b/static/dashboard/sockets/books.query.ts index dd988a6d2..2a31cc5e8 100644 --- a/static/dashboard/sockets/books.query.ts +++ b/static/dashboard/sockets/books.query.ts @@ -55,10 +55,13 @@ function coverUrl(id: number | string): string { } /** FTS/browse the offline catalog. query MATCH (prefix) | 'educational' | top-by-downloads. - * An optional ISO language code (e.g. "en") narrows every branch to that language. */ -export function searchCatalog(q: string, filter: string, lang: string, limit: number): CatalogBook[] { + * An optional ISO language code (e.g. "en") narrows every branch to that language. + * ADFA-5329: offset pages through the results ("Load more"); every branch has a stable tiebreaker + * on gutenberg_id so consecutive batches don't overlap or skip when rank/downloads tie. */ +export function searchCatalog(q: string, filter: string, lang: string, offset: number, limit: number): CatalogBook[] { if (!fs.existsSync(CATALOG_DB_PATH)) throw new Error('catalog database not found (sync first)'); const lim = Math.max(1, Math.min(200, Number.isFinite(limit) ? limit : 40)); + const off = Math.max(0, Number.isFinite(offset) ? Math.trunc(offset) : 0); const cols = 'gutenberg_id, title, author, language, download_url, description'; const useLang = typeof lang === 'string' && lang.trim().length > 0; const langArg: any[] = useLang ? [lang.trim()] : []; @@ -68,18 +71,18 @@ export function searchCatalog(q: string, filter: string, lang: string, limit: nu if (q && q.trim().length > 0) { const langClause = useLang ? ' AND language = ?' : ''; rows = db.prepare( - `SELECT ${cols} FROM catalog WHERE catalog MATCH ?${langClause} ORDER BY rank LIMIT ?` - ).all(q.trim() + '*', ...langArg, lim); + `SELECT ${cols} FROM catalog WHERE catalog MATCH ?${langClause} ORDER BY rank, gutenberg_id LIMIT ? OFFSET ?` + ).all(q.trim() + '*', ...langArg, lim, off); } else if (filter === 'educational') { const langClause = useLang ? ' AND language = ?' : ''; rows = db.prepare( - `SELECT ${cols} FROM catalog WHERE (bookshelves LIKE '%Children%' OR bookshelves LIKE '%Education%')${langClause} ORDER BY downloads DESC LIMIT ?` - ).all(...langArg, lim); + `SELECT ${cols} FROM catalog WHERE (bookshelves LIKE '%Children%' OR bookshelves LIKE '%Education%')${langClause} ORDER BY downloads DESC, gutenberg_id DESC LIMIT ? OFFSET ?` + ).all(...langArg, lim, off); } else { const whereLang = useLang ? ' WHERE language = ?' : ''; rows = db.prepare( - `SELECT ${cols} FROM catalog${whereLang} ORDER BY downloads DESC LIMIT ?` - ).all(...langArg, lim); + `SELECT ${cols} FROM catalog${whereLang} ORDER BY downloads DESC, gutenberg_id DESC LIMIT ? OFFSET ?` + ).all(...langArg, lim, off); } return rows.map((r) => ({ ...r, cover_url: coverUrl(r.gutenberg_id) })) as CatalogBook[]; } finally {