Skip to content

Commit 15148fe

Browse files
Merge pull request #345 from appdevforall/feat/ADFA-5033-zim-catalog-browse-breathe
ADFA-5033: ZIM catalog browse — make it breathe (flat rows, top-N + grouped See-all, one-control language selector)
2 parents 211310e + 1c6e8c7 commit 15148fe

40 files changed

Lines changed: 1177 additions & 135 deletions
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* ============================================================================
3+
* Name : KiwixGroups.java
4+
* Author : AppDevForAll
5+
* Copyright : Copyright (c) 2026 AppDevForAll
6+
* Description : ADFA-5033. Static theme grouping for the ZIM catalog category index. Maps each of the
7+
* ~23 categories (KiwixCategories) to one of 5 groups, used by the browse screen's filter
8+
* chips (short label) and the "See all" section headers (longer label). Pure client-side
9+
* table shipped with the app; the same shape is meant to back other content types (e.g.
10+
* Kolibri) later. A category not listed falls into "media" (the catch-all) so the grouped
11+
* view is always complete. DRAFT map — under team review (ADFA-5033).
12+
*
13+
* Template conventions (respect when copying this pattern to Kolibri/Books):
14+
* controller/docs/CATALOG_BROWSE_TEMPLATE.md
15+
* ============================================================================
16+
*/
17+
package org.iiab.controller.redesign;
18+
19+
import org.iiab.controller.R;
20+
21+
import java.util.HashMap;
22+
import java.util.Map;
23+
24+
public final class KiwixGroups {
25+
private KiwixGroups() {}
26+
27+
/** A theme group: a short chip label and a longer section header, both string resources. */
28+
public static final class Group {
29+
public final String key;
30+
public final int chipLabel; // short — used on the filter chip
31+
public final int headerLabel; // longer — used as the "See all" section header
32+
Group(String key, int chipLabel, int headerLabel) {
33+
this.key = key; this.chipLabel = chipLabel; this.headerLabel = headerLabel;
34+
}
35+
}
36+
37+
/** Display + filter order (chips and See-all sections follow this). */
38+
public static final Group[] ALL = {
39+
new Group("reference", R.string.k2go_zim_grp_reference, R.string.k2go_zim_grp_reference_hdr),
40+
new Group("learning", R.string.k2go_zim_grp_learning, R.string.k2go_zim_grp_learning_hdr),
41+
new Group("tools", R.string.k2go_zim_grp_tools, R.string.k2go_zim_grp_tools_hdr),
42+
new Group("media", R.string.k2go_zim_grp_media, R.string.k2go_zim_grp_media_hdr),
43+
new Group("kids", R.string.k2go_zim_grp_kids, R.string.k2go_zim_grp_kids_hdr),
44+
};
45+
46+
private static final String CATCH_ALL = "media";
47+
48+
// category key -> group key. DRAFT (ADFA-5033); balance/placement pending team review.
49+
private static final Map<String, String> MAP = new HashMap<>();
50+
static {
51+
put("reference", "wikipedia", "wiktionary", "wikiquote", "wikisource", "wikinews", "psiram");
52+
put("learning", "freecodecamp", "phet", "wikiversity", "wikibooks", "libretexts", "mooc");
53+
put("tools", "devdocs", "stack_exchange", "ifixit");
54+
put("media", "ted", "videos", "gutenberg", "zimit", "wikivoyage", "other", "maps");
55+
put("kids", "vikidia");
56+
}
57+
58+
private static void put(String group, String... catKeys) {
59+
for (String k : catKeys) MAP.put(k, group);
60+
}
61+
62+
/** Group key for a category (catch-all "media" if unmapped, so grouping is always complete). */
63+
public static String groupOf(String categoryKey) {
64+
String g = MAP.get(categoryKey);
65+
return g != null ? g : CATCH_ALL;
66+
}
67+
68+
/** Look up a group by key (null if unknown). */
69+
public static Group byKey(String groupKey) {
70+
for (Group g : ALL) if (g.key.equals(groupKey)) return g;
71+
return null;
72+
}
73+
}

