diff --git a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java index 40bfef72..7ac94462 100644 --- a/controller/app/src/main/java/org/iiab/controller/PortalActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/PortalActivity.java @@ -69,6 +69,7 @@ public class PortalActivity extends AppCompatActivity { private GestureWebView webView; private org.iiab.controller.redesign.FqrController fqr; // ADFA-4879: FQR maps (only on /maps/) + private org.iiab.controller.redesign.KiwixManageController kiwixMgr; // ADFA-5004: ZIM delete (only on /kiwix/) private static final long AUTO_HIDE_MS = 4000L; // ADFA-4887: nav-bar auto-hide after inactivity private boolean fullscreenOn = false; // ADFA-4887: Home button toggles fullscreen private Handler hideHandler; // ADFA-4887: nav-bar auto-hide (cleared in onDestroy) @@ -223,6 +224,8 @@ public void onPageFinished(WebView view, String url) { // ADFA-4879: arm/disarm in-app FQR maps depending on whether this is /maps/. if (fqr != null) fqr.onPageFinished(url); + // ADFA-5004: arm/disarm in-app ZIM manager depending on whether this is /kiwix/. + if (kiwixMgr != null) kiwixMgr.onPageFinished(url); } @Override @@ -331,6 +334,10 @@ public boolean onConsoleMessage(android.webkit.ConsoleMessage consoleMessage) { fqr = new org.iiab.controller.redesign.FqrController(this, webView); fqr.prepareForUrl(finalTargetUrl); + // ADFA-5004: ZIM manager lives in this same shared WebView but activates only on /kiwix/ + // (gated in KiwixManageController#onPageFinished). + kiwixMgr = new org.iiab.controller.redesign.KiwixManageController(this, webView); + // Native architecture: content is served locally; load it directly. webView.loadUrl(finalTargetUrl); } @@ -414,6 +421,7 @@ protected void onDestroy() { // ADFA-4879: stop FQR polling + drop its overlay/dialog so we don't leak the activity. // The durable server job (if any) keeps running and shows up on the next /maps/ reload. if (fqr != null) fqr.detach(); + if (kiwixMgr != null) kiwixMgr.detach(); // ADFA-5004 if (hideHandler != null && hideRunnable != null) hideHandler.removeCallbacks(hideRunnable); // ADFA-4887 super.onDestroy(); } diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/KiwixClient.java b/controller/app/src/main/java/org/iiab/controller/redesign/KiwixClient.java new file mode 100644 index 00000000..24b1e353 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/KiwixClient.java @@ -0,0 +1,116 @@ +/* + * ============================================================================ + * Name : KiwixClient.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : ADFA-5004. App-side client for the dashboard's Kiwix ZIM-management REST endpoints: + * GET /api/kiwix/library -> installed ZIMs [{ name, bytes, mtime }, ...] + * POST /api/kiwix/delete {name} -> { ok } (unlink + rebuild the Kiwix index) + * Mirrors BooksClient/MapsRegionClient: short, stateless calls off the main thread with + * results posted back to the UI. Only reachable on-device (localhost); the box denies + * /api to remote clients. The ZIM download itself stays a durable job (KiwixDownload). + * ============================================================================ + */ +package org.iiab.controller.redesign; + +import android.os.Handler; +import android.os.Looper; + +import org.iiab.controller.config.BoxEndpoints; +import org.iiab.controller.util.AppExecutors; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.charset.StandardCharsets; + +public final class KiwixClient { + private KiwixClient() {} + + private static final String BASE = BoxEndpoints.API + "/kiwix"; + private static final Handler MAIN = new Handler(Looper.getMainLooper()); + + public interface ArrayCb { void onOk(JSONArray rows); void onErr(String message); } + /** {@code deferred} = the file was removed but the reindex was skipped (a download is in + * progress); kiwix-serve reflects it after that download's own reindex. */ + public interface OkCb { void onOk(boolean deferred); void onErr(String message); } + + /** The ZIMs currently installed on the box (rows: { name, bytes, mtime }), newest first. */ + public static void library(ArrayCb cb) { + AppExecutors.get().io().execute(() -> { + try { + JSONArray a = new JSONArray(httpGet(BASE + "/library")); + MAIN.post(() -> cb.onOk(a)); + } catch (Exception e) { + MAIN.post(() -> cb.onErr("couldn't reach the content service")); + } + }); + } + + /** Delete one installed ZIM by its file name (e.g. "wikipedia_en_all_maxi_2024-01.zim"). + * The server removes the file and rebuilds the Kiwix index; this returns when that completes. */ + public static void delete(String name, OkCb cb) { + AppExecutors.get().io().execute(() -> { + try { + String resp = httpPostJson(BASE + "/delete", new JSONObject().put("name", name)); + final boolean deferred = !resp.isEmpty() && new JSONObject(resp).optBoolean("deferred", false); + MAIN.post(() -> cb.onOk(deferred)); + } catch (Exception e) { + MAIN.post(() -> cb.onErr("delete failed")); + } + }); + } + + private static String httpGet(String urlStr) throws Exception { + HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection(); + try { + c.setUseCaches(false); + c.setConnectTimeout(5000); + c.setReadTimeout(8000); + c.setRequestProperty("Accept", "application/json"); + int code = c.getResponseCode(); + String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream()); + if (code < 200 || code >= 400) throw new Exception("HTTP " + code + ": " + text); + return text.isEmpty() ? "[]" : text; + } finally { + c.disconnect(); + } + } + + /** POST a JSON body and return the response text (for reading fields like {@code deferred}). */ + private static String httpPostJson(String urlStr, JSONObject body) throws Exception { + HttpURLConnection c = (HttpURLConnection) new URL(urlStr).openConnection(); + try { + c.setUseCaches(false); + c.setConnectTimeout(5000); + // The delete rebuilds the Kiwix index server-side; give it room. + c.setReadTimeout(60000); + c.setRequestMethod("POST"); + c.setRequestProperty("Accept", "application/json"); + c.setDoOutput(true); + c.setRequestProperty("Content-Type", "application/json"); + byte[] payload = body.toString().getBytes(StandardCharsets.UTF_8); + try (OutputStream os = c.getOutputStream()) { os.write(payload); } + int code = c.getResponseCode(); + String text = readAll(code >= 200 && code < 400 ? c.getInputStream() : c.getErrorStream()); + if (code < 200 || code >= 400) throw new Exception("HTTP " + code + ": " + text); + return text; + } finally { + c.disconnect(); + } + } + + private static String readAll(InputStream is) throws Exception { + if (is == null) return ""; + ByteArrayOutputStream buf = new ByteArrayOutputStream(); + byte[] chunk = new byte[4096]; + int n; + while ((n = is.read(chunk)) != -1) buf.write(chunk, 0, n); + is.close(); + return buf.toString(StandardCharsets.UTF_8.name()); + } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/KiwixManageController.java b/controller/app/src/main/java/org/iiab/controller/redesign/KiwixManageController.java new file mode 100644 index 00000000..593800d3 --- /dev/null +++ b/controller/app/src/main/java/org/iiab/controller/redesign/KiwixManageController.java @@ -0,0 +1,401 @@ +/* + * ============================================================================ + * Name : KiwixManageController.java + * Author : AppDevForAll + * Copyright : Copyright (c) 2026 AppDevForAll + * Description : ADFA-5004. In-WebView management of installed Kiwix ZIMs, mirroring the Maps FQR + * region manager (FqrController) but for the "Read Wikipedia" page. Because that page + * is kiwix-serve (upstream) and carries no delete control of its own, this overlays a + * NATIVE trash button on the shared PortalActivity WebView, shown only while the + * /kiwix/ page is loaded. Tapping it opens a ~45% bottom sheet (title + search + the + * installed-ZIM list, each row with a Material delete icon in the M3 error colour, plus + * a pinned "Get more content" button to the Wikipedia & ZIM screen) backed by the REST + * endpoints (KiwixClient GET /kiwix/library, POST /kiwix/delete). After a delete the + * WebView reloads so kiwix-serve stops showing the removed book. + * + * Lifecycle mirrors FqrController: PortalActivity forwards onPageFinished(url) (arm on + * the kiwix page, disarm elsewhere) and onDestroy -> detach(). Theming uses a + * ContextThemeWrapper(Theme_K2Go) + Material 3 colour roles, like FqrController. + * ============================================================================ + */ +package org.iiab.controller.redesign; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Color; +import android.graphics.Typeface; +import android.graphics.drawable.GradientDrawable; +import android.view.ContextThemeWrapper; +import android.view.Gravity; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.WebView; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.ScrollView; +import android.widget.TextView; +import android.widget.Toast; + +import com.google.android.material.button.MaterialButton; +import com.google.android.material.color.MaterialColors; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import org.iiab.controller.R; +import org.iiab.controller.util.ByteFormatter; +import org.iiab.controller.util.Snackbars; +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Set; + +public final class KiwixManageController { + + private final Activity activity; + private final WebView webView; + private final Context themed; + private final int cSurface, cOnSurface, cOnSurfaceVariant, cError; + + private static final float SHEET_FRACTION = 0.45f; // overlay height; content stays visible above + + private boolean active = false; + private FloatingActionButton trigger; // native trash button (only on the kiwix page) + private View deleteSheet; + private EditText searchField; + private LinearLayout listContainer; + private final List items = new ArrayList<>(); + // ZIM file-name stems deleted while a download was in progress: hidden cosmetically in the reader + // until that download's reindex removes them for real. Re-applied on every kiwix page load. + private final Set pendingHidden = new HashSet<>(); + + public KiwixManageController(Activity activity, WebView webView) { + this.activity = activity; + this.webView = webView; + this.themed = new ContextThemeWrapper(activity, R.style.Theme_K2Go); + this.cSurface = attr(com.google.android.material.R.attr.colorSurface, 0xFF16201B); + this.cOnSurface = attr(com.google.android.material.R.attr.colorOnSurface, Color.WHITE); + this.cOnSurfaceVariant = attr(com.google.android.material.R.attr.colorOnSurfaceVariant, 0xFF8FA39B); + this.cError = attr(androidx.appcompat.R.attr.colorError, 0xFFE05353); + } + + private int attr(int attrId, int fallback) { + return MaterialColors.getColor(themed, attrId, fallback); + } + + // ---- lifecycle (forwarded by PortalActivity) -------------------------------------------- + + /** Arm on the box's /kiwix/ page, disarm (and drop UI) elsewhere. */ + public void onPageFinished(String url) { + active = isKiwixPage(url); + if (active) { + // Don't drop the FAB on top of an open sheet — a delete triggers webView.reload(), whose + // onPageFinished lands here while the sheet is still up. The sheet restores the FAB on close. + if (deleteSheet == null) showTrigger(); + applyHide(); // re-apply cosmetic hides after any reload/navigation within kiwix + } else { + pendingHidden.clear(); // left the reader; deleted books are gone for real by next visit + hideSheet(); + hideTrigger(); + } + } + + /** Host activity going away: drop the button + sheet so we don't leak it. */ + public void detach() { + hideSheet(); + hideTrigger(); + } + + /** True when the URL's path is under the box's kiwix reader. Pure string parsing (no + * android.net.Uri) so it is unit-testable and dependency-free. */ + static boolean isKiwixPage(String url) { + if (url == null) return false; + String u = url; + int hash = u.indexOf('#'); if (hash >= 0) u = u.substring(0, hash); + int q = u.indexOf('?'); if (q >= 0) u = u.substring(0, q); + int scheme = u.indexOf("://"); + if (scheme >= 0) { int slash = u.indexOf('/', scheme + 3); u = slash >= 0 ? u.substring(slash) : "/"; } + return u.equals("/kiwix") || u.startsWith("/kiwix/"); + } + + // ---- native trigger button --------------------------------------------------------------- + + private void showTrigger() { + if (trigger != null) return; + FloatingActionButton fab = new FloatingActionButton(themed); + fab.setImageResource(R.drawable.ic_content_library_24); // opens the manager (list + add + delete), not a delete-only action + fab.setContentDescription(activity.getString(R.string.k2go_zim_manage_action)); + fab.setOnClickListener(v -> openSheet()); + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); + lp.gravity = Gravity.BOTTOM | Gravity.END; + lp.rightMargin = dp(16); + lp.bottomMargin = dp(88); // clear the portal's bottom toolbar + trigger = fab; + activity.addContentView(fab, lp); + } + + private void hideTrigger() { + if (trigger == null) return; + ViewGroup parent = (ViewGroup) trigger.getParent(); + if (parent != null) parent.removeView(trigger); + trigger = null; + } + + // ---- overlay list ------------------------------------------------------------------------ + + private void openSheet() { + hideTrigger(); // avoid overlapping the sheet; restored when the sheet closes + if (deleteSheet == null) buildSheet(); + refresh(); + } + + private void buildSheet() { + LinearLayout sheet = new LinearLayout(themed); + sheet.setOrientation(LinearLayout.VERTICAL); + sheet.setBackground(rounded(cSurface, 16f, true)); // rounded top, flat bottom + sheet.setElevation(dp(12)); + sheet.setPadding(dp(16), dp(14), dp(16), dp(12)); + + LinearLayout header = new LinearLayout(themed); + header.setOrientation(LinearLayout.HORIZONTAL); + header.setGravity(Gravity.CENTER_VERTICAL); + TextView title = new TextView(themed); + title.setText(R.string.k2go_zim_manage_title); + title.setTextColor(cOnSurface); + title.setTypeface(title.getTypeface(), Typeface.BOLD); + title.setTextSize(16); + header.addView(title, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + ImageView close = new ImageView(themed); + close.setImageResource(R.drawable.ic_close_24); + close.setColorFilter(cOnSurfaceVariant); + int closePad = dp(6); + close.setPadding(closePad, closePad, closePad, closePad); + close.setContentDescription(activity.getString(android.R.string.cancel)); + close.setOnClickListener(v -> hideSheet()); + header.addView(close, new LinearLayout.LayoutParams(dp(36), dp(36))); + sheet.addView(header); + + searchField = new EditText(themed); + searchField.setHint(R.string.k2go_zim_search_hint); + searchField.setSingleLine(true); + searchField.setTextColor(cOnSurface); + searchField.setHintTextColor(cOnSurfaceVariant); + LinearLayout.LayoutParams sflp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + sflp.topMargin = dp(8); + sheet.addView(searchField, sflp); + searchField.addTextChangedListener(new android.text.TextWatcher() { + @Override public void beforeTextChanged(CharSequence s, int a, int b, int c) { } + @Override public void onTextChanged(CharSequence s, int a, int b, int c) { renderRows(s.toString()); } + @Override public void afterTextChanged(android.text.Editable e) { } + }); + + ScrollView sv = new ScrollView(themed); + listContainer = new LinearLayout(themed); + listContainer.setOrientation(LinearLayout.VERTICAL); + sv.addView(listContainer); + LinearLayout.LayoutParams svlp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, 0, 1f); + svlp.topMargin = dp(8); + sheet.addView(sv, svlp); + + // Pinned footer: jump to the Wikipedia & ZIM content screen (adding lives there, not here). + MaterialButton more = new MaterialButton(themed); + more.setText(R.string.k2go_zim_get_more); + more.setIconResource(R.drawable.ic_arrow_right); + more.setIconGravity(MaterialButton.ICON_GRAVITY_TEXT_END); + more.setOnClickListener(v -> { + hideSheet(); + activity.startActivity(new Intent(activity, SetupLibraryActivity.class) + .putExtra(SetupLibraryActivity.EXTRA_ZIM_SETUP, true)); + }); + LinearLayout.LayoutParams mlp = new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + mlp.topMargin = dp(8); + sheet.addView(more, mlp); + + int h = Math.round(activity.getResources().getDisplayMetrics().heightPixels * SHEET_FRACTION); + FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, h); + params.gravity = Gravity.BOTTOM; + deleteSheet = sheet; + activity.addContentView(deleteSheet, params); + } + + private void refresh() { + KiwixClient.library(new KiwixClient.ArrayCb() { + @Override public void onOk(JSONArray rows) { + if (activity.isFinishing() || deleteSheet == null) return; + items.clear(); + for (int i = 0; i < rows.length(); i++) { + JSONObject r = rows.optJSONObject(i); + if (r != null) items.add(r); + } + renderRows(searchField != null ? searchField.getText().toString() : ""); + } + @Override public void onErr(String message) { + if (activity.isFinishing() || listContainer == null) return; + listContainer.removeAllViews(); + listContainer.addView(note(activity.getString(R.string.k2go_zim_load_failed))); + } + }); + } + + private void renderRows(String filter) { + if (listContainer == null) return; + listContainer.removeAllViews(); + final String f = filter == null ? "" : filter.trim().toLowerCase(Locale.US); + int shown = 0; + for (JSONObject it : items) { + String name = it.optString("name", ""); + if (!f.isEmpty() && !name.toLowerCase(Locale.US).contains(f)) continue; + listContainer.addView(row(it)); + shown++; + } + if (shown == 0) { + listContainer.addView(note(activity.getString( + items.isEmpty() ? R.string.k2go_zim_empty : R.string.k2go_zim_no_matches))); + } + } + + private View row(JSONObject item) { + final String name = item.optString("name", ""); + long bytes = item.optLong("bytes", 0); + + LinearLayout row = new LinearLayout(themed); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + row.setPadding(dp(10), dp(12), dp(6), dp(12)); + + LinearLayout col = new LinearLayout(themed); + col.setOrientation(LinearLayout.VERTICAL); + TextView tt = new TextView(themed); + tt.setText(prettyName(name)); + tt.setTextColor(cOnSurface); + tt.setTextSize(14); + col.addView(tt); + TextView sz = new TextView(themed); + sz.setText(ByteFormatter.toHuman(bytes)); + sz.setTextColor(cOnSurfaceVariant); + sz.setTextSize(12); + col.addView(sz); + row.addView(col, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); + + ImageView del = new ImageView(themed); + del.setImageResource(R.drawable.ic_delete_24); + del.setColorFilter(cError); + int delPad = dp(8); + del.setPadding(delPad, delPad, delPad, delPad); + del.setContentDescription(activity.getString(R.string.k2go_zim_delete)); + del.setOnClickListener(v -> confirmDelete(name)); + row.addView(del, new LinearLayout.LayoutParams(dp(40), dp(40))); + return row; + } + + private void confirmDelete(String name) { + new MaterialAlertDialogBuilder(themed) + .setTitle(activity.getString(R.string.k2go_zim_delete_confirm_title, prettyName(name))) + .setMessage(R.string.k2go_zim_delete_confirm_msg) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(R.string.k2go_zim_delete, (d, w) -> { + Toast.makeText(activity, R.string.k2go_zim_deleting, Toast.LENGTH_SHORT).show(); + KiwixClient.delete(name, new KiwixClient.OkCb() { + @Override public void onOk(boolean deferred) { + if (activity.isFinishing()) return; + refresh(); // the overlay list reads disk, so the item is gone either way + if (deferred) { + // File removed, but a download is in progress so we skipped the reindex; + // kiwix-serve still shows it until that download's reindex. Don't reload + // (it would just re-render the ghost) — hide it cosmetically now and + // explain the wait; the real removal lands with that download's reindex. + pendingHidden.add(stemOf(name)); + applyHide(); + Snackbars.make(activity.findViewById(android.R.id.content), + R.string.k2go_zim_deleted_deferred).show(); + } else { + Toast.makeText(activity, activity.getString(R.string.k2go_zim_deleted, prettyName(name)), Toast.LENGTH_SHORT).show(); + webView.reload(); // kiwix-serve redraws its library without the ZIM + } + } + @Override public void onErr(String message) { + if (activity.isFinishing()) return; + new MaterialAlertDialogBuilder(themed) + .setMessage(R.string.k2go_zim_delete_failed) + .setPositiveButton(android.R.string.ok, null).show(); + } + }); + }) + .show(); + } + + private void hideSheet() { + if (deleteSheet != null) { + ViewGroup parent = (ViewGroup) deleteSheet.getParent(); + if (parent != null) parent.removeView(deleteSheet); + } + deleteSheet = null; searchField = null; listContainer = null; + if (active) showTrigger(); // bring the button back + } + + // ---- helpers ----------------------------------------------------------------------------- + + private TextView note(String text) { + TextView t = new TextView(themed); + t.setText(text); + t.setTextColor(cOnSurfaceVariant); + t.setPadding(dp(10), dp(14), dp(10), dp(14)); + return t; + } + + /** "wikipedia_en_all_maxi_2024-01.zim" -> "wikipedia en all maxi 2024-01" (display only). */ + private static String prettyName(String zim) { + if (zim == null) return ""; + String n = zim.endsWith(".zim") ? zim.substring(0, zim.length() - 4) : zim; + return n.replace('_', ' '); + } + + /** ZIM file-name without the ".zim" — kiwix-serve names a book by this stem, so we match its card + * link on it. */ + private static String stemOf(String zim) { + if (zim == null) return ""; + return zim.endsWith(".zim") ? zim.substring(0, zim.length() - 4) : zim; + } + + /** Cosmetically hide, in the kiwix-serve reader, the cards for ZIMs deleted while a download is in + * progress (their real removal waits for that download's reindex). Best-effort + fail-safe: it + * matches the book link by the ZIM's file-name stem; if kiwix's markup doesn't match, nothing is + * hidden (no harm) and the ghost simply lingers until the reindex. Re-applied on every kiwix load. */ + private void applyHide() { + if (pendingHidden.isEmpty()) return; + StringBuilder stems = new StringBuilder("["); + boolean first = true; + for (String s : pendingHidden) { + if (!first) stems.append(','); + stems.append(JSONObject.quote(s)); + first = false; + } + stems.append(']'); + String js = "(function(){try{var S=" + stems + ";S.forEach(function(st){" + + "document.querySelectorAll('a[href*=\"'+st+'\"]').forEach(function(a){" + + "var c=a.closest('li, .book, .book-tile, .book__item, article')||a;" + + "if(c)c.style.display='none';});});}catch(e){}})();"; + webView.evaluateJavascript(js, null); + } + + private GradientDrawable rounded(int color, float radiusDp, boolean topOnly) { + GradientDrawable g = new GradientDrawable(); + g.setColor(color); + float r = dp(radiusDp); + if (topOnly) g.setCornerRadii(new float[]{ r, r, r, r, 0, 0, 0, 0 }); + else g.setCornerRadius(r); + return g; + } + + private int dp(float v) { return Math.round(v * activity.getResources().getDisplayMetrics().density); } +} diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java index e235f00e..a86c7c1e 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/SetupLibraryActivity.java @@ -36,6 +36,8 @@ public class SetupLibraryActivity extends AppCompatActivity implements org.iiab. public static final String EXTRA_MODULE_DETAIL = "moduleDetail"; /** ADFA-4958: open the maps content selector directly from Home (maps is a module with a selector step). */ public static final String EXTRA_MAPS_SETUP = "mapsSetup"; + /** ADFA-5004: open the Wikipedia & ZIM content screen directly (from the reader's Get-more shortcut). */ + public static final String EXTRA_ZIM_SETUP = "zimSetup"; /** ADFA-4952: open Backup & restore directly (Settings → Advanced). */ public static final String EXTRA_BACKUP_RESTORE = "backupRestore"; /** ADFA-4957: open BackupJobFragment(mode) directly — used to deep-link back to a LIVE backup/restore @@ -82,6 +84,7 @@ protected void onCreate(Bundle savedInstanceState) { boolean moduleMgmt = getIntent().getBooleanExtra(EXTRA_MODULE_MGMT, false); final String moduleDetail = getIntent().getStringExtra(EXTRA_MODULE_DETAIL); boolean mapsSetup = getIntent().getBooleanExtra(EXTRA_MAPS_SETUP, false); + boolean zimSetup = getIntent().getBooleanExtra(EXTRA_ZIM_SETUP, false); boolean backupRestore = getIntent().getBooleanExtra(EXTRA_BACKUP_RESTORE, false); boolean contentOnly = getIntent().getBooleanExtra(EXTRA_CONTENT_ONLY, false); String brJobMode = getIntent().getStringExtra(EXTRA_BR_JOB_MODE); // ADFA-4957 @@ -96,6 +99,9 @@ protected void onCreate(Bundle savedInstanceState) { } else if (mapsSetup) { selectedTier = readInstalledTier(); // ADFA-4958: maps content selector, entered from Home first = new MapsChooseFragment(); + } else if (zimSetup) { + selectedTier = readInstalledTier(); // ADFA-5004: Wikipedia & ZIM content, from the reader + first = new ZimLandingFragment(); } else if (contentOnly) { selectedTier = readInstalledTier(); // size content against the installed tier first = new GetMoreHubFragment(); // ADFA-4848: Get More opens the content hub diff --git a/controller/app/src/main/res/drawable/ic_content_library_24.xml b/controller/app/src/main/res/drawable/ic_content_library_24.xml new file mode 100644 index 00000000..1ce1ec06 --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_content_library_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/controller/app/src/main/res/drawable/ic_delete_24.xml b/controller/app/src/main/res/drawable/ic_delete_24.xml new file mode 100644 index 00000000..6c6f704a --- /dev/null +++ b/controller/app/src/main/res/drawable/ic_delete_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/controller/app/src/main/res/values-ar/strings_k2go.xml b/controller/app/src/main/res/values-ar/strings_k2go.xml index f8ae16c0..f36c4890 100644 --- a/controller/app/src/main/res/values-ar/strings_k2go.xml +++ b/controller/app/src/main/res/values-ar/strings_k2go.xml @@ -657,4 +657,20 @@ استعادة لا يمكن إلغاء تثبيت الوحدات — يمكن إخفاؤها فقط، ثم استعادتها هنا. انقر للفتح · ضع علامة لتثبيت عدة وحدات دفعة واحدة. + + إدارة التنزيلات + الحصول على المزيد من المحتوى + المحتوى المُنزَّل + جارٍ تحميل المحتوى المُنزَّل… + لا يوجد محتوى مُنزَّل بعد. + لا توجد نتائج. + تعذّر تحميل المحتوى المُنزَّل. + %1$d عنصر · %2$s + حذف + حذف %1$s؟ + يؤدي هذا إلى إزالة الملف وتحديث مكتبة المحتوى. يمكنك تنزيله مرة أخرى لاحقًا. + جارٍ الحذف… + تم حذف %1$s + تم الحذف — مخفي الآن، وسيُزال بالكامل بعد انتهاء التنزيل الحالي. + تعذّر الحذف. يُرجى المحاولة مرة أخرى. diff --git a/controller/app/src/main/res/values-az/strings_k2go.xml b/controller/app/src/main/res/values-az/strings_k2go.xml index debaab2f..8010ebaf 100644 --- a/controller/app/src/main/res/values-az/strings_k2go.xml +++ b/controller/app/src/main/res/values-az/strings_k2go.xml @@ -657,4 +657,20 @@ Bərpa et Modullar silinə bilməz — yalnız gizlədilir, sonra burada bərpa olunur. Açmaq üçün toxun · bir neçəsini birdən quraşdırmaq üçün işarələ. + + Endirmələri idarə et + Daha çox məzmun əldə et + Endirilmiş məzmun + Endirilmiş məzmun yüklənir… + Hələ endirilmiş məzmun yoxdur. + Uyğunluq yoxdur. + Endirilmiş məzmun yüklənə bilmədi. + %1$d element · %2$s + Sil + %1$s silinsin? + Bu, faylı silir və məzmun kitabxanasını yeniləyir. Sonra yenidən endirə bilərsiniz. + Silinir… + %1$s silindi + Silindi — hələlik gizlədilib, cari endirmə bitdikdə tam silinəcək. + Silinmədi. Zəhmət olmasa yenidən cəhd edin. diff --git a/controller/app/src/main/res/values-bg/strings_k2go.xml b/controller/app/src/main/res/values-bg/strings_k2go.xml index 34807f07..b05893fe 100644 --- a/controller/app/src/main/res/values-bg/strings_k2go.xml +++ b/controller/app/src/main/res/values-bg/strings_k2go.xml @@ -657,4 +657,20 @@ Възстанови Модулите не могат да се деинсталират — само да се скрият, а после да се възстановят тук. Докоснете за отваряне · отметнете, за да инсталирате няколко наведнъж. + + Управление на изтеглянията + Вземи още съдържание + Изтеглено съдържание + Зареждане на изтегленото съдържание… + Все още няма изтеглено съдържание. + Няма съвпадения. + Изтегленото съдържание не можа да се зареди. + %1$d елемент(а) · %2$s + Изтрий + Да се изтрие ли %1$s? + Това премахва файла и актуализира библиотеката със съдържание. Можете да го изтеглите отново по-късно. + Изтриване… + %1$s е изтрит + Изтрито — засега скрито, напълно премахнато след завършване на текущото изтегляне. + Изтриването не бе успешно. Опитайте отново. diff --git a/controller/app/src/main/res/values-bn/strings_k2go.xml b/controller/app/src/main/res/values-bn/strings_k2go.xml index 93cb6635..1f4c05c2 100644 --- a/controller/app/src/main/res/values-bn/strings_k2go.xml +++ b/controller/app/src/main/res/values-bn/strings_k2go.xml @@ -657,4 +657,20 @@ পুনরুদ্ধার করুন মডিউল আনইনস্টল করা যায় না — শুধু লুকানো যায়, তারপর এখানে পুনরুদ্ধার করা যায়। খুলতে ট্যাপ করুন · একসাথে একাধিক ইনস্টল করতে টিক দিন। + + ডাউনলোড পরিচালনা করুন + আরও কনটেন্ট পান + ডাউনলোড করা কনটেন্ট + ডাউনলোড করা কনটেন্ট লোড হচ্ছে… + এখনও কোনো ডাউনলোড করা কনটেন্ট নেই। + কোনো মিল নেই। + ডাউনলোড করা কনটেন্ট লোড করা যায়নি। + %1$d টি আইটেম · %2$s + মুছুন + %1$s মুছবেন? + এটি ফাইলটি সরিয়ে দেয় এবং কনটেন্ট লাইব্রেরি আপডেট করে। আপনি পরে আবার এটি ডাউনলোড করতে পারবেন। + মুছে ফেলা হচ্ছে… + %1$s মুছে ফেলা হয়েছে + মুছে ফেলা হয়েছে — আপাতত লুকানো, বর্তমান ডাউনলোড শেষ হলে সম্পূর্ণ সরিয়ে ফেলা হবে। + মুছে ফেলা যায়নি। আবার চেষ্টা করুন। diff --git a/controller/app/src/main/res/values-cs/strings_k2go.xml b/controller/app/src/main/res/values-cs/strings_k2go.xml index e1aa902f..02726725 100644 --- a/controller/app/src/main/res/values-cs/strings_k2go.xml +++ b/controller/app/src/main/res/values-cs/strings_k2go.xml @@ -657,4 +657,20 @@ Obnovit Moduly nelze odinstalovat — pouze skrýt a zde znovu obnovit. Klepnutím otevřete · zaškrtnutím nainstalujete několik najednou. + + Spravovat stažené + Získat další obsah + Stažený obsah + Načítání staženého obsahu… + Zatím žádný stažený obsah. + Žádné shody. + Nepodařilo se načíst stažený obsah. + %1$d položek · %2$s + Odstranit + Odstranit %1$s? + Tímto se soubor odstraní a aktualizuje se knihovna obsahu. Později jej můžete stáhnout znovu. + Odstraňování… + %1$s odstraněno + Odstraněno — prozatím skryto, zcela odstraněno po dokončení aktuálního stahování. + Odstranění se nezdařilo. Zkuste to znovu. diff --git a/controller/app/src/main/res/values-de/strings_k2go.xml b/controller/app/src/main/res/values-de/strings_k2go.xml index 9be1bb6e..52d64666 100644 --- a/controller/app/src/main/res/values-de/strings_k2go.xml +++ b/controller/app/src/main/res/values-de/strings_k2go.xml @@ -658,4 +658,20 @@ Wiederherstellen Module können nicht deinstalliert werden — nur ausblenden, dann hier wiederherstellen. Zum Öffnen tippen · zum gleichzeitigen Installieren mehrerer ankreuzen. + + Downloads verwalten + Mehr Inhalte holen + Heruntergeladene Inhalte + Heruntergeladene Inhalte werden geladen… + Noch keine heruntergeladenen Inhalte. + Keine Treffer. + Heruntergeladene Inhalte konnten nicht geladen werden. + %1$d Element(e) · %2$s + Löschen + %1$s löschen? + Dadurch wird die Datei entfernt und die Inhaltsbibliothek aktualisiert. Du kannst sie später erneut herunterladen. + Wird gelöscht… + %1$s gelöscht + Gelöscht – vorerst ausgeblendet, vollständig entfernt, sobald der aktuelle Download abgeschlossen ist. + Löschen fehlgeschlagen. Bitte versuche es erneut. diff --git a/controller/app/src/main/res/values-el/strings_k2go.xml b/controller/app/src/main/res/values-el/strings_k2go.xml index fe3dc2fa..3a8eb3bc 100644 --- a/controller/app/src/main/res/values-el/strings_k2go.xml +++ b/controller/app/src/main/res/values-el/strings_k2go.xml @@ -658,4 +658,20 @@ Επαναφορά Οι μονάδες δεν μπορούν να απεγκατασταθούν — μόνο να κρυφτούν και μετά να επαναφερθούν εδώ. Πατήστε για άνοιγμα · επιλέξτε για εγκατάσταση πολλών ταυτόχρονα. + + Διαχείριση λήψεων + Λήψη περισσότερου περιεχομένου + Ληφθέν περιεχόμενο + Φόρτωση ληφθέντος περιεχομένου… + Δεν υπάρχει ακόμη ληφθέν περιεχόμενο. + Καμία αντιστοιχία. + Δεν ήταν δυνατή η φόρτωση του ληφθέντος περιεχομένου. + %1$d στοιχείο(-α) · %2$s + Διαγραφή + Διαγραφή %1$s; + Αυτό αφαιρεί το αρχείο και ενημερώνει τη βιβλιοθήκη περιεχομένου. Μπορείτε να το κατεβάσετε ξανά αργότερα. + Διαγραφή… + Διαγράφηκε %1$s + Διαγράφηκε — προς το παρόν κρυφό, θα αφαιρεθεί πλήρως μόλις ολοκληρωθεί η τρέχουσα λήψη. + Η διαγραφή απέτυχε. Δοκιμάστε ξανά. diff --git a/controller/app/src/main/res/values-es/strings_k2go.xml b/controller/app/src/main/res/values-es/strings_k2go.xml index 63aca613..32537127 100644 --- a/controller/app/src/main/res/values-es/strings_k2go.xml +++ b/controller/app/src/main/res/values-es/strings_k2go.xml @@ -657,4 +657,20 @@ Restaurar Los módulos no se pueden desinstalar — solo ocultar, y luego restaurar aquí. Toca para abrir · marca para instalar varios a la vez. + + Gestionar descargas + Obtener más contenido + Contenido descargado + Cargando contenido descargado… + Aún no hay contenido descargado. + Sin coincidencias. + No se pudo cargar el contenido descargado. + %1$d elemento(s) · %2$s + Eliminar + ¿Eliminar %1$s? + Esto elimina el archivo y actualiza la biblioteca de contenido. Podrás descargarlo de nuevo más tarde. + Eliminando… + %1$s eliminado + Eliminado — oculto por ahora, se quitará por completo cuando termine la descarga actual. + No se pudo eliminar. Inténtalo de nuevo. diff --git a/controller/app/src/main/res/values-fa/strings_k2go.xml b/controller/app/src/main/res/values-fa/strings_k2go.xml index 1897b4ee..f7222e9e 100644 --- a/controller/app/src/main/res/values-fa/strings_k2go.xml +++ b/controller/app/src/main/res/values-fa/strings_k2go.xml @@ -657,4 +657,20 @@ بازگرداندن ماژول‌ها قابل حذف نصب نیستند — فقط می‌توان آن‌ها را پنهان و سپس اینجا بازگرداند. برای باز کردن ضربه بزنید · برای نصب چند مورد به‌طور همزمان تیک بزنید. + + مدیریت دانلودها + دریافت محتوای بیشتر + محتوای دانلودشده + در حال بارگذاری محتوای دانلودشده… + هنوز محتوای دانلودشده‌ای وجود ندارد. + موردی یافت نشد. + بارگذاری محتوای دانلودشده ممکن نشد. + %1$d مورد · %2$s + حذف + %1$s حذف شود؟ + این کار فایل را حذف و کتابخانهٔ محتوا را به‌روزرسانی می‌کند. می‌توانید بعداً دوباره آن را دانلود کنید. + در حال حذف… + %1$s حذف شد + حذف شد — فعلاً پنهان شده، پس از پایان دانلود فعلی به‌طور کامل حذف می‌شود. + حذف ممکن نشد. لطفاً دوباره تلاش کنید. diff --git a/controller/app/src/main/res/values-fr/strings_k2go.xml b/controller/app/src/main/res/values-fr/strings_k2go.xml index 7fb58371..a9712d11 100644 --- a/controller/app/src/main/res/values-fr/strings_k2go.xml +++ b/controller/app/src/main/res/values-fr/strings_k2go.xml @@ -667,4 +667,20 @@ Restaurer Les modules ne peuvent pas être désinstallés — seulement masqués, puis restaurés ici. Appuyez pour ouvrir · cochez pour en installer plusieurs à la fois. + + Gérer les téléchargements + Obtenir plus de contenu + Contenu téléchargé + Chargement du contenu téléchargé… + Aucun contenu téléchargé pour l\'instant. + Aucun résultat. + Impossible de charger le contenu téléchargé. + %1$d élément(s) · %2$s + Supprimer + Supprimer %1$s ? + Cela supprime le fichier et met à jour la bibliothèque de contenu. Vous pourrez le télécharger à nouveau plus tard. + Suppression… + %1$s supprimé + Supprimé — masqué pour l\'instant, entièrement retiré une fois le téléchargement en cours terminé. + Échec de la suppression. Veuillez réessayer. diff --git a/controller/app/src/main/res/values-gu/strings_k2go.xml b/controller/app/src/main/res/values-gu/strings_k2go.xml index 62175250..19f35e8a 100644 --- a/controller/app/src/main/res/values-gu/strings_k2go.xml +++ b/controller/app/src/main/res/values-gu/strings_k2go.xml @@ -657,4 +657,20 @@ પુનઃસ્થાપિત કરો મૉડ્યુલ અનઇન્સ્ટોલ કરી શકાતા નથી — ફક્ત છુપાવી શકાય, પછી અહીં પુનઃસ્થાપિત કરી શકાય. ખોલવા ટૅપ કરો · એકસાથે અનેક ઇન્સ્ટોલ કરવા ટિક કરો. + + ડાઉનલોડ્સ મેનેજ કરો + વધુ સામગ્રી મેળવો + ડાઉનલોડ કરેલી સામગ્રી + ડાઉનલોડ કરેલી સામગ્રી લોડ થઈ રહી છે… + હજી સુધી કોઈ ડાઉનલોડ કરેલી સામગ્રી નથી. + કોઈ મેળ નથી. + ડાઉનલોડ કરેલી સામગ્રી લોડ કરી શકાઈ નથી. + %1$d આઇટમ · %2$s + કાઢી નાખો + %1$s કાઢી નાખવું? + આ ફાઇલ દૂર કરે છે અને સામગ્રી લાઇબ્રેરી અપડેટ કરે છે. તમે તેને પછીથી ફરીથી ડાઉનલોડ કરી શકો છો. + કાઢી રહ્યું છે… + %1$s કાઢી નાખ્યું + કાઢી નાખ્યું — હમણાં માટે છુપાવ્યું, વર્તમાન ડાઉનલોડ પૂર્ણ થતાં સંપૂર્ણપણે દૂર થશે. + કાઢી શકાયું નથી. કૃપા કરીને ફરી પ્રયાસ કરો. diff --git a/controller/app/src/main/res/values-hi/strings_k2go.xml b/controller/app/src/main/res/values-hi/strings_k2go.xml index 62c87868..5403883c 100644 --- a/controller/app/src/main/res/values-hi/strings_k2go.xml +++ b/controller/app/src/main/res/values-hi/strings_k2go.xml @@ -657,4 +657,20 @@ पुनर्स्थापित करें मॉड्यूल अनइंस्टॉल नहीं किए जा सकते — केवल छिपाए, फिर यहाँ पुनर्स्थापित किए जा सकते हैं। खोलने के लिए टैप करें · एक साथ कई इंस्टॉल करने के लिए टिक करें। + + डाउनलोड प्रबंधित करें + और सामग्री प्राप्त करें + डाउनलोड की गई सामग्री + डाउनलोड की गई सामग्री लोड हो रही है… + अभी तक कोई डाउनलोड की गई सामग्री नहीं। + कोई मिलान नहीं। + डाउनलोड की गई सामग्री लोड नहीं हो सकी। + %1$d आइटम · %2$s + हटाएँ + %1$s हटाएँ? + इससे फ़ाइल हट जाती है और सामग्री लाइब्रेरी अपडेट हो जाती है। आप इसे बाद में फिर से डाउनलोड कर सकते हैं। + हटाया जा रहा है… + %1$s हटाया गया + हटाया गया — फ़िलहाल छिपाया गया, मौजूदा डाउनलोड पूरा होने पर पूरी तरह हटा दिया जाएगा। + हटाया नहीं जा सका। कृपया पुनः प्रयास करें। diff --git a/controller/app/src/main/res/values-hu/strings_k2go.xml b/controller/app/src/main/res/values-hu/strings_k2go.xml index 17022c39..ab5de75a 100644 --- a/controller/app/src/main/res/values-hu/strings_k2go.xml +++ b/controller/app/src/main/res/values-hu/strings_k2go.xml @@ -657,4 +657,20 @@ Visszaállítás A modulok nem távolíthatók el — csak elrejthetők, majd itt visszaállíthatók. Koppints a megnyitáshoz · pipáld be, hogy egyszerre többet telepíts. + + Letöltések kezelése + További tartalom letöltése + Letöltött tartalom + Letöltött tartalom betöltése… + Még nincs letöltött tartalom. + Nincs találat. + A letöltött tartalmat nem sikerült betölteni. + %1$d elem · %2$s + Törlés + Törli a következőt: %1$s? + Ez eltávolítja a fájlt, és frissíti a tartalomtárat. Később újra letöltheted. + Törlés… + %1$s törölve + Törölve — egyelőre elrejtve, a jelenlegi letöltés befejeztével teljesen eltávolítva. + A törlés sikertelen. Próbáld újra. diff --git a/controller/app/src/main/res/values-in/strings_k2go.xml b/controller/app/src/main/res/values-in/strings_k2go.xml index e4114b2f..596c1ed0 100644 --- a/controller/app/src/main/res/values-in/strings_k2go.xml +++ b/controller/app/src/main/res/values-in/strings_k2go.xml @@ -657,4 +657,20 @@ Pulihkan Modul tidak bisa dihapus — hanya disembunyikan, lalu dipulihkan di sini. Ketuk untuk membuka · centang untuk memasang beberapa sekaligus. + + Kelola unduhan + Dapatkan konten lainnya + Konten yang diunduh + Memuat konten yang diunduh… + Belum ada konten yang diunduh. + Tidak ada kecocokan. + Tidak dapat memuat konten yang diunduh. + %1$d item · %2$s + Hapus + Hapus %1$s? + Ini menghapus file dan memperbarui pustaka konten. Anda dapat mengunduhnya lagi nanti. + Menghapus… + %1$s dihapus + Dihapus — disembunyikan untuk saat ini, dihapus sepenuhnya setelah unduhan saat ini selesai. + Tidak dapat menghapus. Silakan coba lagi. diff --git a/controller/app/src/main/res/values-it/strings_k2go.xml b/controller/app/src/main/res/values-it/strings_k2go.xml index 2e88b4f9..14088c3e 100644 --- a/controller/app/src/main/res/values-it/strings_k2go.xml +++ b/controller/app/src/main/res/values-it/strings_k2go.xml @@ -657,4 +657,20 @@ Ripristina I moduli non possono essere disinstallati — solo nascosti, poi ripristinati qui. Tocca per aprire · spunta per installarne diversi in una volta. + + Gestisci download + Ottieni altri contenuti + Contenuti scaricati + Caricamento dei contenuti scaricati… + Nessun contenuto scaricato. + Nessun risultato. + Impossibile caricare i contenuti scaricati. + %1$d elemento/i · %2$s + Elimina + Eliminare %1$s? + Questo rimuove il file e aggiorna la libreria dei contenuti. Potrai scaricarlo di nuovo in seguito. + Eliminazione… + %1$s eliminato + Eliminato — nascosto per ora, rimosso completamente al termine del download in corso. + Impossibile eliminare. Riprova. diff --git a/controller/app/src/main/res/values-ja/strings_k2go.xml b/controller/app/src/main/res/values-ja/strings_k2go.xml index 6f802788..837d8e1e 100644 --- a/controller/app/src/main/res/values-ja/strings_k2go.xml +++ b/controller/app/src/main/res/values-ja/strings_k2go.xml @@ -657,4 +657,20 @@ 復元 モジュールはアンインストールできません — 非表示にしてから、ここで復元するだけです。 タップして開く · チェックして複数を一度にインストール。 + + ダウンロードを管理 + コンテンツを追加取得 + ダウンロード済みコンテンツ + ダウンロード済みコンテンツを読み込み中… + ダウンロード済みのコンテンツはまだありません。 + 一致するものがありません。 + ダウンロード済みコンテンツを読み込めませんでした。 + %1$d 件 · %2$s + 削除 + %1$s を削除しますか? + ファイルを削除し、コンテンツライブラリを更新します。後で再度ダウンロードできます。 + 削除しています… + %1$s を削除しました + 削除しました — 現在は非表示です。進行中のダウンロードが完了すると完全に削除されます。 + 削除できませんでした。もう一度お試しください。 diff --git a/controller/app/src/main/res/values-ko/strings_k2go.xml b/controller/app/src/main/res/values-ko/strings_k2go.xml index 695d17e7..e18154a8 100644 --- a/controller/app/src/main/res/values-ko/strings_k2go.xml +++ b/controller/app/src/main/res/values-ko/strings_k2go.xml @@ -657,4 +657,20 @@ 복원 모듈은 제거할 수 없습니다 — 숨긴 후 여기서 복원할 수만 있습니다. 탭하여 열기 · 체크하여 여러 개를 한 번에 설치. + + 다운로드 관리 + 콘텐츠 더 가져오기 + 다운로드한 콘텐츠 + 다운로드한 콘텐츠 불러오는 중… + 아직 다운로드한 콘텐츠가 없습니다. + 일치하는 항목이 없습니다. + 다운로드한 콘텐츠를 불러오지 못했습니다. + %1$d개 항목 · %2$s + 삭제 + %1$s을(를) 삭제하시겠습니까? + 파일을 제거하고 콘텐츠 라이브러리를 업데이트합니다. 나중에 다시 다운로드할 수 있습니다. + 삭제 중… + %1$s 삭제됨 + 삭제됨 — 지금은 숨김 상태이며, 현재 다운로드가 완료되면 완전히 제거됩니다. + 삭제하지 못했습니다. 다시 시도해 주세요. diff --git a/controller/app/src/main/res/values-lt/strings_k2go.xml b/controller/app/src/main/res/values-lt/strings_k2go.xml index ee2fb388..a8534869 100644 --- a/controller/app/src/main/res/values-lt/strings_k2go.xml +++ b/controller/app/src/main/res/values-lt/strings_k2go.xml @@ -667,4 +667,20 @@ Atkurti Modulių pašalinti negalima — tik paslėpti, o paskui čia atkurti. Bakstelėkite, kad atidarytumėte · pažymėkite, kad įdiegtumėte kelis iš karto. + + Tvarkyti atsisiuntimus + Gauti daugiau turinio + Atsisiųstas turinys + Įkeliamas atsisiųstas turinys… + Kol kas nėra atsisiųsto turinio. + Nėra atitikmenų. + Nepavyko įkelti atsisiųsto turinio. + %1$d elem. · %2$s + Ištrinti + Ištrinti %1$s? + Tai pašalina failą ir atnaujina turinio biblioteką. Vėliau galėsite jį atsisiųsti dar kartą. + Trinama… + %1$s ištrinta + Ištrinta — kol kas paslėpta, visiškai pašalinta pasibaigus dabartiniam atsisiuntimui. + Nepavyko ištrinti. Bandykite dar kartą. diff --git a/controller/app/src/main/res/values-nl/strings_k2go.xml b/controller/app/src/main/res/values-nl/strings_k2go.xml index 9b5e33ab..ad67999e 100644 --- a/controller/app/src/main/res/values-nl/strings_k2go.xml +++ b/controller/app/src/main/res/values-nl/strings_k2go.xml @@ -657,4 +657,20 @@ Herstellen Modules kunnen niet worden verwijderd — alleen verborgen en hier hersteld. Tik om te openen · vink aan om er meerdere tegelijk te installeren. + + Downloads beheren + Meer inhoud ophalen + Gedownloade inhoud + Gedownloade inhoud laden… + Nog geen gedownloade inhoud. + Geen resultaten. + Kan gedownloade inhoud niet laden. + %1$d item(s) · %2$s + Verwijderen + %1$s verwijderen? + Hiermee wordt het bestand verwijderd en de inhoudsbibliotheek bijgewerkt. Je kunt het later opnieuw downloaden. + Verwijderen… + %1$s verwijderd + Verwijderd — voorlopig verborgen, volledig verwijderd zodra de huidige download klaar is. + Verwijderen mislukt. Probeer het opnieuw. diff --git a/controller/app/src/main/res/values-no/strings_k2go.xml b/controller/app/src/main/res/values-no/strings_k2go.xml index 0b2b376a..e598cd60 100644 --- a/controller/app/src/main/res/values-no/strings_k2go.xml +++ b/controller/app/src/main/res/values-no/strings_k2go.xml @@ -667,4 +667,20 @@ Gjenopprett Moduler kan ikke avinstalleres — bare skjules, og gjenopprettes her. Trykk for å åpne · huk av for å installere flere på én gang. + + Administrer nedlastinger + Hent mer innhold + Nedlastet innhold + Laster nedlastet innhold… + Ingen nedlastet innhold ennå. + Ingen treff. + Kunne ikke laste nedlastet innhold. + %1$d element(er) · %2$s + Slett + Slette %1$s? + Dette fjerner filen og oppdaterer innholdsbiblioteket. Du kan laste den ned igjen senere. + Sletter… + %1$s slettet + Slettet — skjult foreløpig, fjernes helt når den pågående nedlastingen er ferdig. + Kunne ikke slette. Prøv igjen. diff --git a/controller/app/src/main/res/values-pl/strings_k2go.xml b/controller/app/src/main/res/values-pl/strings_k2go.xml index b81bbe24..c554c8aa 100644 --- a/controller/app/src/main/res/values-pl/strings_k2go.xml +++ b/controller/app/src/main/res/values-pl/strings_k2go.xml @@ -667,4 +667,20 @@ Przywróć Modułów nie można odinstalować — tylko ukryć, a potem tu przywrócić. Dotknij, aby otworzyć · zaznacz, aby zainstalować kilka naraz. + + Zarządzaj pobranymi + Pobierz więcej treści + Pobrana zawartość + Ładowanie pobranej zawartości… + Brak pobranej zawartości. + Brak wyników. + Nie udało się załadować pobranej zawartości. + %1$d element(y) · %2$s + Usuń + Usunąć %1$s? + Spowoduje to usunięcie pliku i aktualizację biblioteki treści. Możesz pobrać go ponownie później. + Usuwanie… + Usunięto %1$s + Usunięto — na razie ukryte, zostanie całkowicie usunięte po zakończeniu bieżącego pobierania. + Nie udało się usunąć. Spróbuj ponownie. diff --git a/controller/app/src/main/res/values-pt/strings_k2go.xml b/controller/app/src/main/res/values-pt/strings_k2go.xml index 7fecb95c..bedc2594 100644 --- a/controller/app/src/main/res/values-pt/strings_k2go.xml +++ b/controller/app/src/main/res/values-pt/strings_k2go.xml @@ -657,4 +657,20 @@ Restaurar Os módulos não podem ser desinstalados — apenas ocultados e depois restaurados aqui. Toque para abrir · marque para instalar vários de uma vez. + + Gerenciar downloads + Obter mais conteúdo + Conteúdo baixado + Carregando conteúdo baixado… + Ainda não há conteúdo baixado. + Nenhuma correspondência. + Não foi possível carregar o conteúdo baixado. + %1$d item(ns) · %2$s + Excluir + Excluir %1$s? + Isso remove o arquivo e atualiza a biblioteca de conteúdo. Você poderá baixá-lo novamente mais tarde. + Excluindo… + %1$s excluído + Excluído — oculto por ora, removido totalmente quando o download atual terminar. + Não foi possível excluir. Tente novamente. diff --git a/controller/app/src/main/res/values-ro/strings_k2go.xml b/controller/app/src/main/res/values-ro/strings_k2go.xml index ec05211a..5661397d 100644 --- a/controller/app/src/main/res/values-ro/strings_k2go.xml +++ b/controller/app/src/main/res/values-ro/strings_k2go.xml @@ -657,4 +657,20 @@ Restaurează Modulele nu pot fi dezinstalate — doar ascunse, apoi restaurate aici. Atinge pentru a deschide · bifează pentru a instala mai multe deodată. + + Gestionează descărcările + Obține mai mult conținut + Conținut descărcat + Se încarcă conținutul descărcat… + Încă nu există conținut descărcat. + Nicio potrivire. + Conținutul descărcat nu a putut fi încărcat. + %1$d element(e) · %2$s + Șterge + Ștergi %1$s? + Aceasta elimină fișierul și actualizează biblioteca de conținut. Îl poți descărca din nou mai târziu. + Se șterge… + %1$s șters + Șters — ascuns momentan, eliminat complet după ce se termină descărcarea curentă. + Ștergerea a eșuat. Încearcă din nou. diff --git a/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml b/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml index 71e39208..176bd726 100644 --- a/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml +++ b/controller/app/src/main/res/values-ru-rRU/strings_k2go.xml @@ -657,4 +657,20 @@ Восстановить Модули нельзя удалить — только скрыть, а затем восстановить здесь. Нажмите, чтобы открыть · отметьте, чтобы установить несколько сразу. + + Управление загрузками + Получить больше контента + Загруженный контент + Загрузка скачанного контента… + Пока нет загруженного контента. + Нет совпадений. + Не удалось загрузить скачанный контент. + %1$d элем. · %2$s + Удалить + Удалить %1$s? + Файл будет удалён, а библиотека контента обновлена. Вы сможете скачать его снова позже. + Удаление… + %1$s удалён + Удалено — пока скрыто, полностью удалится после завершения текущей загрузки. + Не удалось удалить. Повторите попытку. diff --git a/controller/app/src/main/res/values-sk/strings_k2go.xml b/controller/app/src/main/res/values-sk/strings_k2go.xml index 2d21ae77..7979297a 100644 --- a/controller/app/src/main/res/values-sk/strings_k2go.xml +++ b/controller/app/src/main/res/values-sk/strings_k2go.xml @@ -657,4 +657,20 @@ Obnoviť Moduly nemožno odinštalovať — iba skryť a potom obnoviť tu. Klepnutím otvoríte · zaškrtnutím nainštalujete viacero naraz. + + Spravovať stiahnuté + Získať ďalší obsah + Stiahnutý obsah + Načítava sa stiahnutý obsah… + Zatiaľ žiadny stiahnutý obsah. + Žiadne zhody. + Nepodarilo sa načítať stiahnutý obsah. + %1$d položiek · %2$s + Odstrániť + Odstrániť %1$s? + Týmto sa súbor odstráni a aktualizuje sa knižnica obsahu. Neskôr ho môžete znova stiahnuť. + Odstraňuje sa… + %1$s odstránené + Odstránené — zatiaľ skryté, úplne odstránené po dokončení aktuálneho sťahovania. + Odstránenie zlyhalo. Skúste to znova. diff --git a/controller/app/src/main/res/values-sr/strings_k2go.xml b/controller/app/src/main/res/values-sr/strings_k2go.xml index d171b863..bcec2f6b 100644 --- a/controller/app/src/main/res/values-sr/strings_k2go.xml +++ b/controller/app/src/main/res/values-sr/strings_k2go.xml @@ -657,4 +657,20 @@ Врати Модули се не могу деинсталирати — само сакрити, па овде вратити. Додирните да отворите · означите да инсталирате више одједном. + + Управљање преузимањима + Преузми још садржаја + Преузети садржај + Учитавање преузетог садржаја… + Још нема преузетог садржаја. + Нема подударања. + Није могуће учитати преузети садржај. + %1$d ставки · %2$s + Обриши + Обрисати %1$s? + Ово уклања датотеку и ажурира библиотеку садржаја. Можете је поново преузети касније. + Брисање… + %1$s обрисано + Обрисано — за сада сакривено, потпуно уклоњено када се заврши тренутно преузимање. + Брисање није успело. Покушајте поново. diff --git a/controller/app/src/main/res/values-sw/strings_k2go.xml b/controller/app/src/main/res/values-sw/strings_k2go.xml index 4b8745cb..c28905b5 100644 --- a/controller/app/src/main/res/values-sw/strings_k2go.xml +++ b/controller/app/src/main/res/values-sw/strings_k2go.xml @@ -657,4 +657,20 @@ Rejesha Moduli haziwezi kuondolewa — hufichwa tu, kisha kurejeshwa hapa. Gusa ili kufungua · tiki ili kusakinisha kadhaa kwa mara moja. + + Dhibiti vipakuliwa + Pata maudhui zaidi + Maudhui yaliyopakuliwa + Inapakia maudhui yaliyopakuliwa… + Bado hakuna maudhui yaliyopakuliwa. + Hakuna zinazolingana. + Imeshindwa kupakia maudhui yaliyopakuliwa. + Vipengele %1$d · %2$s + Futa + Ufute %1$s? + Hii huondoa faili na kusasisha maktaba ya maudhui. Unaweza kuipakua tena baadaye. + Inafuta… + %1$s imefutwa + Imefutwa — imefichwa kwa sasa, itaondolewa kabisa upakuaji wa sasa utakapokamilika. + Imeshindwa kufuta. Tafadhali jaribu tena. diff --git a/controller/app/src/main/res/values-ta/strings_k2go.xml b/controller/app/src/main/res/values-ta/strings_k2go.xml index 55507e1d..ce5f5c11 100644 --- a/controller/app/src/main/res/values-ta/strings_k2go.xml +++ b/controller/app/src/main/res/values-ta/strings_k2go.xml @@ -657,4 +657,20 @@ மீட்டமை தொகுதிகளை நிறுவல் நீக்க முடியாது — மறைக்க மட்டுமே முடியும், பிறகு இங்கே மீட்டமைக்கலாம். திறக்க தட்டவும் · பலவற்றை ஒரே நேரத்தில் நிறுவ டிக் செய்யவும். + + பதிவிறக்கங்களை நிர்வகி + மேலும் உள்ளடக்கம் பெறு + பதிவிறக்கிய உள்ளடக்கம் + பதிவிறக்கிய உள்ளடக்கம் ஏற்றப்படுகிறது… + இதுவரை பதிவிறக்கிய உள்ளடக்கம் இல்லை. + பொருத்தம் இல்லை. + பதிவிறக்கிய உள்ளடக்கத்தை ஏற்ற முடியவில்லை. + %1$d உருப்படி · %2$s + நீக்கு + %1$s ஐ நீக்கவா? + இது கோப்பை அகற்றி உள்ளடக்க நூலகத்தைப் புதுப்பிக்கும். பிறகு மீண்டும் பதிவிறக்கலாம். + நீக்குகிறது… + %1$s நீக்கப்பட்டது + நீக்கப்பட்டது — தற்போது மறைக்கப்பட்டது, தற்போதைய பதிவிறக்கம் முடிந்ததும் முழுமையாக அகற்றப்படும். + நீக்க முடியவில்லை. மீண்டும் முயற்சிக்கவும். diff --git a/controller/app/src/main/res/values-tr/strings_k2go.xml b/controller/app/src/main/res/values-tr/strings_k2go.xml index c58dccbf..6ac8a675 100644 --- a/controller/app/src/main/res/values-tr/strings_k2go.xml +++ b/controller/app/src/main/res/values-tr/strings_k2go.xml @@ -657,4 +657,20 @@ Geri yükle Modüller kaldırılamaz — yalnızca gizlenir, sonra buradan geri yüklenir. Açmak için dokunun · birkaçını birden kurmak için işaretleyin. + + İndirmeleri yönet + Daha fazla içerik al + İndirilen içerik + İndirilen içerik yükleniyor… + Henüz indirilen içerik yok. + Eşleşme yok. + İndirilen içerik yüklenemedi. + %1$d öğe · %2$s + Sil + %1$s silinsin mi? + Bu, dosyayı kaldırır ve içerik kitaplığını günceller. Daha sonra tekrar indirebilirsiniz. + Siliniyor… + %1$s silindi + Silindi — şimdilik gizlendi, geçerli indirme tamamlanınca tamamen kaldırılacak. + Silinemedi. Lütfen tekrar deneyin. diff --git a/controller/app/src/main/res/values-uk/strings_k2go.xml b/controller/app/src/main/res/values-uk/strings_k2go.xml index 37fef93d..c93f89f0 100644 --- a/controller/app/src/main/res/values-uk/strings_k2go.xml +++ b/controller/app/src/main/res/values-uk/strings_k2go.xml @@ -657,4 +657,20 @@ Відновити Модулі не можна видалити — лише приховати, потім відновити тут. Торкніться, щоб відкрити · позначте, щоб встановити кілька одразу. + + Керувати завантаженнями + Отримати більше вмісту + Завантажений вміст + Завантаження скачаного вмісту… + Ще немає завантаженого вмісту. + Немає збігів. + Не вдалося завантажити скачаний вміст. + %1$d елем. · %2$s + Видалити + Видалити %1$s? + Файл буде видалено, а бібліотеку вмісту оновлено. Ви зможете завантажити його знову пізніше. + Видалення… + %1$s видалено + Видалено — поки що приховано, повністю видалиться після завершення поточного завантаження. + Не вдалося видалити. Спробуйте ще раз. diff --git a/controller/app/src/main/res/values-vi/strings_k2go.xml b/controller/app/src/main/res/values-vi/strings_k2go.xml index dd1adfb3..dc8f8ee2 100644 --- a/controller/app/src/main/res/values-vi/strings_k2go.xml +++ b/controller/app/src/main/res/values-vi/strings_k2go.xml @@ -657,4 +657,20 @@ Khôi phục Không thể gỡ cài đặt mô-đun — chỉ có thể ẩn, rồi khôi phục ở đây. Chạm để mở · đánh dấu để cài nhiều cùng lúc. + + Quản lý tải xuống + Nhận thêm nội dung + Nội dung đã tải + Đang tải nội dung đã tải xuống… + Chưa có nội dung nào được tải. + Không có kết quả. + Không thể tải nội dung đã tải xuống. + %1$d mục · %2$s + Xóa + Xóa %1$s? + Thao tác này sẽ xóa tệp và cập nhật thư viện nội dung. Bạn có thể tải lại sau. + Đang xóa… + Đã xóa %1$s + Đã xóa — tạm ẩn, sẽ bị xóa hoàn toàn khi tải xuống hiện tại hoàn tất. + Không thể xóa. Vui lòng thử lại. diff --git a/controller/app/src/main/res/values-yo/strings_k2go.xml b/controller/app/src/main/res/values-yo/strings_k2go.xml index 1c473997..bcae5bcc 100644 --- a/controller/app/src/main/res/values-yo/strings_k2go.xml +++ b/controller/app/src/main/res/values-yo/strings_k2go.xml @@ -657,4 +657,20 @@ Mú padà A kò lè yọ àwọn mọ́díù kúrò — a lè fi pamọ́ nìkan, lẹ́yìn náà mú padà níbí. Tẹ láti ṣí · fàmì láti fi ọ̀pọ̀lọpọ̀ sórí ẹ̀rọ lẹ́ẹ̀kan. + + Ṣàkóso àwọn ìgbàsílẹ̀ + Rí àkóónú síi + Àkóónú tí a gbà sílẹ̀ + Ń gbé àkóónú tí a gbà sílẹ̀ wọlé… + Kò sí àkóónú tí a gbà sílẹ̀ síbẹ̀. + Kò sí ìbámu. + Kò lè gbé àkóónú tí a gbà sílẹ̀ wọlé. + %1$d nǹkan · %2$s + Parẹ́ + Paarẹ́ %1$s? + Èyí máa yọ fáìlì náà kúrò yóò sì ṣàtúnṣe àká àkóónú. O lè tún gbà á sílẹ̀ nígbà mìíràn. + Ń paarẹ́… + A ti paarẹ́ %1$s + A ti paarẹ́ — a fi pamọ́ fún ìsinsìnyí, yóò yọ kúrò pátápátá nígbà tí ìgbàsílẹ̀ lọ́wọ́lọ́wọ́ bá parí. + Kò lè paarẹ́. Jọ̀wọ́ gbìyànjú lẹ́ẹ̀kansi. diff --git a/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml b/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml index 10b8daee..32607582 100644 --- a/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml +++ b/controller/app/src/main/res/values-zh-rCN/strings_k2go.xml @@ -657,4 +657,20 @@ 恢复 模块无法卸载 — 只能隐藏,然后在此处恢复。 点按打开 · 勾选以一次安装多个。 + + 管理下载 + 获取更多内容 + 已下载内容 + 正在加载已下载内容… + 尚无已下载内容。 + 无匹配项。 + 无法加载已下载内容。 + %1$d 项 · %2$s + 删除 + 删除 %1$s? + 这将移除文件并更新内容库。稍后可重新下载。 + 正在删除… + 已删除 %1$s + 已删除 —— 暂时隐藏,当前下载完成后将彻底移除。 + 无法删除。请重试。 diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 12a8c670..3f0bb85a 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -710,4 +710,26 @@ Restore Modules can\'t be uninstalled — only hidden, then restored here. Tap to open · tick to install several at once. + + + Manage downloads + Get more content + Downloaded content + Loading downloaded content… + No downloaded content yet. + No matches. + Couldn\'t load downloaded content. + + %1$d item(s) · %2$s + Delete + + Delete %1$s? + This removes the file and updates the content library. You can download it again later. + Deleting… + + Deleted %1$s + + Deleted — hidden for now, fully removed once the current download finishes. + Couldn\'t delete. Please try again. diff --git a/controller/app/src/test/java/org/iiab/controller/redesign/KiwixManageControllerTest.java b/controller/app/src/test/java/org/iiab/controller/redesign/KiwixManageControllerTest.java new file mode 100644 index 00000000..38dd70cd --- /dev/null +++ b/controller/app/src/test/java/org/iiab/controller/redesign/KiwixManageControllerTest.java @@ -0,0 +1,27 @@ +package org.iiab.controller.redesign; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import org.junit.Test; + +/** + * Pure-JVM unit tests for the static URL matcher in {@link KiwixManageController} (ADFA-5004). + * isKiwixPage is plain string parsing (no Android deps) — it gates the in-WebView ZIM manager to the + * box's /kiwix/ pages, mirroring FqrController.isMapsPage. + */ +public class KiwixManageControllerTest { + + @Test + public void isKiwixPage_matchesOnlyTheKiwixPaths() { + assertTrue(KiwixManageController.isKiwixPage("http://localhost:8085/kiwix/")); + assertTrue(KiwixManageController.isKiwixPage("http://localhost:8085/kiwix")); + assertTrue(KiwixManageController.isKiwixPage("http://localhost:8085/kiwix/viewer#wikipedia")); + assertTrue(KiwixManageController.isKiwixPage("http://localhost:8085/kiwix/?lang=en")); + assertTrue(KiwixManageController.isKiwixPage("http://localhost:8085/kiwix/content/foo")); + assertFalse(KiwixManageController.isKiwixPage("http://localhost:8085/maps/")); + assertFalse(KiwixManageController.isKiwixPage("http://localhost:8085/home/")); + assertFalse(KiwixManageController.isKiwixPage("http://localhost:8085/kiwixx/")); // not a prefix boundary + assertFalse(KiwixManageController.isKiwixPage(null)); + } +} diff --git a/static/dashboard/routes.ts b/static/dashboard/routes.ts index 9177a8ac..ccad623f 100644 --- a/static/dashboard/routes.ts +++ b/static/dashboard/routes.ts @@ -6,6 +6,8 @@ // none of these calls hold state — a client can drop and re-attach by polling the id. import express, { Router, Request, Response } from 'express'; import { spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; import { jobs, Job, JobType } from './sockets/jobs'; import { searchCatalog, listLibrary, removeBook, listLanguages } from './sockets/books.query'; import { parseBox, parseEstimate } from './sockets/maps.socket'; @@ -25,6 +27,14 @@ import { const MAPS_SCRIPT = '/opt/iiab/maps/tile-extract/tile-extract.py'; const MAPS_NAME_RE = /^[A-Za-z0-9_-]{1,34}$/; +// ADFA-5004: Kiwix ZIM management (list + delete), reached from the app so users can free space +// without the retired web dashboard. ZIMS_DIR/KIWIX_INDEXER mirror sockets/kiwix.exec.ts — keep in +// sync. ZIM_NAME_RE only ever matches a plain ".zim" (no path separators), so a delete can +// never escape ZIMS_DIR. +const ZIMS_DIR = '/library/zims/content/'; +const KIWIX_INDEXER = '/usr/bin/iiab-make-kiwix-lib'; +const ZIM_NAME_RE = /^[A-Za-z0-9._-]{1,150}\.zim$/; + const VALID_TYPES: JobType[] = ['kiwix', 'maps', 'books', 'kolibri']; function isType(t: string): t is JobType { return (VALID_TYPES as string[]).includes(t); @@ -161,6 +171,69 @@ apiRouter.post('/maps/delete', (req: Request, res: Response): void => { }); }); +// --- Kiwix: list installed ZIMs + delete (ADFA-5004) ------------------------------------------ +// Direct (non-job) ops mirroring /maps/delete; the download itself stays a durable job +// (POST /kiwix/download). Ported from the retired dashboard socket handler (delete_zim): remove the +// .zim from disk, then rebuild the Kiwix library index so the deleted book stops being served. +// Declared before the generic /:type/* routes so these literal paths win. + +// The ZIMs currently on disk (name + bytes), newest first. Drives the in-app manage list. +apiRouter.get('/kiwix/library', (_req: Request, res: Response): void => { + try { + if (!fs.existsSync(ZIMS_DIR)) { res.json([]); return; } + const rows = fs.readdirSync(ZIMS_DIR) + .filter((f) => f.endsWith('.zim')) + .map((name) => { + let bytes = 0, mtime = 0; + try { const st = fs.statSync(path.join(ZIMS_DIR, name)); bytes = st.size; mtime = st.mtimeMs; } + catch { /* file vanished between readdir and stat — skip */ } + return { name, bytes, mtime }; + }) + .sort((a, b) => b.mtime - a.mtime); + res.json(rows); + } catch (e: any) { + res.status(500).json({ error: e?.message || 'library read failed' }); + } +}); + +// Delete one installed ZIM, then rebuild the Kiwix index so kiwix-serve stops serving it. +apiRouter.post('/kiwix/delete', (req: Request, res: Response): void => { + const name = String((req.body as { name?: unknown })?.name ?? '').trim(); + // Strict name + basename guard: only a plain ".zim" inside ZIMS_DIR, never a path. + if (!ZIM_NAME_RE.test(name) || name !== path.basename(name)) { + res.status(400).json({ error: 'invalid zim name' }); return; + } + const filePath = path.join(ZIMS_DIR, name); + if (!fs.existsSync(filePath)) { res.status(404).json({ error: 'not found' }); return; } + try { + fs.unlinkSync(filePath); + } catch (e: any) { + res.status(500).json({ error: e?.message || 'delete failed' }); return; + } + // No indexer on the box: the file is gone, report success without a reindex. + if (!fs.existsSync(KIWIX_INDEXER)) { res.json({ ok: true, reindexed: false }); return; } + // ADFA-5004: don't run a second indexer while a kiwix download/index job is in flight. The jobs + // engine does NOT serialize (each runner is its own promise) and this delete runs outside it, so + // spawning iiab-make-kiwix-lib now could race that job's own end-of-run reindex on library.xml + // (and pick up an incomplete, still-downloading .zim). The file is already unlinked, so the + // active job's reindex — which reads the current disk state — will reflect the deletion when it + // finishes. See ADR-4832 (proot/index collisions). + const kiwixBusy = jobs.list('kiwix').some((j) => + j.phase === 'queued' || j.phase === 'downloading' || j.phase === 'indexing' || j.phase === 'processing'); + if (kiwixBusy) { res.json({ ok: true, reindexed: false, deferred: true }); return; } + // Rebuild the library index — the SAME step the download runner (kiwix.exec.ts) uses to make + // content show up, so it also makes a deleted ZIM disappear (no kiwix-serve restart needed). + // We MUST drain stdout/stderr: with the default piped stdio, a chatty indexer fills the ~64KB + // pipe buffer and blocks forever (never exits, library never rebuilt) — exactly what left a + // deleted ZIM still served. Its exit code is advisory (kiwix.exec.ts treats it the same), so the + // unlink is the real signal; we respond when it finishes. + const idx = spawn(KIWIX_INDEXER, [], { env: { ...process.env } }); + idx.stdout?.on('data', () => { /* drain so the indexer never blocks on a full pipe */ }); + idx.stderr?.on('data', () => { /* drain */ }); + idx.on('error', (e) => { if (!res.headersSent) res.status(500).json({ error: String(e) }); }); + idx.on('exit', () => { if (!res.headersSent) res.json({ ok: true, reindexed: true }); }); +}); + // --- Kolibri: readiness, catálogo y selección (ADFA-4949) ------------------------- // Consultas directas (no-job). La descarga en sí es un job durable // (POST /kolibri/download), que sale gratis al añadir 'kolibri' a VALID_TYPES.