diff --git a/controller/app/src/main/java/org/iiab/controller/redesign/FqrController.java b/controller/app/src/main/java/org/iiab/controller/redesign/FqrController.java index 805816e13..8b1b43784 100644 --- a/controller/app/src/main/java/org/iiab/controller/redesign/FqrController.java +++ b/controller/app/src/main/java/org/iiab/controller/redesign/FqrController.java @@ -14,29 +14,46 @@ * 3) run the durable REST download (MapsRegionClient) with a floating progress * overlay that can be minimized; on success we reload so the region draws. * No sudo/shell on the device — the box's localhost REST does the work. + * + * ADFA-4884: the overlays/dialogs (consent sheet, "calculating" dialog, floating + * progress overlay, delete list) are themed with Material 3 + system day/night by + * building them through a ContextThemeWrapper(Theme.K2Go) — a Material3.DayNight theme + * carrying the app palette — without touching PortalActivity's global theme. Colors come + * from theme attributes (colorSurface/onSurface/primary/error/…) so they follow light/dark; + * dialogs use MaterialAlertDialogBuilder, buttons MaterialButton, and the progress bars + * the Material progress indicators. All user-facing text lives in string resources. * ============================================================================ */ package org.iiab.controller.redesign; import android.app.Activity; +import android.content.Context; 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.JavascriptInterface; import android.webkit.WebView; -import android.widget.Button; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.LinearLayout; -import android.widget.ProgressBar; import android.widget.ScrollView; import android.widget.TextView; import android.widget.Toast; import androidx.appcompat.app.AlertDialog; +import androidx.core.graphics.ColorUtils; +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.progressindicator.CircularProgressIndicator; +import com.google.android.material.progressindicator.LinearProgressIndicator; + +import org.iiab.controller.R; import org.json.JSONArray; import org.json.JSONObject; @@ -53,11 +70,17 @@ public final class FqrController { private final WebView webView; private final MapsRegionClient client = new MapsRegionClient(); + // Material 3 + day/night for our own overlays/dialogs, without changing PortalActivity's global + // theme: build every view/dialog through this wrapper so ?attr colors + the app font resolve. + private final Context themed; + private final int cSurface, cSurfaceContainer, cSurfaceHighest, cOnSurface, cOnSurfaceVariant, cPrimary, cError; + private volatile boolean active = false; // written on UI thread, read on the WebView binder thread private AlertDialog dialog; // "calculating" / consent (one at a time) private View overlay; // floating progress card (null when hidden) - private ProgressBar overlayBar; - private TextView overlayPct, overlayTitle, overlayMin; + private LinearProgressIndicator overlayBar; + private TextView overlayPct, overlayTitle; + private TextView overlayMin; private boolean overlayMinimized = false; // Delete: unified list bottom-sheet (~55%) fed by the manual trash tool. @@ -86,6 +109,20 @@ private static final class Region { public FqrController(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.cSurfaceContainer= attr(com.google.android.material.R.attr.colorSurfaceContainerHigh, 0xFF16201B); + this.cSurfaceHighest = attr(com.google.android.material.R.attr.colorSurfaceContainerHighest, 0xFF223029); + this.cOnSurface = attr(com.google.android.material.R.attr.colorOnSurface, Color.WHITE); + this.cOnSurfaceVariant= attr(com.google.android.material.R.attr.colorOnSurfaceVariant, 0xFF8FA39B); + // colorPrimary/colorError are declared by appcompat (not the Material R.attr), unlike the + // Material3-specific surface roles above; read them from the appcompat namespace. + this.cPrimary = attr(androidx.appcompat.R.attr.colorPrimary, 0xFF4CAF7D); + this.cError = attr(androidx.appcompat.R.attr.colorError, 0xFFE05353); + } + + private int attr(int attrId, int fallback) { + return MaterialColors.getColor(themed, attrId, fallback); } /** Add the JS bridge only for the /maps/ page and remove it elsewhere, to keep the interface off @@ -157,7 +194,7 @@ public void onDeleteRequested(String name) { private void handleExtract(String name, String box) { if (!validName(name) || !validBox(box)) { - toast("That region name or area looks invalid."); + toast(str(R.string.k2go_fqr_invalid)); return; } // "Calculating size…" while the server runs its dry-run. @@ -169,10 +206,10 @@ private void handleExtract(String name, String box) { } @Override public void onError(String message) { dismissDialog(); - new AlertDialog.Builder(activity) - .setTitle("Can't download this region") + new MaterialAlertDialogBuilder(themed) + .setTitle(R.string.k2go_fqr_estimate_error_title) .setMessage(message) - .setPositiveButton("OK", null) + .setPositiveButton(android.R.string.ok, null) .show(); } }); @@ -181,61 +218,67 @@ private void handleExtract(String name, String box) { // ---- Consent ----------------------------------------------------------------------------- private void showCalculating() { dismissDialog(); - LinearLayout box = card(dp(20)); - ProgressBar spin = new ProgressBar(activity); - box.addView(spin, new LinearLayout.LayoutParams(dp(28), dp(28))); - TextView t = new TextView(activity); - t.setText("Calculating download size…"); + LinearLayout row = dialogContent(dp(20)); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(Gravity.CENTER_VERTICAL); + CircularProgressIndicator spin = new CircularProgressIndicator(themed); + spin.setIndeterminate(true); + spin.setIndicatorSize(dp(28)); + row.addView(spin, new LinearLayout.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT)); + TextView t = new TextView(themed); + t.setText(R.string.k2go_fqr_calculating); t.setPadding(dp(14), 0, 0, 0); - t.setTextColor(Color.WHITE); - box.setOrientation(LinearLayout.HORIZONTAL); - box.setGravity(Gravity.CENTER_VERTICAL); - box.addView(t); - dialog = new AlertDialog.Builder(activity).setView(box).setCancelable(true).show(); + t.setTextColor(cOnSurface); + row.addView(t); + dialog = new MaterialAlertDialogBuilder(themed).setView(row).setCancelable(true).show(); } private void showConsent(String name, String box, long transfer, long archive, long free, long freeAfter) { - LinearLayout root = card(dp(20)); - - TextView title = new TextView(activity); - title.setText("Download this region?"); - title.setTextColor(Color.WHITE); - title.setTextSize(18); - title.setTypeface(title.getTypeface(), Typeface.BOLD); - root.addView(title); + LinearLayout body = dialogContent(dp(4)); - TextView sub = new TextView(activity); - sub.setText(String.format(Locale.US, "“%s” — %s to download, %s on disk.", - name, human(transfer), human(archive))); - sub.setTextColor(0xFFB9C4BF); - sub.setPadding(0, dp(6), 0, dp(14)); - root.addView(sub); + TextView sub = new TextView(themed); + sub.setText(str(R.string.k2go_fqr_consent_sub, name, human(transfer), human(archive))); + sub.setTextColor(cOnSurface); + sub.setPadding(0, 0, 0, dp(14)); + body.addView(sub); // Free-space bar: how much of the current free space this region takes. - LinearLayout bar = new LinearLayout(activity); + LinearLayout bar = new LinearLayout(themed); bar.setOrientation(LinearLayout.HORIZONTAL); long denom = Math.max(free, archive + freeAfter); if (denom <= 0) denom = 1; - View used = new View(activity); - used.setBackgroundColor(0xFF4CAF7D); // this region (leaf) - View freeSeg = new View(activity); - freeSeg.setBackgroundColor(0xFF223029); // free after + View used = new View(themed); + used.setBackgroundColor(cPrimary); // this region (leaf) + View freeSeg = new View(themed); + freeSeg.setBackgroundColor(cSurfaceHighest); // free after int h = dp(12); bar.addView(used, new LinearLayout.LayoutParams(0, h, Math.max(1f, archive))); bar.addView(freeSeg, new LinearLayout.LayoutParams(0, h, Math.max(1f, Math.max(0, denom - archive)))); - root.addView(bar); + body.addView(bar); - TextView legend = new TextView(activity); - legend.setText(String.format(Locale.US, "This region: %s · Free after: %s", human(archive), human(freeAfter))); - legend.setTextColor(0xFF8FA39B); + TextView legend = new TextView(themed); + legend.setText(str(R.string.k2go_fqr_consent_legend, human(archive), human(Math.max(0, freeAfter)))); + legend.setTextColor(cOnSurfaceVariant); legend.setTextSize(11); legend.setPadding(0, dp(8), 0, 0); - root.addView(legend); + body.addView(legend); + + // ADFA-4884: warn when the region wouldn't fit (negative free-after = disk almost full). + if (freeAfter < 0) { + TextView warn = new TextView(themed); + warn.setText(str(R.string.k2go_fqr_consent_wont_fit, human(-freeAfter))); + warn.setTextColor(cError); + warn.setTextSize(12); + warn.setPadding(0, dp(10), 0, 0); + body.addView(warn); + } - dialog = new AlertDialog.Builder(activity) - .setView(root) - .setNegativeButton("Not now", (d, w) -> d.dismiss()) - .setPositiveButton("Download", (d, w) -> startDownload(name)) + dialog = new MaterialAlertDialogBuilder(themed) + .setTitle(R.string.k2go_fqr_consent_title) + .setView(body) + .setNegativeButton(R.string.k2go_fqr_not_now, (d, w) -> d.dismiss()) + .setPositiveButton(R.string.k2go_fqr_download, (d, w) -> startDownload(name)) .setCancelable(true) .show(); @@ -256,15 +299,15 @@ private void startDownload(String name) { @Override public void onProgress(int percent, long speed) { updateOverlay(percent, speed); } @Override public void onDone() { updateOverlay(100, 0); - if (overlayTitle != null) overlayTitle.setText("Region added"); // may be gone if hidden + if (overlayTitle != null) overlayTitle.setText(R.string.k2go_fqr_region_added); // may be gone if hidden webView.postDelayed(() -> { hideOverlay(); webView.reload(); }, 1200); } @Override public void onError(String message) { hideOverlay(); if (!"canceled".equals(message)) { - new AlertDialog.Builder(activity) - .setTitle("Download failed").setMessage(message) - .setPositiveButton("OK", null).show(); + new MaterialAlertDialogBuilder(themed) + .setTitle(R.string.k2go_fqr_download_failed).setMessage(message) + .setPositiveButton(android.R.string.ok, null).show(); } } }); @@ -273,48 +316,49 @@ private void startDownload(String name) { // ---- Floating progress overlay ----------------------------------------------------------- private void showOverlay(String name, long sizeBytes) { hideOverlay(); - LinearLayout cardV = card(dp(16)); + LinearLayout cardV = surfaceCard(dp(16), 16f); cardV.setOrientation(LinearLayout.VERTICAL); - LinearLayout top = new LinearLayout(activity); + LinearLayout top = new LinearLayout(themed); top.setOrientation(LinearLayout.HORIZONTAL); top.setGravity(Gravity.CENTER_VERTICAL); - overlayTitle = new TextView(activity); - final String size = sizeBytes > 0 ? " · " + human(sizeBytes) : ""; // "Downloading “x” · 2.1 GB" - overlayTitle.setText(String.format(Locale.US, "Downloading “%s”%s", name, size)); - overlayTitle.setTextColor(Color.WHITE); + overlayTitle = new TextView(themed); + final String title = str(R.string.k2go_fqr_downloading, name) + + (sizeBytes > 0 ? " · " + human(sizeBytes) : ""); // "Downloading “x” · 2.1 GB" + overlayTitle.setText(title); + overlayTitle.setTextColor(cOnSurface); overlayTitle.setTypeface(overlayTitle.getTypeface(), Typeface.BOLD); top.addView(overlayTitle, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); - overlayMin = new TextView(activity); - overlayMin.setText("Hide"); // minimize to a compact card; tap again to Show - overlayMin.setTextColor(0xFF8FA39B); + overlayMin = new TextView(themed); + overlayMin.setText(R.string.k2go_hide); // minimize to a compact card; tap again to Show + overlayMin.setTextColor(cPrimary); overlayMin.setTextSize(13); overlayMin.setPadding(dp(12), 0, dp(4), 0); overlayMin.setOnClickListener(v -> toggleMinimize()); top.addView(overlayMin); cardV.addView(top); - overlayBar = new ProgressBar(activity, null, android.R.attr.progressBarStyleHorizontal); - overlayBar.setMax(100); + overlayBar = new LinearProgressIndicator(themed); overlayBar.setIndeterminate(true); + overlayBar.setTrackCornerRadius(dp(2)); LinearLayout.LayoutParams blp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - blp.topMargin = dp(10); + blp.topMargin = dp(12); cardV.addView(overlayBar, blp); - LinearLayout row = new LinearLayout(activity); + LinearLayout row = new LinearLayout(themed); row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); LinearLayout.LayoutParams rlp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); rlp.topMargin = dp(8); - overlayPct = new TextView(activity); - overlayPct.setText("Starting…"); - overlayPct.setTextColor(0xFF8FA39B); + overlayPct = new TextView(themed); + overlayPct.setText(R.string.k2go_fqr_starting); + overlayPct.setTextColor(cOnSurfaceVariant); row.addView(overlayPct, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); - Button cancel = new Button(activity); - cancel.setText("Cancel"); - cancel.setAllCaps(false); + MaterialButton cancel = new MaterialButton(themed, null, + com.google.android.material.R.attr.materialButtonOutlinedStyle); + cancel.setText(R.string.k2go_cancel); cancel.setOnClickListener(v -> { client.cancel(); hideOverlay(); }); row.addView(cancel); cardV.addView(row, rlp); @@ -336,22 +380,32 @@ private void toggleMinimize() { // so progress stays visible even when collapsed (the point of minimizing is minimal chrome). View controls = cardV.getChildAt(cardV.getChildCount() - 1); controls.setVisibility(overlayMinimized ? View.GONE : View.VISIBLE); - if (overlayMin != null) overlayMin.setText(overlayMinimized ? "Show" : "Hide"); + if (overlayMin != null) overlayMin.setText(overlayMinimized ? R.string.k2go_fqr_show : R.string.k2go_hide); } private void updateOverlay(int percent, long speed) { if (overlayBar == null || overlayPct == null) return; final String rate = speed > 0 ? " · " + humanRate(speed) : ""; if (percent < 0) { - overlayBar.setIndeterminate(true); - overlayPct.setText("Working…" + rate); + if (!overlayBar.isIndeterminate()) setBarMode(true); + overlayPct.setText(str(R.string.k2go_fqr_working) + rate); } else { - overlayBar.setIndeterminate(false); - overlayBar.setProgress(percent); - overlayPct.setText(percent + "%" + rate); + if (overlayBar.isIndeterminate()) setBarMode(false); + overlayBar.setProgressCompat(percent, true); + overlayPct.setText(str(R.string.k2go_fqr_percent, percent) + rate); } } + /** Switch the Material progress indicator between indeterminate/determinate. The indicator forbids + * the switch while visible, so toggle visibility around it. */ + private void setBarMode(boolean indeterminate) { + if (overlayBar == null) return; + int vis = overlayBar.getVisibility(); + overlayBar.setVisibility(View.GONE); + overlayBar.setIndeterminate(indeterminate); + overlayBar.setVisibility(vis == View.GONE ? View.VISIBLE : vis); + } + /** bytes/sec -> "2.4 MB/s". Empty for non-positive (speed not reported yet). */ private static String humanRate(long bps) { if (bps <= 0) return ""; @@ -377,35 +431,35 @@ private void openDeleteList(String hl) { } private void buildDeleteSheet() { - LinearLayout sheet = new LinearLayout(activity); + LinearLayout sheet = new LinearLayout(themed); sheet.setOrientation(LinearLayout.VERTICAL); - sheet.setBackgroundColor(0xFF0F1512); + sheet.setBackground(rounded(cSurface, 16f, true)); // rounded top corners, flat bottom sheet.setElevation(dp(12)); sheet.setPadding(dp(16), dp(14), dp(16), dp(12)); - LinearLayout header = new LinearLayout(activity); + LinearLayout header = new LinearLayout(themed); header.setOrientation(LinearLayout.HORIZONTAL); header.setGravity(Gravity.CENTER_VERTICAL); - TextView title = new TextView(activity); - title.setText("Downloaded regions"); - title.setTextColor(Color.WHITE); + TextView title = new TextView(themed); + title.setText(R.string.k2go_fqr_delete_list_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)); - TextView close = new TextView(activity); + TextView close = new TextView(themed); close.setText("✕"); - close.setTextColor(0xFF8FA39B); + close.setTextColor(cOnSurfaceVariant); close.setTextSize(18); close.setPadding(dp(12), 0, dp(4), 0); close.setOnClickListener(v -> hideDeleteSheet()); header.addView(close); sheet.addView(header); - searchField = new EditText(activity); - searchField.setHint("Search by name"); + searchField = new EditText(themed); + searchField.setHint(R.string.k2go_fqr_search_hint); searchField.setSingleLine(true); - searchField.setTextColor(Color.WHITE); - searchField.setHintTextColor(0xFF6E7F78); + searchField.setTextColor(cOnSurface); + searchField.setHintTextColor(cOnSurfaceVariant); LinearLayout.LayoutParams sflp = new LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); sflp.topMargin = dp(8); @@ -416,8 +470,8 @@ private void buildDeleteSheet() { @Override public void afterTextChanged(android.text.Editable e) { } }); - ScrollView sv = new ScrollView(activity); - listContainer = new LinearLayout(activity); + ScrollView sv = new ScrollView(themed); + listContainer = new LinearLayout(themed); listContainer.setOrientation(LinearLayout.VERTICAL); sv.addView(listContainer); LinearLayout.LayoutParams svlp = new LinearLayout.LayoutParams( @@ -472,29 +526,29 @@ private void renderRows(String filter) { shown++; } if (shown == 0) { - TextView t = new TextView(activity); - t.setText(regions.isEmpty() ? "No downloaded regions yet." : "No matches."); - t.setTextColor(0xFF8FA39B); + TextView t = new TextView(themed); + t.setText(regions.isEmpty() ? R.string.k2go_fqr_empty : R.string.k2go_fqr_no_matches); + t.setTextColor(cOnSurfaceVariant); t.setPadding(0, dp(14), 0, 0); listContainer.addView(t); } } private View regionRow(Region r) { - LinearLayout row = new LinearLayout(activity); + LinearLayout row = new LinearLayout(themed); row.setOrientation(LinearLayout.HORIZONTAL); row.setGravity(Gravity.CENTER_VERTICAL); row.setPadding(dp(10), dp(12), dp(10), dp(12)); - if (r.name.equals(highlight)) row.setBackgroundColor(0x334CAF7D); - TextView name = new TextView(activity); + if (r.name.equals(highlight)) row.setBackgroundColor(ColorUtils.setAlphaComponent(cPrimary, 0x33)); + TextView name = new TextView(themed); name.setText(r.name); - name.setTextColor(Color.WHITE); + name.setTextColor(cOnSurface); name.setTextSize(14); row.addView(name, new LinearLayout.LayoutParams(0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f)); row.setOnClickListener(v -> flyTo(r)); // tapping a row flies the map behind to that region - TextView x = new TextView(activity); + TextView x = new TextView(themed); x.setText("✕"); - x.setTextColor(0xFFE05353); + x.setTextColor(cError); x.setTextSize(16); x.setPadding(dp(16), dp(2), dp(6), dp(2)); x.setOnClickListener(v -> confirmDelete(r.name)); @@ -510,20 +564,20 @@ private void flyTo(Region r) { } private void confirmDelete(String name) { - new AlertDialog.Builder(activity) - .setTitle("Delete “" + name + "”?") - .setMessage("This full-quality region will be removed from this device.") - .setNegativeButton("Cancel", null) - .setPositiveButton("Delete", (d, w) -> client.deleteRegion(name, new MapsRegionClient.DeleteListener() { + new MaterialAlertDialogBuilder(themed) + .setTitle(str(R.string.k2go_fqr_delete_confirm_title, name)) + .setMessage(R.string.k2go_fqr_delete_confirm_msg) + .setNegativeButton(R.string.k2go_cancel, null) + .setPositiveButton(R.string.k2go_fqr_delete, (d, w) -> client.deleteRegion(name, new MapsRegionClient.DeleteListener() { @Override public void onOk() { - toast("Deleted “" + name + "”"); + toast(str(R.string.k2go_fqr_deleted, name)); highlight = null; webView.reload(); // map redraws without the region refreshRegions(); // and the list drops it } @Override public void onError(String m) { - new AlertDialog.Builder(activity).setTitle("Delete failed").setMessage(m) - .setPositiveButton("OK", null).show(); + new MaterialAlertDialogBuilder(themed).setTitle(R.string.k2go_fqr_delete_failed).setMessage(m) + .setPositiveButton(android.R.string.ok, null).show(); } })) .show(); @@ -537,21 +591,44 @@ private void hideDeleteSheet() { } // ---- helpers ----------------------------------------------------------------------------- - private LinearLayout card(int pad) { - LinearLayout l = new LinearLayout(activity); + /** Padded, transparent container for MaterialAlertDialog setView (the dialog paints the surface). */ + private LinearLayout dialogContent(int pad) { + LinearLayout l = new LinearLayout(themed); + l.setOrientation(LinearLayout.VERTICAL); + l.setPadding(pad, pad, pad, pad); + return l; + } + + /** Opaque, rounded Material surface for the floating overlay / bottom sheet (added via + * addContentView, so it carries its own surface). */ + private LinearLayout surfaceCard(int pad, float radius) { + LinearLayout l = new LinearLayout(themed); l.setOrientation(LinearLayout.VERTICAL); l.setPadding(pad, pad, pad, pad); - l.setBackgroundColor(0xFF16201B); + l.setBackground(rounded(cSurfaceContainer, radius, false)); l.setElevation(dp(8)); return l; } + 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 void dismissDialog() { if (dialog != null) { try { dialog.dismiss(); } catch (Exception ignore) { } dialog = null; } } private void toast(String m) { Toast.makeText(activity, m, Toast.LENGTH_SHORT).show(); } + private String str(int resId, Object... args) { + return args.length == 0 ? activity.getString(resId) : activity.getString(resId, args); + } + private int dp(float v) { return Math.round(v * activity.getResources().getDisplayMetrics().density); } private static String human(long bytes) { diff --git a/controller/app/src/main/res/values/strings_k2go.xml b/controller/app/src/main/res/values/strings_k2go.xml index 527f4a92f..c3d13c53a 100644 --- a/controller/app/src/main/res/values/strings_k2go.xml +++ b/controller/app/src/main/res/values/strings_k2go.xml @@ -501,4 +501,32 @@ Couldn\'t add — tap Retry Book downloads Adding books to your library + + + That region name or area looks invalid. + Calculating download size… + Can\'t download this region + Download this region? + “%1$s” — %2$s to download, %3$s on disk. + This region: %1$s · Free after: %2$s + This region may not fit — only %1$s would be free afterward. + Download + Not now + Downloading “%1$s” + Show + Starting… + Working… + %1$d%% + Region added + Download failed + Downloaded regions + Search by name + No downloaded regions yet. + No matches. + Delete “%1$s”? + This full-quality region will be removed from this device. + Delete + Deleted “%1$s” + Delete failed