‎controller/app/src/main/java/org/iiab/controller/redesign/ZimCategoryFragment.java‎

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ private static final class Entry {
7272
private long freeMb = 0, totalMb = 0;
7373

7474
private LinearLayout list;
75-
private TextView freeLabel, sortSize, sortName, sortGroup, langChip, langCurrent, langSub;
75+
private TextView freeLabel, sortSize, sortName, sortGroup, count, langCurrent, langSub;
7676
private ProgressBar bar;
7777
private Button add;
7878

@@ -101,10 +101,10 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c
101101
sortSize = root.findViewById(R.id.k2go_zc_sort_size);
102102
sortName = root.findViewById(R.id.k2go_zc_sort_name);
103103
sortGroup = root.findViewById(R.id.k2go_zc_sort_group);
104-
langChip = root.findViewById(R.id.k2go_zc_lang); // informative label only (not a button)
104+
count = root.findViewById(R.id.k2go_zc_count); // ADFA-5033: item count, list metadata
105105
langCurrent = root.findViewById(R.id.k2go_zc_lang_current);
106106
langSub = root.findViewById(R.id.k2go_zc_lang_sub);
107-
root.findViewById(R.id.k2go_zc_change).setOnClickListener(v -> pickLanguage());
107+
root.findViewById(R.id.k2go_zc_lang_box).setOnClickListener(v -> pickLanguage()); // ADFA-5033: whole-row control
108108

109109
android.widget.EditText search = root.findViewById(R.id.k2go_zc_search);
110110
search.setHint(getString(R.string.k2go_zc_search_hint, cat != null ? cat.title : project));
@@ -181,11 +181,14 @@ private String label(String creator, String flavour) {
181181
}
182182

183183
private void render() {
184-
langChip.setText(getString(R.string.k2go_zc_lang_fmt, langDisplay(lang), entries.size()));
184+
int shownCount = 0;
185+
for (Entry e : entries) if (passes(e)) shownCount++;
186+
// ADFA-5033: "N items" normally, live "N results" while searching — no language (that's the selector).
187+
count.setText(getString(query.isEmpty() ? R.string.k2go_zc_count_items : R.string.k2go_zc_count_results, shownCount));
185188
langCurrent.setText(getString(R.string.k2go_zim_lang_fmt, langDisplay(lang)));
186189
boolean manual = (getActivity() instanceof SetupLibraryActivity)
187190
&& ((SetupLibraryActivity) getActivity()).isZimLangManual();
188-
langSub.setText(manual ? R.string.k2go_zim_lang_sub_manual : R.string.k2go_zim_lang_sub);
191+
langSub.setText(manual ? R.string.k2go_zim_lang_state_manual : R.string.k2go_zim_lang_state_system);
189192
sortSize.setText(getString(R.string.k2go_zc_sort_size) + (sizeDir < 0 ? " ▼" : " ▲"));
190193
sortName.setText((nameDir > 0 ? getString(R.string.k2go_zc_sort_name) : getString(R.string.k2go_zc_sort_name_desc))
191194
+ (nameDir > 0 ? " ▼" : " ▲"));
@@ -279,57 +282,91 @@ private void chip(TextView t, boolean on) {
279282
t.setTextColor(ContextCompat.getColor(requireContext(), on ? android.R.color.white : R.color.k2go_ink));
280283
}
281284

285+
// ADFA-5033: flat list row (spec §10) — a simple line with a hairline between rows, NOT a card.
286+
// The only rounded/filled shape is the SELECTED row's highlight; unselected rows are plain.
282287
private View row(Entry e, String labelText, int indent) {
283288
boolean fits = freeMb <= 0 || (e.bytes / (1024L * 1024L)) <= freeMb;
289+
boolean selected = e.checked && fits;
284290

285-
LinearLayout r = new LinearLayout(requireContext());
286-
r.setOrientation(LinearLayout.VERTICAL);
287-
LinearLayout.LayoutParams rlp = new LinearLayout.LayoutParams(
291+
LinearLayout wrap = new LinearLayout(requireContext());
292+
wrap.setOrientation(LinearLayout.VERTICAL);
293+
LinearLayout.LayoutParams wlp = new LinearLayout.LayoutParams(
288294
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
289-
rlp.bottomMargin = px(8); rlp.leftMargin = indent;
290-
r.setLayoutParams(rlp);
291-
r.setBackgroundResource(R.drawable.k2go_card_bg);
292-
r.setPadding(px(12), px(10), px(12), px(10));
295+
wlp.leftMargin = indent;
296+
wrap.setLayoutParams(wlp);
297+
298+
LinearLayout content = new LinearLayout(requireContext());
299+
content.setOrientation(LinearLayout.VERTICAL);
300+
content.setPadding(px(12), px(10), px(12), px(10));
301+
if (selected) content.setBackground(selectedHighlight()); // the one rounded/filled shape
293302

294303
LinearLayout top = new LinearLayout(requireContext());
295304
top.setOrientation(LinearLayout.HORIZONTAL);
296305
top.setGravity(Gravity.CENTER_VERTICAL);
306+
top.setMinimumHeight(px(36));
297307

298308
CheckBox cb = new CheckBox(requireContext());
299-
cb.setChecked(e.checked && fits);
309+
cb.setChecked(selected);
300310
cb.setEnabled(fits);
301311
cb.setClickable(false);
302312
cb.setFocusable(false);
303313
top.addView(cb);
304314

305315
TextView name = new TextView(requireContext());
306-
name.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
316+
name.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyLarge);
307317
name.setText(labelText);
308318
name.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_ink));
319+
if (selected) name.setTypeface(name.getTypeface(), android.graphics.Typeface.BOLD);
309320
LinearLayout.LayoutParams nlp = new LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f);
310-
nlp.leftMargin = px(6);
321+
nlp.leftMargin = px(8);
311322
top.addView(name, nlp);
312323

313324
TextView size = new TextView(requireContext());
314-
size.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall);
325+
size.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodyMedium);
315326
size.setText(gb(e.bytes / (1024L * 1024L)));
316327
size.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_muted));
328+
size.setGravity(Gravity.END);
317329
top.addView(size);
318-
r.addView(top);
330+
content.addView(top);
319331

