Skip to content

Firebase to custom api mgration #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
137 changes: 106 additions & 31 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,39 +1,114 @@
# 📰 Headlines Toolkit

## 📖 Overview

**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.
## ✨ Features

- 🗞️ **Headlines Feed:** Displays a minimalist list of news headlines (title only, with source, category, and country represented as icons).
- 📃 **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).
- 🔎 **Search:** Allows users to search for headlines.
- 🗂️ **Filtering:** Allows users to filter headlines by category, source, and event country.
- 🌗 **Dark Mode:** Supports light and dark themes.
- 📅 **Planned Features:**
- 👥 User accounts/profiles
- 🌟 Personalized recommendations
- 💾 Saving articles
- 📵 Offline Reading
- 🔔 Push notifications
- 🚀 Social sharing
- 💬 Comments/discussion features
# 📱✨ ht_main: Headlines Toolkit Main App

![coverage: percentage](https://img.shields.io/badge/coverage-XX-green)
[![style: very good analysis](https://img.shields.io/badge/style-very_good_analysis-B22C89.svg)](https://pub.dev/packages/very_good_analysis)
[![License: PolyForm Free Trial](https://img.shields.io/badge/License-PolyForm%20Free%20Trial-blue)](https://polyformproject.org/licenses/free-trial/1.0.0)

`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).

## ⭐ Features & Benefits

`ht_main` comes packed with features to accelerate your development and delight your users:

#### 📰 **Dynamic & Engaging Headlines Feed**
Experience a beautifully crafted, infinitely scrolling news feed. It's highly performant and ready for your content.
* **Benefit for you:** Save months of UI/UX development and complex state management. Get a production-quality feed system instantly! ⏱️

#### 🔍 **Advanced Content Filtering & Search**
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.
* **Benefit for you:** Offer powerful content discovery tools that significantly enhance user engagement and satisfaction. 🎯

#### 🔐 **Robust User Authentication**
Secure and flexible authentication flows are built-in:
* 📧 **Email + Code (Passwordless) Sign-In:** Modern and secure.
* 👤 **Anonymous Sign-In:** Allow users to explore before committing.
* 🔗 **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.
* **Benefit for you:** Complex security and user management handled, including data migration during account linking, letting you focus on features. ✅

#### 🧑‍🎨 **Personalized User Accounts & Preferences**
Users can tailor their experience:
* **Content Preferences:** Follow/unfollow categories, sources, and countries.
* **Saved Headlines:** Bookmark articles for easy access later.
* **Benefit for you:** A strong foundation for personalization, driving user retention and creating a sticky app experience. ❤️

#### ⚙️ **Customizable App Settings**
Offer users control over their app experience:
* **Appearance:** Light/Dark/System themes, accent colors (via FlexColorScheme), font choices, and text scaling.
* **Feed Display:** Customize how headlines are presented.
* **Benefit for you:** Provide a premium, adaptable user experience that caters to individual needs. 🔧

#### 📱 **Adaptive UI for All Screens**
Built with `flutter_adaptive_scaffold`, `ht_main` offers responsive navigation and layouts that look great on both phones and tablets.
* **Benefit for you:** Deliver a consistent and optimized UX across a wide range of devices effortlessly. ↔️

#### 🏗️ **Clean & Modern Architecture**
Developed with best practices for a maintainable and scalable codebase:
* **Flutter & Dart:** Cutting-edge mobile development.
* **BLoC Pattern:** Predictable and robust state management.
* **GoRouter:** Well-structured and powerful navigation.
* **Benefit for you:** An easy-to-understand, extendable, and testable foundation for your project. 📈

#### 🌍 **Localization Ready**
Fully internationalized with working English and Arabic localizations (`.arb` files). Adding more languages is straightforward.
* **Benefit for you:** Easily adapt your application for a global audience. 🌐

---

## 🛠️ Technical Overview

- 🎯 **Language:** Dart
- 💙 **Framework:** Flutter
- 🧱 **State Management:** BLoC
- 🔀 **Routing:** go_router
- ⚙️ **Backend:** Firebase (current), Supabase (future)
- 🏛️ **Architecture:** Layered architecture (Data, Repository, Business Logic, Presentation)
* **Framework:** Flutter
* **Language:** Dart
* **State Management:** BLoC / flutter_bloc
* **Navigation:** GoRouter
* **Theming:** FlexColorScheme
* **Core Dependencies:** Integrates seamlessly with Headlines Toolkit shared packages (`ht_shared`, `ht_auth_repository`, `ht_data_repository`, `ht_http_client`, etc.).

---

## 🔑 Access and Licensing

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

The source code for `ht_main` is available for review as part of the Headlines
Toolkit ecosystem. To acquire a commercial license for building unlimited news
applications with the Headlines Toolkit repositories, please visit the
[Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
To acquire a commercial license for building unlimited news applications, please visit
the [Headlines Toolkit GitHub organization page](https://github.com/headlines-toolkit)
for more details.

---

## 🚀 Getting Started

1. **Ensure Flutter is installed.** (See [Flutter documentation](https://flutter.dev/docs/get-started/install))
2. **Clone the repository:**
```bash
git clone https://github.com/headlines-toolkit/ht-main.git
cd ht-main
```
3. **Get dependencies:**
```bash
flutter pub get
```
4. **Run the app:**
```bash
flutter run
```
*(Note: For full functionality, ensure the `ht-api` backend service is running and accessible.)*

---

## ✅ Testing

This project aims for high test coverage to ensure quality and reliability.

* Run tests with:
```bash
flutter test
```

---

## 📜 License

This package is licensed under the **PolyForm Free Trial**.
Please review the [LICENSE](LICENSE) file for details.

---
166 changes: 164 additions & 2 deletions lib/account/bloc/account_bloc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import 'dart:async';

import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
// Hide Category
import 'package:ht_auth_repository/ht_auth_repository.dart';
import 'package:ht_data_repository/ht_data_repository.dart';
import 'package:ht_shared/ht_shared.dart'
show HtHttpException, User, UserContentPreferences;
import 'package:ht_shared/ht_shared.dart';

part 'account_event.dart';
part 'account_state.dart';
Expand All @@ -31,6 +31,10 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
on<AccountLoadContentPreferencesRequested>(
_onAccountLoadContentPreferencesRequested,
);
on<AccountFollowCategoryToggled>(_onFollowCategoryToggled);
on<AccountFollowSourceToggled>(_onFollowSourceToggled);
on<AccountFollowCountryToggled>(_onFollowCountryToggled);
on<AccountSaveHeadlineToggled>(_onSaveHeadlineToggled);
// Handlers for AccountSettingsNavigationRequested and
// AccountBackupNavigationRequested are typically handled in the UI layer
// (e.g., BlocListener navigating) or could emit specific states if needed.
Expand Down Expand Up @@ -90,4 +94,162 @@ class AccountBloc extends Bloc<AccountEvent, AccountState> {
);
}
}

Future<void> _persistPreferences(
UserContentPreferences preferences,
Emitter<AccountState> emit,
) async {
if (state.user == null) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'User not authenticated to save preferences.',
),
);
return;
}
try {
await _userContentPreferencesRepository.update(
id: state.user!.id, // ID of the preferences object is the user's ID
item: preferences,
userId: state.user!.id,
);
// Optimistic update already done, emit success if needed for UI feedback
// emit(state.copyWith(status: AccountStatus.success));
} on HtHttpException catch (e) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'Failed to save preferences: ${e.message}',
),
);
} catch (e) {
emit(
state.copyWith(
status: AccountStatus.failure,
errorMessage: 'An unexpected error occurred while saving: $e',
),
);
}
}

Future<void> _onFollowCategoryToggled(
AccountFollowCategoryToggled event,
Emitter<AccountState> emit,
) async {
if (state.preferences == null || state.user == null) return;

final currentPrefs = state.preferences!;
final updatedFollowedCategories = List<Category>.from(
currentPrefs.followedCategories,
);

final isCurrentlyFollowing = updatedFollowedCategories.any(
(category) => category.id == event.category.id,
);

if (isCurrentlyFollowing) {
updatedFollowedCategories.removeWhere(
(category) => category.id == event.category.id,
);
} else {
updatedFollowedCategories.add(event.category);
}

final newPreferences = currentPrefs.copyWith(
followedCategories: updatedFollowedCategories,
);
emit(state.copyWith(preferences: newPreferences));
await _persistPreferences(newPreferences, emit);
}

Future<void> _onFollowSourceToggled(
AccountFollowSourceToggled event,
Emitter<AccountState> emit,
) async {
if (state.preferences == null || state.user == null) return;

final currentPrefs = state.preferences!;
final updatedFollowedSources = List<Source>.from(
currentPrefs.followedSources,
);

final isCurrentlyFollowing = updatedFollowedSources.any(
(source) => source.id == event.source.id,
);

if (isCurrentlyFollowing) {
updatedFollowedSources.removeWhere(
(source) => source.id == event.source.id,
);
} else {
updatedFollowedSources.add(event.source);
}

final newPreferences = currentPrefs.copyWith(
followedSources: updatedFollowedSources,
);
emit(state.copyWith(preferences: newPreferences));
await _persistPreferences(newPreferences, emit);
}

Future<void> _onFollowCountryToggled(
AccountFollowCountryToggled event,
Emitter<AccountState> emit,
) async {
if (state.preferences == null || state.user == null) return;

final currentPrefs = state.preferences!;
final updatedFollowedCountries = List<Country>.from(
currentPrefs.followedCountries,
);

final isCurrentlyFollowing = updatedFollowedCountries.any(
(country) => country.id == event.country.id,
);

if (isCurrentlyFollowing) {
updatedFollowedCountries.removeWhere(
(country) => country.id == event.country.id,
);
} else {
updatedFollowedCountries.add(event.country);
}

final newPreferences = currentPrefs.copyWith(
followedCountries: updatedFollowedCountries,
);
emit(state.copyWith(preferences: newPreferences));
await _persistPreferences(newPreferences, emit);
}

Future<void> _onSaveHeadlineToggled(
AccountSaveHeadlineToggled event,
Emitter<AccountState> emit,
) async {
if (state.preferences == null || state.user == null) return;

final currentPrefs = state.preferences!;
final updatedSavedHeadlines = List<Headline>.from(
currentPrefs.savedHeadlines,
);

final isCurrentlySaved = updatedSavedHeadlines.any(
(headline) => headline.id == event.headline.id,
);

if (isCurrentlySaved) {
updatedSavedHeadlines.removeWhere(
(headline) => headline.id == event.headline.id,
);
} else {
updatedSavedHeadlines.add(event.headline);
}

final newPreferences = currentPrefs.copyWith(
savedHeadlines: updatedSavedHeadlines,
);
emit(state.copyWith(preferences: newPreferences));
await _persistPreferences(newPreferences, emit);
}
}
52 changes: 52 additions & 0 deletions lib/account/bloc/account_event.dart
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,55 @@ final class AccountLoadContentPreferencesRequested extends AccountEvent {
@override
List<Object> get props => [userId];
}

/// {@template account_follow_category_toggled}
/// Event triggered when a user toggles following a category.
/// {@endtemplate}
final class AccountFollowCategoryToggled extends AccountEvent {
/// {@macro account_follow_category_toggled}
const AccountFollowCategoryToggled({required this.category});

final Category category;

@override
List<Object> get props => [category];
}

/// {@template account_follow_source_toggled}
/// Event triggered when a user toggles following a source.
/// {@endtemplate}
final class AccountFollowSourceToggled extends AccountEvent {
/// {@macro account_follow_source_toggled}
const AccountFollowSourceToggled({required this.source});

final Source source;

@override
List<Object> get props => [source];
}

/// {@template account_follow_country_toggled}
/// Event triggered when a user toggles following a country.
/// {@endtemplate}
final class AccountFollowCountryToggled extends AccountEvent {
/// {@macro account_follow_country_toggled}
const AccountFollowCountryToggled({required this.country});

final Country country;

@override
List<Object> get props => [country];
}

/// {@template account_save_headline_toggled}
/// Event triggered when a user toggles saving a headline.
/// {@endtemplate}
final class AccountSaveHeadlineToggled extends AccountEvent {
/// {@macro account_save_headline_toggled}
const AccountSaveHeadlineToggled({required this.headline});

final Headline headline;

@override
List<Object> get props => [headline];
}
Loading
Loading