Skip to content

Commit 361ed40

Browse files
authored
Merge pull request #17 from headlines-toolkit/firebase_to_custom_api_mgration
Firebase to custom api mgration
2 parents 035d8cd + c9e69ed commit 361ed40

21 files changed

+1057
-542
lines changed

README.md

Lines changed: 106 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,114 @@
1-
# 📰 Headlines Toolkit
2-
3-
## 📖 Overview
4-
5-
**source-available**, full-stack Flutter application designed as a robust foundation for building modern news applications. This toolkit offers a streamlined, user-friendly experience for browsing news headlines and is built upon a clean, maintainable architecture.
6-
## ✨ Features
7-
8-
- 🗞️ **Headlines Feed:** Displays a minimalist list of news headlines (title only, with source, category, and country represented as icons).
9-
- 📃 **Headline Details Page:** Provides detailed information about a headline (title, image, source, category, date, and a "Continue Reading" button that opens the original article in the browser).
10-
- 🔎 **Search:** Allows users to search for headlines.
11-
- 🗂️ **Filtering:** Allows users to filter headlines by category, source, and event country.
12-
- 🌗 **Dark Mode:** Supports light and dark themes.
13-
- 📅 **Planned Features:**
14-
- 👥 User accounts/profiles
15-
- 🌟 Personalized recommendations
16-
- 💾 Saving articles
17-
- 📵 Offline Reading
18-
- 🔔 Push notifications
19-
- 🚀 Social sharing
20-
- 💬 Comments/discussion features
1+
# 📱✨ ht_main: Headlines Toolkit Main App
2+
3+
![coverage: percentage](https://img.shields.io/badge/coverage-XX-green)
4+
[![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis)
5+
[![License: PolyForm Free Trial](https://img.shields.io/badge/License-PolyForm%20Free%20Trial-blue)](https://polyformproject.org/licenses/free-trial/1.0.0)
6+
7+
`ht_main`** is a Flutter mobile application serves as both a powerful, fully functional news application ready for deployment, and an exceptionally robust starter kit, architected for easy extension and customization. It is a key component of the [Headlines Toolkit](https://github.com/headlines-toolkit), an ecosystem that also includes a [Dart Frog backend API](https://github.com/headlines-toolkit/ht-api) and a [web-based content dashboard](https://github.com/headlines-toolkit/ht-dashboard).
8+
9+
## ⭐ Features & Benefits
10+
11+
`ht_main` comes packed with features to accelerate your development and delight your users:
12+
13+
#### 📰 **Dynamic & Engaging Headlines Feed**
14+
Experience a beautifully crafted, infinitely scrolling news feed. It's highly performant and ready for your content.
15+
* **Benefit for you:** Save months of UI/UX development and complex state management. Get a production-quality feed system instantly! ⏱️
16+
17+
#### 🔍 **Advanced Content Filtering & Search**
18+
Empower users with intuitive filtering for headlines by categories, sources, and countries. A dedicated search page helps users find exactly what they're looking for.
19+
* **Benefit for you:** Offer powerful content discovery tools that significantly enhance user engagement and satisfaction. 🎯
20+
21+
#### 🔐 **Robust User Authentication**
22+
Secure and flexible authentication flows are built-in:
23+
* 📧 **Email + Code (Passwordless) Sign-In:** Modern and secure.
24+
* 👤 **Anonymous Sign-In:** Allow users to explore before committing.
25+
* 🔗 **Account Linking:** Seamlessly convert anonymous users to registered accounts, ensuring all their personalized settings (like theme and language), content preferences (followed categories, sources, countries), and saved headlines are preserved and synced.
26+
* **Benefit for you:** Complex security and user management handled, including data migration during account linking, letting you focus on features. ✅
27+
28+
#### 🧑‍🎨 **Personalized User Accounts & Preferences**
29+
Users can tailor their experience:
30+
* **Content Preferences:** Follow/unfollow categories, sources, and countries.
31+
* **Saved Headlines:** Bookmark articles for easy access later.
32+
* **Benefit for you:** A strong foundation for personalization, driving user retention and creating a sticky app experience. ❤️
33+
34+
#### ⚙️ **Customizable App Settings**
35+
Offer users control over their app experience:
36+
* **Appearance:** Light/Dark/System themes, accent colors (via FlexColorScheme), font choices, and text scaling.
37+
* **Feed Display:** Customize how headlines are presented.
38+
* **Benefit for you:** Provide a premium, adaptable user experience that caters to individual needs. 🔧
39+
40+
#### 📱 **Adaptive UI for All Screens**
41+
Built with `flutter_adaptive_scaffold`, `ht_main` offers responsive navigation and layouts that look great on both phones and tablets.
42+
* **Benefit for you:** Deliver a consistent and optimized UX across a wide range of devices effortlessly. ↔️
43+
44+
#### 🏗️ **Clean & Modern Architecture**
45+
Developed with best practices for a maintainable and scalable codebase:
46+
* **Flutter & Dart:** Cutting-edge mobile development.
47+
* **BLoC Pattern:** Predictable and robust state management.
48+
* **GoRouter:** Well-structured and powerful navigation.
49+
* **Benefit for you:** An easy-to-understand, extendable, and testable foundation for your project. 📈
50+
51+
#### 🌍 **Localization Ready**
52+
Fully internationalized with working English and Arabic localizations (`.arb` files). Adding more languages is straightforward.
53+
* **Benefit for you:** Easily adapt your application for a global audience. 🌐
54+
55+
---
2156

2257
## 🛠️ Technical Overview
2358

24-
- 🎯 **Language:** Dart
25-
- 💙 **Framework:** Flutter
26-
- 🧱 **State Management:** BLoC
27-
- 🔀 **Routing:** go_router
28-
- ⚙️ **Backend:** Firebase (current), Supabase (future)
29-
- 🏛️ **Architecture:** Layered architecture (Data, Repository, Business Logic, Presentation)
59+
* **Framework:** Flutter
60+
* **Language:** Dart
61+
* **State Management:** BLoC / flutter_bloc
62+
* **Navigation:** GoRouter
63+
* **Theming:** FlexColorScheme
64+
* **Core Dependencies:** Integrates seamlessly with Headlines Toolkit shared packages (`ht_shared`, `ht_auth_repository`, `ht_data_repository`, `ht_http_client`, etc.).
65+
66+
---
3067

3168
## 🔑 Access and Licensing
3269

33-
`ht_main` is source-available as part of the Headlines Toolkit ecosystem.
70+
`ht-main` is source-available as part of the Headlines Toolkit ecosystem.
3471

35-
The source code for `ht_main` is available for review as part of the Headlines
36-
Toolkit ecosystem. To acquire a commercial license for building unlimited news
37-
applications with the Headlines Toolkit repositories, please visit the
38-
[Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
72+
To acquire a commercial license for building unlimited news applications, please visit
73+
the [Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
3974
for more details.
75+
76+
---
77+
78+
## 🚀 Getting Started
79+
80+
1. **Ensure Flutter is installed.** (See [Flutter documentation](https://flutter.dev/docs/get-started/install))
81+
2. **Clone the repository:**
82+
```bash
83+
git clone https://github.com/headlines-toolkit/ht-main.git
84+
cd ht-main
85+
```
86+
3. **Get dependencies:**
87+
```bash
88+
flutter pub get
89+
```
90+
4. **Run the app:**
91+
```bash
92+
flutter run
93+
```
94+
*(Note: For full functionality, ensure the `ht-api` backend service is running and accessible.)*
95+
96+
---
97+
98+
## ✅ Testing
99+
100+
This project aims for high test coverage to ensure quality and reliability.
101+
102+
* Run tests with:
103+
```bash
104+
flutter test
105+
```
106+
107+
---
108+
109+
## 📜 License
110+
111+
This package is licensed under the **PolyForm Free Trial**.
112+
Please review the [LICENSE](LICENSE) file for details.
113+
114+
---

lib/account/bloc/account_bloc.dart

Lines changed: 164 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@ import 'dart:async';
22

33
import 'package:bloc/bloc.dart';
44
import 'package:equatable/equatable.dart';
5+
// Hide Category
56
import 'package:ht_auth_repository/ht_auth_repository.dart';
67
import 'package:ht_data_repository/ht_data_repository.dart';
7-
import 'package:ht_shared/ht_shared.dart'
8-
show HtHttpException, User, UserContentPreferences;
8+
import 'package:ht_shared/ht_shared.dart';
99

1010
part 'account_event.dart';
1111
part 'account_state.dart';
@@ -31,6 +31,10 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
3131
on<AccountLoadContentPreferencesRequested>(
3232
_onAccountLoadContentPreferencesRequested,
3333
);
34+
on<AccountFollowCategoryToggled>(_onFollowCategoryToggled);
35+
on<AccountFollowSourceToggled>(_onFollowSourceToggled);
36+
on<AccountFollowCountryToggled>(_onFollowCountryToggled);
37+
on<AccountSaveHeadlineToggled>(_onSaveHeadlineToggled);
3438
// Handlers for AccountSettingsNavigationRequested and
3539
// AccountBackupNavigationRequested are typically handled in the UI layer
3640
// (e.g., BlocListener navigating) or could emit specific states if needed.
@@ -90,4 +94,162 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
9094
);
9195
}
9296
}
97+
98+
Future<void> _persistPreferences(
99+
UserContentPreferences preferences,
100+
Emitter<AccountState> emit,
101+
) async {
102+
if (state.user == null) {
103+
emit(
104+
state.copyWith(
105+
status: AccountStatus.failure,
106+
errorMessage: 'User not authenticated to save preferences.',
107+
),
108+
);
109+
return;
110+
}
111+
try {
112+
await _userContentPreferencesRepository.update(
113+
id: state.user!.id, // ID of the preferences object is the user's ID
114+
item: preferences,
115+
userId: state.user!.id,
116+
);
117+
// Optimistic update already done, emit success if needed for UI feedback
118+
// emit(state.copyWith(status: AccountStatus.success));
119+
} on HtHttpException catch (e) {
120+
emit(
121+
state.copyWith(
122+
status: AccountStatus.failure,
123+
errorMessage: 'Failed to save preferences: ${e.message}',
124+
),
125+
);
126+
} catch (e) {
127+
emit(
128+
state.copyWith(
129+
status: AccountStatus.failure,
130+
errorMessage: 'An unexpected error occurred while saving: $e',
131+
),
132+
);
133+
}
134+
}
135+
136+
Future<void> _onFollowCategoryToggled(
137+
AccountFollowCategoryToggled event,
138+
Emitter<AccountState> emit,
139+
) async {
140+
if (state.preferences == null || state.user == null) return;
141+
142+
final currentPrefs = state.preferences!;
143+
final updatedFollowedCategories = List<Category>.from(
144+
currentPrefs.followedCategories,
145+
);
146+
147+
final isCurrentlyFollowing = updatedFollowedCategories.any(
148+
(category) => category.id == event.category.id,
149+
);
150+
151+
if (isCurrentlyFollowing) {
152+
updatedFollowedCategories.removeWhere(
153+
(category) => category.id == event.category.id,
154+
);
155+
} else {
156+
updatedFollowedCategories.add(event.category);
157+
}
158+
159+
final newPreferences = currentPrefs.copyWith(
160+
followedCategories: updatedFollowedCategories,
161+
);
162+
emit(state.copyWith(preferences: newPreferences));
163+
await _persistPreferences(newPreferences, emit);
164+
}
165+
166+
Future<void> _onFollowSourceToggled(
167+
AccountFollowSourceToggled event,
168+
Emitter<AccountState> emit,
169+
) async {
170+
if (state.preferences == null || state.user == null) return;
171+
172+
final currentPrefs = state.preferences!;
173+
final updatedFollowedSources = List<Source>.from(
174+
currentPrefs.followedSources,
175+
);
176+
177+
final isCurrentlyFollowing = updatedFollowedSources.any(
178+
(source) => source.id == event.source.id,
179+
);
180+
181+
if (isCurrentlyFollowing) {
182+
updatedFollowedSources.removeWhere(
183+
(source) => source.id == event.source.id,
184+
);
185+
} else {
186+
updatedFollowedSources.add(event.source);
187+
}
188+
189+
final newPreferences = currentPrefs.copyWith(
190+
followedSources: updatedFollowedSources,
191+
);
192+
emit(state.copyWith(preferences: newPreferences));
193+
await _persistPreferences(newPreferences, emit);
194+
}
195+
196+
Future<void> _onFollowCountryToggled(
197+
AccountFollowCountryToggled event,
198+
Emitter<AccountState> emit,
199+
) async {
200+
if (state.preferences == null || state.user == null) return;
201+
202+
final currentPrefs = state.preferences!;
203+
final updatedFollowedCountries = List<Country>.from(
204+
currentPrefs.followedCountries,
205+
);
206+
207+
final isCurrentlyFollowing = updatedFollowedCountries.any(
208+
(country) => country.id == event.country.id,
209+
);
210+
211+
if (isCurrentlyFollowing) {
212+
updatedFollowedCountries.removeWhere(
213+
(country) => country.id == event.country.id,
214+
);
215+
} else {
216+
updatedFollowedCountries.add(event.country);
217+
}
218+
219+
final newPreferences = currentPrefs.copyWith(
220+
followedCountries: updatedFollowedCountries,
221+
);
222+
emit(state.copyWith(preferences: newPreferences));
223+
await _persistPreferences(newPreferences, emit);
224+
}
225+
226+
Future<void> _onSaveHeadlineToggled(
227+
AccountSaveHeadlineToggled event,
228+
Emitter<AccountState> emit,
229+
) async {
230+
if (state.preferences == null || state.user == null) return;
231+
232+
final currentPrefs = state.preferences!;
233+
final updatedSavedHeadlines = List<Headline>.from(
234+
currentPrefs.savedHeadlines,
235+
);
236+
237+
final isCurrentlySaved = updatedSavedHeadlines.any(
238+
(headline) => headline.id == event.headline.id,
239+
);
240+
241+
if (isCurrentlySaved) {
242+
updatedSavedHeadlines.removeWhere(
243+
(headline) => headline.id == event.headline.id,
244+
);
245+
} else {
246+
updatedSavedHeadlines.add(event.headline);
247+
}
248+
249+
final newPreferences = currentPrefs.copyWith(
250+
savedHeadlines: updatedSavedHeadlines,
251+
);
252+
emit(state.copyWith(preferences: newPreferences));
253+
await _persistPreferences(newPreferences, emit);
254+
}
93255
}

lib/account/bloc/account_event.dart

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,55 @@ final class AccountLoadContentPreferencesRequested extends AccountEvent {
3838
@override
3939
List<Object> get props => [userId];
4040
}
41+
42+
/// {@template account_follow_category_toggled}
43+
/// Event triggered when a user toggles following a category.
44+
/// {@endtemplate}
45+
final class AccountFollowCategoryToggled extends AccountEvent {
46+
/// {@macro account_follow_category_toggled}
47+
const AccountFollowCategoryToggled({required this.category});
48+
49+
final Category category;
50+
51+
@override
52+
List<Object> get props => [category];
53+
}
54+
55+
/// {@template account_follow_source_toggled}
56+
/// Event triggered when a user toggles following a source.
57+
/// {@endtemplate}
58+
final class AccountFollowSourceToggled extends AccountEvent {
59+
/// {@macro account_follow_source_toggled}
60+
const AccountFollowSourceToggled({required this.source});
61+
62+
final Source source;
63+
64+
@override
65+
List<Object> get props => [source];
66+
}
67+
68+
/// {@template account_follow_country_toggled}
69+
/// Event triggered when a user toggles following a country.
70+
/// {@endtemplate}
71+
final class AccountFollowCountryToggled extends AccountEvent {
72+
/// {@macro account_follow_country_toggled}
73+
const AccountFollowCountryToggled({required this.country});
74+
75+
final Country country;
76+
77+
@override
78+
List<Object> get props => [country];
79+
}
80+
81+
/// {@template account_save_headline_toggled}
82+
/// Event triggered when a user toggles saving a headline.
83+
/// {@endtemplate}
84+
final class AccountSaveHeadlineToggled extends AccountEvent {
85+
/// {@macro account_save_headline_toggled}
86+
const AccountSaveHeadlineToggled({required this.headline});
87+
88+
final Headline headline;
89+
90+
@override
91+
List<Object> get props => [headline];
92+
}

0 commit comments

Comments
 (0)