|
| 1 | +# Feature: "X of Y" Event Count in Search Bar |
| 2 | + |
| 3 | +**GitHub Issue:** [#241](https://github.com/williscool/CalendarNotification/issues/241) |
| 4 | +**Parent Doc:** [event_lookahead_milestone3_filter_pills.md](./event_lookahead_milestone3_filter_pills.md) |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +When filter pills (Calendar, Status, Time) reduce the event list, the search bar hint should show "X of Y" so the user knows they're viewing a subset. |
| 9 | + |
| 10 | +## Current State |
| 11 | + |
| 12 | +| Feature | Status | |
| 13 | +|---------|--------| |
| 14 | +| Search hint shows event count | ✅ `"Search 70 Active events..."` | |
| 15 | +| Count reflects filter pills | ✅ Count already updates after filtering | |
| 16 | +| User can tell it's a subset | ❌ No indication that 70 is a filtered view of a larger list | |
| 17 | + |
| 18 | +### Current Flow |
| 19 | + |
| 20 | +1. Fragment calls `filterState.filterEvents(db.eventsForDisplay, now)` → filtered events |
| 21 | +2. Adapter stores filtered events as `allEvents` |
| 22 | +3. `getEventCount()` returns `allEvents.size` (post-filter count) |
| 23 | +4. `MainActivityModern.onCreateOptionsMenu()` uses that count for the hint |
| 24 | +5. Hint: `"Search %d %s events..."` → `"Search 70 Active events..."` |
| 25 | + |
| 26 | +The total (unfiltered) count is never tracked — it's discarded during filtering. |
| 27 | + |
| 28 | +## Goal |
| 29 | + |
| 30 | +When filter pills are active and reduce the count: |
| 31 | + |
| 32 | +``` |
| 33 | +Search 70 of 200 Active events... |
| 34 | +``` |
| 35 | + |
| 36 | +When no filters are active (or filters match everything): |
| 37 | + |
| 38 | +``` |
| 39 | +Search 200 Active events... |
| 40 | +``` |
| 41 | + |
| 42 | +The hint is only visible when the search box is empty (Android replaces it with typed text), so text-search filtering is irrelevant. |
| 43 | + |
| 44 | +## Design Decisions |
| 45 | + |
| 46 | +| Decision | Choice | Rationale | |
| 47 | +|----------|--------|-----------| |
| 48 | +| Text format | `"Search X of Y Tab events..."` | Clear, concise, standard "X of Y" pattern | |
| 49 | +| When to show "of Y" | Only when `hasActiveFilters() && filtered != total` | Don't show "70 of 70" when filters are active but match everything | |
| 50 | +| What counts as "total" | Unfiltered DB count for the tab | The number before any filter pills are applied | |
| 51 | +| Text search interaction | N/A | Hint is not visible while typing — only filter pills matter | |
| 52 | +| Plural key | Keys off filtered count (`X`) | "Search 1 of 200 Active event..." vs "Search 5 of 200 Active events..." | |
| 53 | + |
| 54 | +## Implementation |
| 55 | + |
| 56 | +### Phase 1: Track Total (Unfiltered) Event Count |
| 57 | + |
| 58 | +**Goal:** Each fragment remembers the total event count before filtering. |
| 59 | + |
| 60 | +**Files:** |
| 61 | +- `SearchableFragment.kt` — add `getTotalEventCount()` with default impl |
| 62 | +- `ActiveEventsFragment.kt` — track and expose total count |
| 63 | +- `UpcomingEventsFragment.kt` — track and expose total count |
| 64 | +- `DismissedEventsFragment.kt` — track and expose total count |
| 65 | + |
| 66 | +**SearchableFragment.kt** — new method: |
| 67 | + |
| 68 | +```kotlin |
| 69 | +/** Get total event count before filter pills (for "X of Y" hint) */ |
| 70 | +fun getTotalEventCount(): Int = getEventCount() |
| 71 | +``` |
| 72 | + |
| 73 | +**Each fragment's `loadEvents()`** — capture total before filtering: |
| 74 | + |
| 75 | +```kotlin |
| 76 | +// ActiveEventsFragment.loadEvents() example |
| 77 | +val allDbEvents = db.eventsForDisplay |
| 78 | +totalEventCount = allDbEvents.size // new field |
| 79 | +val events = filterState.filterEvents(allDbEvents, now) |
| 80 | +``` |
| 81 | + |
| 82 | +Add a `private var totalEventCount: Int = 0` field to each fragment and override `getTotalEventCount()`. |
| 83 | + |
| 84 | +### Phase 2: Conditional Search Hint |
| 85 | + |
| 86 | +**Goal:** Show "X of Y" format when filters reduce the count. |
| 87 | + |
| 88 | +**File:** `MainActivityModern.kt` (lines ~256-263) |
| 89 | + |
| 90 | +Replace: |
| 91 | + |
| 92 | +```kotlin |
| 93 | +val count = currentFragment?.getEventCount() ?: 0 |
| 94 | +searchView?.queryHint = resources.getQuantityString( |
| 95 | + R.plurals.search_placeholder, count, count, tabName) |
| 96 | +``` |
| 97 | + |
| 98 | +With: |
| 99 | + |
| 100 | +```kotlin |
| 101 | +val count = currentFragment?.getEventCount() ?: 0 |
| 102 | +val totalCount = currentFragment?.getTotalEventCount() ?: count |
| 103 | +val hasActiveFilters = getCurrentFilterState().hasActiveFilters() |
| 104 | + |
| 105 | +searchView?.queryHint = if (hasActiveFilters && count != totalCount) { |
| 106 | + resources.getQuantityString( |
| 107 | + R.plurals.search_placeholder_filtered, count, count, totalCount, tabName) |
| 108 | +} else { |
| 109 | + resources.getQuantityString( |
| 110 | + R.plurals.search_placeholder, count, count, tabName) |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +### Phase 3: String Resources |
| 115 | + |
| 116 | +**File:** `values/strings.xml` |
| 117 | + |
| 118 | +```xml |
| 119 | +<plurals name="search_placeholder_filtered"> |
| 120 | + <item quantity="one">Search %1$d of %3$d %4$s event...</item> |
| 121 | + <item quantity="other">Search %1$d of %3$d %4$s events...</item> |
| 122 | +</plurals> |
| 123 | +``` |
| 124 | + |
| 125 | +Note: `%2$d` is skipped intentionally — `getQuantityString(id, quantity, arg1, arg2, arg3)` uses `quantity` for plural selection and `%1$d`/`%3$d`/`%4$s` for formatting. Keeping arg order consistent: filtered count, total count, tab name. |
| 126 | + |
| 127 | +**File:** `values-fr/strings.xml` |
| 128 | + |
| 129 | +```xml |
| 130 | +<plurals name="search_placeholder_filtered"> |
| 131 | + <item quantity="one">Rechercher %1$d sur %3$d événement %4$s…</item> |
| 132 | + <item quantity="other">Rechercher %1$d sur %3$d événements %4$s…</item> |
| 133 | +</plurals> |
| 134 | +``` |
| 135 | + |
| 136 | +## Testing |
| 137 | + |
| 138 | +### Unit/Instrumentation Tests |
| 139 | + |
| 140 | +- Search hint shows standard format when no filters active |
| 141 | +- Search hint shows "X of Y" format when filter pills reduce count |
| 142 | +- Search hint shows standard format when filters are active but match all events (count == totalCount) |
| 143 | +- `getTotalEventCount()` returns correct unfiltered count |
| 144 | +- `getEventCount()` returns correct filtered count (existing behavior, regression check) |
| 145 | + |
| 146 | +## Files Changed Summary |
| 147 | + |
| 148 | +| File | Change | |
| 149 | +|------|--------| |
| 150 | +| `SearchableFragment.kt` | Add `getTotalEventCount()` default method | |
| 151 | +| `ActiveEventsFragment.kt` | Track `totalEventCount`, override `getTotalEventCount()` | |
| 152 | +| `UpcomingEventsFragment.kt` | Track `totalEventCount`, override `getTotalEventCount()` | |
| 153 | +| `DismissedEventsFragment.kt` | Track `totalEventCount`, override `getTotalEventCount()` | |
| 154 | +| `MainActivityModern.kt` | Conditional hint text in `onCreateOptionsMenu()` | |
| 155 | +| `values/strings.xml` | Add `search_placeholder_filtered` plural | |
| 156 | +| `values-fr/strings.xml` | Add French translation | |
0 commit comments