320332
if (!fits) {
321333
TextView warn = new TextView(requireContext());
322334
warn.setTextAppearance(com.google.android.material.R.style.TextAppearance_Material3_BodySmall);
323335
warn.setText(getString(R.string.k2go_zc_nospace, gb(e.bytes / (1024L * 1024L)), gb(freeMb)));
324336
warn.setTextColor(ContextCompat.getColor(requireContext(), R.color.k2go_amber_text));
325-
LinearLayout.LayoutParams wlp = new LinearLayout.LayoutParams(
337+
LinearLayout.LayoutParams warnLp = new LinearLayout.LayoutParams(
326338
LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
327-
wlp.topMargin = px(2); wlp.leftMargin = px(34);
328-
r.addView(warn, wlp);
339+
warnLp.topMargin = px(2); warnLp.leftMargin = px(36);
340+
content.addView(warn, warnLp);
329341
} else {
330-
r.setOnClickListener(v -> { e.checked = !e.checked; cb.setChecked(e.checked); updateTotals(); });
342+
content.setOnClickListener(v -> {
343+
e.checked = !e.checked;
344+
cb.setChecked(e.checked); // fits is true in this branch
345+
content.setBackground(e.checked ? selectedHighlight() : null);
346+
name.setTypeface(null, e.checked ? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL);
347+
updateTotals();
348+
});
331349
}
332-
return r;
350+
351+
wrap.addView(content);
352+
wrap.addView(hairline());
353+
return wrap;
354+
}
355+
356+
/** Rounded, subtly-filled highlight for the selected row (the only non-flat shape in the list). */
357+
private android.graphics.drawable.GradientDrawable selectedHighlight() {
358+
android.graphics.drawable.GradientDrawable g = new android.graphics.drawable.GradientDrawable();
359+
g.setCornerRadius(px(10));
360+
g.setColor(androidx.core.graphics.ColorUtils.setAlphaComponent(
361+
ContextCompat.getColor(requireContext(), R.color.k2go_teal), 0x33));
362+
return g;
363+
}
364+
365+
private View hairline() {
366+
View v = new View(requireContext());
367+
v.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.k2go_hairline));
368+
v.setLayoutParams(new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, px(1)));
369+
return v;
333370
}
334371

335372
private long checkedMb() {

0 commit comments

Comments
 (0)