Commit bc2dc6fb authored by Lückemeyer's avatar Lückemeyer
Browse files

Gemini enhanced stats fragment

parent 9b3438fb
...@@ -24,12 +24,32 @@ public class StatsFragment extends Fragment { ...@@ -24,12 +24,32 @@ public class StatsFragment extends Fragment {
/** true = all-time volet table (toggle right), false = by-day (toggle left) */ /** true = all-time volet table (toggle right), false = by-day (toggle left) */
private boolean voletAllTime = true; private boolean voletAllTime = true;
@FunctionalInterface
interface HeaderClickListener {
void onHeaderClick(int col);
}
// ── Simple display-ready row holder ────────────────────────────────────── // ── Simple display-ready row holder ──────────────────────────────────────
/** A fully-formatted table row, ready to hand to the UI thread. */ /** A fully-formatted table row, ready to hand to the UI thread. */
private static class DisplayRow { private static class DisplayRow {
final String[] cells; final String[] cells;
DisplayRow(String... cells) { this.cells = cells; } final Comparable[] keys;
DisplayRow(Comparable[] keys, String... cells) {
this.keys = keys;
this.cells = cells;
} }
}
private List<DisplayRow> cacheVoletAllTime = new ArrayList<>();
private List<DisplayRow> cacheVoletByDay = new ArrayList<>();
private List<DisplayRow> cacheDay = new ArrayList<>();
private int sortVoletAllTimeCol = 4; // Rate
private boolean sortVoletAllTimeAsc = false;
private int sortVoletByDayCol = 5; // Rate
private boolean sortVoletByDayAsc = false;
private int sortDayCol = 0; // Date
private boolean sortDayAsc = false;
@Override @Override
public View onCreateView(@NonNull LayoutInflater inflater, public View onCreateView(@NonNull LayoutInflater inflater,
...@@ -67,17 +87,19 @@ public class StatsFragment extends Fragment { ...@@ -67,17 +87,19 @@ public class StatsFragment extends Fragment {
: String.format("%.0f%%", 100.0 * s.correct / s.total); : String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak( int streak = longestStreak(
db.tryDao().getCorrectSequenceForDaySync(s.day)); db.tryDao().getCorrectSequenceForDaySync(s.day));
double ratio = s.total == 0 ? 0 : (double) s.correct / s.total;
dayRows.add(new DisplayRow( dayRows.add(new DisplayRow(
new Comparable[]{s.day, ratio, ratio, streak},
s.day, s.day,
String.valueOf(s.correct), s.correct + "/" + s.total,
String.valueOf(s.total),
rate, rate,
String.valueOf(streak))); String.valueOf(streak)));
} }
cacheDay = dayRows;
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
binding.progressStats.setVisibility(View.GONE); binding.progressStats.setVisibility(View.GONE);
renderDayTable(dayRows); renderDayTable(cacheDay);
}); });
// Also kick off the volet table on the same background thread // Also kick off the volet table on the same background thread
...@@ -93,40 +115,44 @@ public class StatsFragment extends Fragment { ...@@ -93,40 +115,44 @@ public class StatsFragment extends Fragment {
/** Must only be called from the executor thread. */ /** Must only be called from the executor thread. */
private void loadVoletTableOnBackground() { private void loadVoletTableOnBackground() {
final List<DisplayRow> rows;
final boolean allTime = voletAllTime; final boolean allTime = voletAllTime;
final List<DisplayRow> rows = new ArrayList<>();
if (allTime) { if (allTime) {
List<TryDao.VoletStat> stats = repo.getVoletStats(); List<TryDao.VoletStat> stats = repo.getVoletStats();
rows = new ArrayList<>();
for (TryDao.VoletStat s : stats) { for (TryDao.VoletStat s : stats) {
String rate = s.total == 0 ? "—" String rate = s.total == 0 ? "—"
: String.format("%.0f%%", 100.0 * s.correct / s.total); : String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak( int streak = longestStreak(
db.tryDao().getCorrectSequenceForVoletSync(s.voletId)); db.tryDao().getCorrectSequenceForVoletSync(s.voletId));
double ratio = s.total == 0 ? 0 : (double) s.correct / s.total;
rows.add(new DisplayRow( rows.add(new DisplayRow(
new Comparable[]{s.bookTitle, s.unitTitle, s.voletTitle, ratio, ratio, streak},
s.bookTitle, s.unitTitle, s.voletTitle, s.bookTitle, s.unitTitle, s.voletTitle,
s.correct + "/" + s.total, rate, s.correct + "/" + s.total, rate,
String.valueOf(streak))); String.valueOf(streak)));
} }
cacheVoletAllTime = rows;
} else { } else {
List<TryDao.VoletDayStat> stats = repo.getVoletDayStats(); List<TryDao.VoletDayStat> stats = repo.getVoletDayStats();
rows = new ArrayList<>();
for (TryDao.VoletDayStat s : stats) { for (TryDao.VoletDayStat s : stats) {
String rate = s.total == 0 ? "—" String rate = s.total == 0 ? "—"
: String.format("%.0f%%", 100.0 * s.correct / s.total); : String.format("%.0f%%", 100.0 * s.correct / s.total);
int streak = longestStreak( int streak = longestStreak(
db.tryDao().getCorrectSequenceForVoletDaySync(s.voletId, s.day)); db.tryDao().getCorrectSequenceForVoletDaySync(s.voletId, s.day));
double ratio = s.total == 0 ? 0 : (double) s.correct / s.total;
rows.add(new DisplayRow( rows.add(new DisplayRow(
s.day, s.voletTitle, new Comparable[]{s.day, s.bookTitle, s.unitTitle, s.voletTitle, ratio, ratio, streak},
s.day, s.bookTitle, s.unitTitle, s.voletTitle,
s.correct + "/" + s.total, rate, s.correct + "/" + s.total, rate,
String.valueOf(streak), "")); String.valueOf(streak)));
} }
cacheVoletByDay = rows;
} }
requireActivity().runOnUiThread(() -> { requireActivity().runOnUiThread(() -> {
binding.progressStats.setVisibility(View.GONE); binding.progressStats.setVisibility(View.GONE);
renderVoletTable(allTime, rows); renderVoletTable(allTime, allTime ? cacheVoletAllTime : cacheVoletByDay);
}); });
} }
...@@ -135,31 +161,93 @@ public class StatsFragment extends Fragment { ...@@ -135,31 +161,93 @@ public class StatsFragment extends Fragment {
private void renderVoletTable(boolean allTime, List<DisplayRow> rows) { private void renderVoletTable(boolean allTime, List<DisplayRow> rows) {
TableLayout table = binding.tableVolet; TableLayout table = binding.tableVolet;
table.removeAllViews(); table.removeAllViews();
int col = allTime ? sortVoletAllTimeCol : sortVoletByDayCol;
boolean asc = allTime ? sortVoletAllTimeAsc : sortVoletByDayAsc;
sortRows(rows, col, asc);
if (allTime) { if (allTime) {
table.addView(makeRow(true, table.addView(makeRow(this::onVoletHeaderClick,
"Book", "Unit", "Volet", "Correct", "Rate", "Streak")); "Book", "Unit", "Volet", "Correct", "Rate", "Streak"));
if (rows.isEmpty()) { if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—", "—")); table.addView(makeRow(null, "—", "—", "No data yet", "—", "—", "—"));
} }
} else { } else {
table.addView(makeRow(true, table.addView(makeRow(this::onVoletHeaderClick,
"Day", "Volet", "Correct", "Rate", "Streak", "")); "Day", "Book", "Unit", "Volet", "Correct", "Rate", "Streak"));
if (rows.isEmpty()) { if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—", "")); table.addView(makeRow(null, "—", "—", "—", "No data yet", "—", "—", ""));
} }
} }
for (DisplayRow r : rows) table.addView(makeRow(false, r.cells)); for (DisplayRow r : rows) table.addView(makeRow(null, r.cells));
} }
private void renderDayTable(List<DisplayRow> rows) { private void renderDayTable(List<DisplayRow> rows) {
TableLayout table = binding.tableDay; TableLayout table = binding.tableDay;
table.removeAllViews(); table.removeAllViews();
table.addView(makeRow(true, "Date", "Correct", "Total", "Rate", "Streak"));
sortRows(rows, sortDayCol, sortDayAsc);
table.addView(makeRow(this::onDayHeaderClick, "Date", "Correct", "Rate", "Streak"));
if (rows.isEmpty()) { if (rows.isEmpty()) {
table.addView(makeRow(false, "—", "—", "No data yet", "—", "—")); table.addView(makeRow(null, "—", "—", "No data yet", "—"));
}
for (DisplayRow r : rows) table.addView(makeRow(null, r.cells));
}
// ── Table Sorting Helpers ─────────────────────────────────────────────────
private void sortRows(List<DisplayRow> rows, int col, boolean asc) {
if (rows.isEmpty()) return;
rows.sort((a, b) -> {
Comparable k1 = a.keys[col];
Comparable k2 = b.keys[col];
int cmp;
if (k1 == null && k2 == null) cmp = 0;
else if (k1 == null) cmp = -1;
else if (k2 == null) cmp = 1;
else {
@SuppressWarnings("unchecked")
int c = k1.compareTo(k2);
cmp = c;
} }
for (DisplayRow r : rows) table.addView(makeRow(false, r.cells)); return asc ? cmp : -cmp;
});
}
private void onVoletHeaderClick(int col) {
if (voletAllTime) {
if (sortVoletAllTimeCol == col) {
sortVoletAllTimeAsc = !sortVoletAllTimeAsc;
} else {
sortVoletAllTimeCol = col;
sortVoletAllTimeAsc = !isVoletAllTimeDescFirst(col);
}
renderVoletTable(true, cacheVoletAllTime);
} else {
if (sortVoletByDayCol == col) {
sortVoletByDayAsc = !sortVoletByDayAsc;
} else {
sortVoletByDayCol = col;
sortVoletByDayAsc = !isVoletByDayDescFirst(col);
} }
renderVoletTable(false, cacheVoletByDay);
}
}
private void onDayHeaderClick(int col) {
if (sortDayCol == col) {
sortDayAsc = !sortDayAsc;
} else {
sortDayCol = col;
sortDayAsc = !isDayDescFirst(col);
}
renderDayTable(cacheDay);
}
private boolean isVoletAllTimeDescFirst(int col) { return col == 4 || col == 5; }
private boolean isVoletByDayDescFirst(int col) { return col == 5 || col == 6; }
private boolean isDayDescFirst(int col) { return col == 2 || col == 3; }
// ── Streak computation ──────────────────────────────────────────────────── // ── Streak computation ────────────────────────────────────────────────────
...@@ -175,16 +263,19 @@ public class StatsFragment extends Fragment { ...@@ -175,16 +263,19 @@ public class StatsFragment extends Fragment {
// ── Row builder ─────────────────────────────────────────────────────────── // ── Row builder ───────────────────────────────────────────────────────────
private TableRow makeRow(boolean header, String... cells) { private TableRow makeRow(HeaderClickListener listener, String... cells) {
boolean header = (listener != null);
TableRow row = new TableRow(requireContext()); TableRow row = new TableRow(requireContext());
if (header) row.setBackgroundColor(0xFF6200EE); if (header) row.setBackgroundColor(0xFF6200EE);
for (String cell : cells) { for (int i = 0; i < cells.length; i++) {
TextView tv = new TextView(requireContext()); TextView tv = new TextView(requireContext());
tv.setText(cell); tv.setText(cells[i]);
tv.setPadding(16, 10, 16, 10); tv.setPadding(16, 10, 16, 10);
if (header) { if (header) {
tv.setTextColor(0xFFFFFFFF); tv.setTextColor(0xFFFFFFFF);
tv.setTypeface(null, android.graphics.Typeface.BOLD); tv.setTypeface(null, android.graphics.Typeface.BOLD);
final int col = i;
tv.setOnClickListener(v -> listener.onHeaderClick(col));
} }
TableRow.LayoutParams lp = new TableRow.LayoutParams( TableRow.LayoutParams lp = new TableRow.LayoutParams(
0, TableRow.LayoutParams.WRAP_CONTENT, 1f); 0, TableRow.LayoutParams.WRAP_CONTENT, 1f);
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment