Skip to content

Commit c58025e

Browse files
committed
refactor: enhanced the UI of the headlines search page
1 parent a4ef9d9 commit c58025e

File tree

5 files changed

+222
-132
lines changed

5 files changed

+222
-132
lines changed
Lines changed: 209 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,228 @@
11
import 'package:flutter/material.dart';
22
import 'package:flutter_bloc/flutter_bloc.dart';
33
import 'package:ht_headlines_repository/ht_headlines_repository.dart';
4+
import 'package:ht_main/headlines-feed/widgets/headline_item_widget.dart';
45
import 'package:ht_main/headlines-search/bloc/headlines_search_bloc.dart';
5-
import 'package:ht_main/headlines-search/view/headlines_search_view.dart';
6+
import 'package:ht_main/l10n/l10n.dart';
7+
import 'package:ht_main/shared/constants/app_spacing.dart'; // Import AppSpacing
8+
import 'package:ht_main/shared/widgets/failure_state_widget.dart';
9+
import 'package:ht_main/shared/widgets/initial_state_widget.dart';
610

11+
/// Page widget responsible for providing the BLoC for the headlines search feature.
712
class HeadlinesSearchPage extends StatelessWidget {
813
const HeadlinesSearchPage({super.key});
914

15+
/// Defines the route for this page.
1016
static Route<void> route() {
1117
return MaterialPageRoute<void>(builder: (_) => const HeadlinesSearchPage());
1218
}
1319

1420
@override
1521
Widget build(BuildContext context) {
1622
return BlocProvider(
17-
create:
18-
(_) => HeadlinesSearchBloc(
19-
headlinesRepository: context.read<HtHeadlinesRepository>(),
23+
create: (_) => HeadlinesSearchBloc(
24+
headlinesRepository: context.read<HtHeadlinesRepository>(),
25+
),
26+
// The actual UI is built by the private _HeadlinesSearchView widget.
27+
child: const _HeadlinesSearchView(),
28+
);
29+
}
30+
}
31+
32+
/// Private View widget that builds the UI for the headlines search page.
33+
/// It listens to the HeadlinesSearchBloc state and displays the appropriate UI.
34+
class _HeadlinesSearchView extends StatefulWidget {
35+
const _HeadlinesSearchView(); // Private constructor
36+
37+
@override
38+
State<_HeadlinesSearchView> createState() => _HeadlinesSearchViewState();
39+
}
40+
41+
class _HeadlinesSearchViewState extends State<_HeadlinesSearchView> {
42+
final _scrollController = ScrollController();
43+
final _textController = TextEditingController(); // Controller for the TextField
44+
bool _showClearButton = false; // State to control clear button visibility
45+
46+
@override
47+
void initState() {
48+
super.initState();
49+
_scrollController.addListener(_onScroll);
50+
// Listen to text changes to control clear button visibility
51+
_textController.addListener(() {
52+
setState(() {
53+
_showClearButton = _textController.text.isNotEmpty;
54+
});
55+
});
56+
}
57+
58+
@override
59+
void dispose() {
60+
_scrollController
61+
..removeListener(_onScroll)
62+
..dispose();
63+
_textController.dispose(); // Dispose the text controller
64+
super.dispose();
65+
}
66+
67+
/// Handles scroll events to trigger fetching more results when near the bottom.
68+
void _onScroll() {
69+
final state = context.read<HeadlinesSearchBloc>().state;
70+
if (_isBottom && state is HeadlinesSearchSuccess) {
71+
final searchTerm = state.lastSearchTerm; // Use lastSearchTerm from state
72+
if (state.hasMore) {
73+
context.read<HeadlinesSearchBloc>().add(
74+
HeadlinesSearchFetchRequested(searchTerm: searchTerm!),
75+
);
76+
}
77+
}
78+
}
79+
80+
/// Checks if the scroll position is near the bottom of the list.
81+
bool get _isBottom {
82+
if (!_scrollController.hasClients) return false;
83+
final maxScroll = _scrollController.position.maxScrollExtent;
84+
final currentScroll = _scrollController.offset;
85+
// Trigger slightly before the absolute bottom for a smoother experience
86+
return currentScroll >= (maxScroll * 0.98);
87+
}
88+
89+
/// Triggers a search request based on the current text input.
90+
void _performSearch() {
91+
context.read<HeadlinesSearchBloc>().add(
92+
HeadlinesSearchFetchRequested(searchTerm: _textController.text),
93+
);
94+
}
95+
96+
@override
97+
Widget build(BuildContext context) {
98+
final l10n = context.l10n;
99+
final theme = Theme.of(context);
100+
final colorScheme = theme.colorScheme;
101+
final appBarTheme = theme.appBarTheme;
102+
103+
return Scaffold(
104+
appBar: AppBar(
105+
// Enhanced TextField integrated into the AppBar title
106+
title: TextField(
107+
controller: _textController,
108+
// Use text style that contrasts well with AppBar background
109+
style: appBarTheme.titleTextStyle ?? theme.textTheme.titleLarge,
110+
decoration: InputDecoration(
111+
hintText: l10n.headlinesSearchHintText,
112+
// Use hint style that contrasts well
113+
hintStyle: appBarTheme.titleTextStyle?.copyWith(
114+
color: (appBarTheme.titleTextStyle?.color ??
115+
colorScheme.onSurface)
116+
.withOpacity(0.6),
117+
) ??
118+
theme.textTheme.titleLarge?.copyWith(
119+
color: colorScheme.onSurface.withOpacity(0.6),
120+
),
121+
// Remove the default border
122+
border: InputBorder.none,
123+
// Remove focused border highlight if any
124+
focusedBorder: InputBorder.none,
125+
// Remove enabled border highlight if any
126+
enabledBorder: InputBorder.none,
127+
// Add a subtle background fill
128+
filled: true,
129+
// Use a subtle color from the theme for the fill
130+
fillColor: colorScheme.surface.withOpacity(0.1),
131+
// Apply consistent padding using AppSpacing
132+
contentPadding: const EdgeInsets.symmetric(
133+
horizontal: AppSpacing.paddingMedium,
134+
vertical: AppSpacing.paddingSmall, // Adjust vertical padding as needed
135+
),
136+
// Add a clear button that appears when text is entered
137+
suffixIcon: _showClearButton
138+
? IconButton(
139+
icon: Icon(
140+
Icons.clear,
141+
// Use icon color appropriate for AppBar
142+
color: appBarTheme.iconTheme?.color ?? colorScheme.onSurface,
143+
),
144+
onPressed: () {
145+
_textController.clear();
146+
// Optionally trigger an empty search or reset state
147+
// _performSearch(); // Uncomment if clearing should trigger search
148+
},
149+
)
150+
: null, // No icon when text field is empty
151+
),
152+
// Trigger search on submit (e.g., pressing Enter on keyboard)
153+
onSubmitted: (_) => _performSearch(),
154+
),
155+
actions: [
156+
// Search action button
157+
IconButton(
158+
icon: const Icon(Icons.search),
159+
tooltip: l10n.headlinesSearchActionTooltip, // Re-added tooltip
160+
onPressed: _performSearch, // Use the dedicated search method
20161
),
21-
child: const HeadlinesSearchView(),
162+
],
163+
),
164+
body: BlocBuilder<HeadlinesSearchBloc, HeadlinesSearchState>(
165+
builder: (context, state) {
166+
// Handle different states of the search BLoC
167+
return switch (state) {
168+
// Loading state
169+
HeadlinesSearchLoading() => InitialStateWidget(
170+
icon: Icons.manage_search, // Changed icon
171+
headline: l10n.headlinesSearchInitialHeadline, // Keep initial text for loading phase
172+
subheadline: l10n.headlinesSearchInitialSubheadline,
173+
),
174+
// Success state with results
175+
HeadlinesSearchSuccess(
176+
:final headlines,
177+
:final hasMore,
178+
:final errorMessage, // Check for specific error message within success
179+
:final lastSearchTerm, // Use lastSearchTerm for retries
180+
) =>
181+
errorMessage != null
182+
// Display error if present within success state
183+
? FailureStateWidget(
184+
message: errorMessage,
185+
onRetry: () {
186+
// Retry with the last successful search term
187+
context.read<HeadlinesSearchBloc>().add(
188+
HeadlinesSearchFetchRequested(
189+
searchTerm: lastSearchTerm ?? '',
190+
),
191+
);
192+
},
193+
)
194+
// Display "no results" if list is empty
195+
: headlines.isEmpty
196+
? InitialStateWidget(
197+
icon: Icons.search_off,
198+
headline: l10n.headlinesSearchNoResultsHeadline,
199+
subheadline:
200+
l10n.headlinesSearchNoResultsSubheadline,
201+
)
202+
// Display the list of headlines
203+
: ListView.builder(
204+
controller: _scrollController,
205+
// Add 1 for loading indicator if more items exist
206+
itemCount:
207+
hasMore ? headlines.length + 1 : headlines.length,
208+
itemBuilder: (context, index) {
209+
// Show loading indicator at the end if hasMore
210+
if (index >= headlines.length) {
211+
// Ensure loading indicator is visible
212+
return const Padding(
213+
padding: EdgeInsets.all(AppSpacing.paddingLarge),
214+
child: Center(child: CircularProgressIndicator()),
215+
);
216+
}
217+
// Display headline item
218+
return HeadlineItemWidget(headline: headlines[index]);
219+
},
220+
),
221+
// Default case (should ideally not be reached if states are handled)
222+
_ => const SizedBox.shrink(),
223+
};
224+
},
225+
),
22226
);
23227
}
24228
}

lib/headlines-search/view/headlines_search_view.dart

Lines changed: 0 additions & 122 deletions
This file was deleted.

lib/l10n/arb/app_ar.arb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,11 +200,11 @@
200200
"@headlinesSearchHintText": {
201201
"description": "Hint text for the search input field"
202202
},
203-
"headlinesSearchInitialHeadline": "ابحث في العناوين",
203+
"headlinesSearchInitialHeadline": "اعثر على العناوين فوراً",
204204
"@headlinesSearchInitialHeadline": {
205205
"description": "Headline text shown in the initial state widget on the search page"
206206
},
207-
"headlinesSearchInitialSubheadline": "أدخل كلمات رئيسية للعثور على المقالات",
207+
"headlinesSearchInitialSubheadline": "اكتب كلمات رئيسية أعلاه لاكتشاف المقالات الإخبارية.",
208208
"@headlinesSearchInitialSubheadline": {
209209
"description": "Subheadline text shown in the initial state widget on the search page"
210210
},
@@ -275,5 +275,9 @@
275275
"accountNotificationsTile": "الإشعارات",
276276
"@accountNotificationsTile": {
277277
"description": "Title for the notifications navigation tile in the account page"
278+
},
279+
"headlinesSearchActionTooltip": "بحث",
280+
"@headlinesSearchActionTooltip": {
281+
"description": "Tooltip text for the search icon button in the search page AppBar"
278282
}
279283
}

0 commit comments

Comments
 (0)