elCaribe app - customization and branding

This commit is contained in:
2025-12-12 19:09:42 -04:00
parent 9e5d0d8ebf
commit ba7deac9f3
402 changed files with 31833 additions and 0 deletions

View File

@@ -0,0 +1,251 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:intl/intl.dart';
import 'package:news/app/routes.dart';
import 'package:news/cubits/deleteUserNewsCubit.dart';
import 'package:news/cubits/getUserNewsCubit.dart';
import 'package:news/cubits/themeCubit.dart';
import 'package:news/data/models/NewsModel.dart';
import 'package:news/ui/styles/appTheme.dart';
import 'package:news/ui/styles/colors.dart';
import 'package:news/ui/widgets/SnackBarWidget.dart';
import 'package:news/ui/widgets/customTextBtn.dart';
import 'package:news/ui/widgets/customTextLabel.dart';
import 'package:news/ui/widgets/networkImage.dart';
import 'package:news/ui/screens/auth/Widgets/svgPictureWidget.dart';
import 'package:news/utils/hiveBoxKeys.dart';
import 'package:news/utils/uiUtils.dart';
class UsernewsWidgets {
static final labelKeys = {"standard_post": 'stdPostLbl', "video_youtube": 'videoYoutubeLbl', "video_other": 'videoOtherUrlLbl', "video_upload": 'videoUploadLbl'};
static buildNewsContainer(
{required BuildContext context,
required NewsModel model,
required int index,
required int totalCurrentNews,
required bool hasMoreNewsFetchError,
required bool hasMore,
required Function fetchMoreNews}) {
if (index == totalCurrentNews - 1 && index != 0) {
if (hasMore) {
if (hasMoreNewsFetchError) {
return Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 8.0),
child: IconButton(onPressed: () => fetchMoreNews, icon: Icon(Icons.error, color: Theme.of(context).primaryColor)),
));
} else {
return Center(child: Padding(padding: const EdgeInsets.symmetric(horizontal: 15.0, vertical: 8.0), child: UiUtils.showCircularProgress(true, Theme.of(context).primaryColor)));
}
}
}
return InkWell(
splashColor: Colors.transparent,
highlightColor: Colors.transparent,
onTap: () {
Navigator.of(context).pushNamed(Routes.newsDetails, arguments: {"model": model, "isFromBreak": false, "fromShowMore": false});
},
child: Container(
decoration: BoxDecoration(borderRadius: BorderRadius.circular(10.0), color: UiUtils.getColorScheme(context).surface),
padding: const EdgeInsetsDirectional.all(15),
margin: const EdgeInsets.only(top: 20),
child: SizedBox(
width: MediaQuery.of(context).size.width * 0.24,
height: MediaQuery.of(context).size.height * 0.26,
child: Column(mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
newsImage(imageURL: model.image!, context: context),
Padding(
padding: const EdgeInsets.only(left: 10.0),
child: Column(mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [
categoryName(context: context, categoryName: (model.categoryName != null && model.categoryName!.trim().isNotEmpty) ? model.categoryName! : ""),
setDate(context: context, dateValue: model.date!)
]),
),
Spacer(),
deleteAndEditButton(context: context, isEdit: true, onTap: () => Navigator.of(context).pushNamed(Routes.addNews, arguments: {"model": model, "isEdit": true, "from": "myNews"})),
deleteAndEditButton(context: context, isEdit: false, onTap: () => deleteNewsDialogue(context, model.id!, index))
],
),
Divider(thickness: 2),
Expanded(
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
CustomTextLabel(
text: model.title!,
maxLines: 3,
overflow: TextOverflow.ellipsis,
softWrap: true,
textStyle: Theme.of(context).textTheme.titleMedium!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer, fontWeight: FontWeight.w700)),
contentTypeView(context: context, model: model),
])),
Divider(thickness: 2),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
(model.isExpired == 1)
? Container(
child: Row(
children: [
SvgPictureWidget(
assetName: 'expiredNews',
assetColor:
(context.read<ThemeCubit>().state.appTheme == AppTheme.Dark ? ColorFilter.mode(darkIconColor, BlendMode.srcIn) : ColorFilter.mode(iconColor, BlendMode.srcIn))),
SizedBox(width: 2.5),
Text(
UiUtils.getTranslatedLabel(context, 'expiredKey'),
style: TextStyle(color: (context.read<ThemeCubit>().state.appTheme == AppTheme.Dark ? darkIconColor : iconColor), fontWeight: FontWeight.w500),
),
],
))
: SizedBox.shrink(),
(model.status == "0")
? Container(
padding: EdgeInsets.symmetric(vertical: 3, horizontal: 5),
child: Tooltip(
margin: const EdgeInsets.symmetric(horizontal: 20),
decoration: BoxDecoration(color: UiUtils.getColorScheme(context).primaryContainer, borderRadius: BorderRadius.circular(10)),
textStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(color: UiUtils.getColorScheme(context).secondary, fontSize: 10),
message: UiUtils.getTranslatedLabel(context, 'newsCreatedSuccessfully'),
child: Row(
children: [
SvgPictureWidget(
assetName: 'deactivatedNews',
assetColor: (context.read<ThemeCubit>().state.appTheme == AppTheme.Dark
? const ColorFilter.mode(darkIconColor, BlendMode.srcIn)
: const ColorFilter.mode(iconColor, BlendMode.srcIn))),
const SizedBox(width: 2.5),
Text(
UiUtils.getTranslatedLabel(context, 'deactivatedKey'),
style: TextStyle(color: (context.read<ThemeCubit>().state.appTheme == AppTheme.Dark ? darkIconColor : iconColor), fontWeight: FontWeight.bold),
),
],
),
))
: SizedBox.shrink(),
],
),
]),
)));
}
static Widget newsImage({required BuildContext context, required String imageURL}) {
return ClipRRect(
borderRadius: BorderRadius.circular(45),
child: CustomNetworkImage(networkImageUrl: imageURL, fit: BoxFit.cover, height: MediaQuery.of(context).size.width * 0.18, isVideo: false, width: MediaQuery.of(context).size.width * 0.18));
}
static Widget categoryName({required BuildContext context, required String categoryName}) {
return (categoryName.trim().isNotEmpty)
? Padding(
padding: const EdgeInsets.only(top: 4),
child: CustomTextLabel(
text: categoryName,
overflow: TextOverflow.ellipsis,
softWrap: true,
textStyle: Theme.of(context).textTheme.bodyLarge!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer.withOpacity(0.9), fontSize: 16, fontWeight: FontWeight.w600)),
)
: const SizedBox.shrink();
}
static Widget deleteAndEditButton({required BuildContext context, required bool isEdit, required void Function()? onTap}) {
return InkWell(
onTap: onTap,
child: Container(
padding: const EdgeInsetsDirectional.only(top: 3, bottom: 3, start: 5),
alignment: Alignment.center,
child: SvgPictureWidget(
assetName: (isEdit) ? 'editMyNews' : 'deleteMyNews',
height: 30,
width: 30,
fit: BoxFit.contain,
assetColor: (isEdit) ? ColorFilter.mode(UiUtils.getColorScheme(context).onPrimary, BlendMode.srcIn) : null),
));
}
static Widget setDate({required BuildContext context, required String dateValue}) {
DateTime time = DateTime.parse(dateValue);
var newFormat = DateFormat("dd-MMM-yyyy", Hive.box(settingsBoxKey).get(currentLanguageCodeKey));
final newNewsDate = newFormat.format(time);
return CustomTextLabel(
text: newNewsDate,
overflow: TextOverflow.ellipsis,
softWrap: true,
textStyle: Theme.of(context).textTheme.bodySmall!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer.withOpacity(0.8)));
}
static deleteNewsDialogue(BuildContext mainContext, String id, int index) async {
await showDialog(
context: mainContext,
builder: (BuildContext context) {
return StatefulBuilder(builder: (BuildContext context, StateSetter setStater) {
return AlertDialog(
backgroundColor: UiUtils.getColorScheme(context).surface,
shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5.0))),
content: CustomTextLabel(text: 'doYouReallyNewsLbl', textStyle: Theme.of(context).textTheme.titleMedium),
title: const CustomTextLabel(text: 'delNewsLbl'),
titleTextStyle: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w600),
actions: <Widget>[
CustomTextButton(
textWidget: CustomTextLabel(
text: 'noLbl', textStyle: Theme.of(context).textTheme.titleSmall?.copyWith(color: UiUtils.getColorScheme(context).primaryContainer, fontWeight: FontWeight.bold)),
onTap: () {
Navigator.of(context).pop(false);
}),
BlocConsumer<DeleteUserNewsCubit, DeleteUserNewsState>(
bloc: context.read<DeleteUserNewsCubit>(),
listener: (context, state) {
if (state is DeleteUserNewsSuccess) {
context.read<GetUserNewsCubit>().deleteNews(index);
showSnackBar(state.message, context);
Navigator.pop(context);
}
},
builder: (context, state) {
return CustomTextButton(
textWidget: CustomTextLabel(
text: 'yesLbl', textStyle: Theme.of(context).textTheme.titleSmall?.copyWith(color: UiUtils.getColorScheme(context).primaryContainer, fontWeight: FontWeight.bold)),
onTap: () async {
context.read<DeleteUserNewsCubit>().setDeleteUserNews(newsId: id);
});
})
],
);
});
});
}
static Widget contentTypeView({required BuildContext context, required NewsModel model}) {
String contType = "";
final key = labelKeys[model.contentType];
if (key != null) {
contType = UiUtils.getTranslatedLabel(context, key);
}
return (model.contentType != "")
? Padding(
padding: const EdgeInsets.only(top: 7),
child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
CustomTextLabel(
text: 'contentTypeLbl',
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: true,
textStyle: Theme.of(context).textTheme.bodyLarge!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer.withAlpha((0.3 * 255).round()))),
CustomTextLabel(text: " : ", textStyle: Theme.of(context).textTheme.bodyLarge!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer.withAlpha((0.3 * 255).round()))),
CustomTextLabel(
text: contType,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: true,
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(color: UiUtils.getColorScheme(context).primaryContainer.withAlpha((0.3 * 255).round())))
]),
)
: SizedBox.shrink();
}
}

View File

@@ -0,0 +1,157 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:news/cubits/categoryCubit.dart';
import 'package:news/cubits/locationCityCubit.dart';
import 'package:news/cubits/updateBottomsheetContentCubit.dart';
import 'package:news/cubits/tagCubit.dart';
import 'package:news/utils/uiUtils.dart';
import 'package:news/ui/widgets/customTextLabel.dart';
class CustomBottomsheet extends StatefulWidget {
final BuildContext context;
final String titleTxt;
final int listLength;
final String language_id;
final NullableIndexedWidgetBuilder listViewChild;
const CustomBottomsheet({super.key, required this.context, required this.titleTxt, required this.listLength, required this.listViewChild, required this.language_id});
@override
CustomBottomsheetState createState() => CustomBottomsheetState();
}
class CustomBottomsheetState extends State<CustomBottomsheet> {
late final ScrollController locationScrollController = ScrollController();
late final ScrollController languageScrollController = ScrollController();
late final ScrollController categoryScrollController = ScrollController();
late final ScrollController subcategoryScrollController = ScrollController();
late final ScrollController tagScrollController = ScrollController();
ScrollController scController = ScrollController();
@override
void initState() {
super.initState();
initScrollController();
}
@override
void dispose() {
disposeScrollController();
super.dispose();
}
void initScrollController() {
switch (widget.titleTxt) {
case 'chooseLanLbl':
scController = languageScrollController;
break;
case 'selCatLbl':
scController = categoryScrollController;
break;
case 'selSubCatLbl':
scController = subcategoryScrollController;
break;
case 'selTagLbl':
scController = tagScrollController;
break;
case 'selLocationLbl':
scController = locationScrollController;
break;
}
scController.addListener(() => hasMoreLocationScrollListener());
}
disposeScrollController() {
switch (widget.titleTxt) {
case 'chooseLanLbl':
languageScrollController.dispose();
break;
case 'selCatLbl':
categoryScrollController.dispose();
break;
case 'selSubCatLbl':
subcategoryScrollController.dispose();
break;
case 'selTagLbl':
tagScrollController.dispose();
break;
case 'selLocationLbl':
locationScrollController.dispose();
break;
}
}
void hasMoreLocationScrollListener() {
if (scController.offset >= scController.position.maxScrollExtent && !scController.position.outOfRange) {
switch (widget.titleTxt) {
case 'selCatLbl':
if (context.read<CategoryCubit>().hasMoreCategory()) {
context.read<CategoryCubit>().getMoreCategory(langId: widget.language_id);
}
break;
case 'selTagLbl':
if (context.read<TagCubit>().hasMoreTags()) {
context.read<TagCubit>().getMoreTags(langId: widget.language_id);
}
break;
case 'selLocationLbl':
if (context.read<LocationCityCubit>().hasMoreLocation()) {
context.read<LocationCityCubit>().getMoreLocationCity();
}
break;
}
}
}
@override
Widget build(BuildContext context) {
return Builder(
builder: (BuildContext context) => BlocBuilder<BottomSheetCubit, BottomSheetState>(
builder: (context, state) {
int listLength = widget.listLength;
switch (widget.titleTxt) {
case 'selLocationLbl':
listLength = state.locationData.length;
break;
case 'selTagLbl':
listLength = state.tagsData.length;
break;
case 'chooseLanLbl':
listLength = state.languageData.length;
break;
case 'selCatLbl':
listLength = state.categoryData.length;
}
return DraggableScrollableSheet(
snap: true,
snapSizes: const [0.5, 0.9],
expand: false,
builder: (_, controller) {
controller = scController;
return Container(
padding: const EdgeInsetsDirectional.only(bottom: 15.0, top: 15.0, start: 20.0, end: 20.0),
decoration: BoxDecoration(borderRadius: const BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)), color: UiUtils.getColorScheme(context).surface),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
CustomTextLabel(
text: widget.titleTxt,
textStyle: Theme.of(context).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.bold, color: UiUtils.getColorScheme(context).primaryContainer)),
const SizedBox(height: 10),
Expanded(
child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsetsDirectional.only(top: 10.0, bottom: 25.0),
itemCount: listLength,
itemBuilder: widget.listViewChild)),
],
));
});
},
));
}
}

View File

@@ -0,0 +1,149 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:news/utils/api.dart';
/// AI Service Module using Google Gemini API
/// This module contains all AI-related API calls for the application
class GeminiService {
static const String _geminiModel = "gemini-2.0-flash"; // Or gemini-1.5-pro
/// Helper function to call Gemini API
static Future<Map<String, dynamic>> _callGeminiAPI(String prompt, String apiKey) async {
try {
final requestBody = {
"contents": [
{
"parts": [
{"text": prompt}
]
}
]
};
final url = Uri.parse("${Api.geminiMetaInfoApi}$_geminiModel:generateContent?key=$apiKey");
final response = await http.post(
url,
headers: {"Content-Type": "application/json"},
body: jsonEncode(requestBody),
);
if (response.statusCode != 200) {
final errorData = jsonDecode(response.body);
throw Exception(errorData["error"]?["message"] ?? "Failed to generate content");
}
return jsonDecode(response.body);
} catch (e) {
rethrow;
}
}
/// Generate content
static Future<String> generateContent({
String? title,
String? category,
String? language,
String? languageCode,
required String apiKey,
}) async {
try {
String fullPrompt = "You are a skilled news article writer. Create engaging and informative content.";
if (title != null) {
fullPrompt += "\n\nWrite an article with the title: \"$title\"";
}
if (category != null) {
fullPrompt += "\nCategory: $category";
}
if (language != null && languageCode != null) {
fullPrompt += "\n\nIMPORTANT: Generate all content in $language language ($languageCode). The response MUST be in $language.";
}
fullPrompt += "\n\nRequest: \n\nArticle:";
final response = await _callGeminiAPI(fullPrompt, apiKey);
return response["candidates"][0]["content"]["parts"][0]["text"];
} catch (e) {
rethrow;
}
}
/// Generate meta info
static Future<Map<String, dynamic>> generateMetaInfo({required String title, String? language, String? languageCode, required String apiKey}) async {
try {
String languageInstruction = "";
if (language != null && languageCode != null) {
languageInstruction = "\n\nIMPORTANT: Generate all content in $language language ($languageCode). The response MUST be in same language as title.";
}
final prompt = """
You are an SEO expert. Generate meta title, description, keywords, and a slug for this news article titled: "$title".$languageInstruction
Return ONLY a JSON object with these fields:
- meta_title
- meta_description
- meta_keywords
- slug
Response must be valid JSON.
""";
final response = await _callGeminiAPI(prompt, apiKey);
final responseText = response["candidates"][0]["content"]["parts"][0]["text"].trim();
try {
return jsonDecode(responseText);
} catch (_) {
final match = RegExp(r"\{[\s\S]*\}").firstMatch(responseText);
if (match != null) {
return jsonDecode(match.group(0)!);
}
return {
"meta_title": title,
"meta_description": "Read about $title in our latest news article.",
"meta_keywords": title.toLowerCase().split(" ").join(","),
"slug": title.toLowerCase().replaceAll(RegExp(r'[^a-z0-9]+'), "-").replaceAll(RegExp(r'^-|-$'), ""),
};
}
} catch (e) {
rethrow;
}
}
/// Summarize description
static Future<String> summarizeDescription(String description, String apiKey, {String language = "English", String languageCode = "en"}) async {
try {
if (description.trim().isEmpty) return "";
final cleanContent = description.replaceAll(RegExp(r"<[^>]*>"), "").trim();
if (cleanContent.isEmpty) return "";
final prompt = """
You are a skilled content summarizer. Summarize the following news content:
Content: "$cleanContent"
Instructions:
- 200-250 words
- Maintain key facts
- Professional news style
- No explanations, only summary
- IMPORTANT: Generate in $language ($languageCode).
Summary:""";
final response = await _callGeminiAPI(prompt, apiKey);
final summary = response["candidates"][0]["content"]["parts"][0]["text"].trim();
String finalSummary = summary.replaceAll(RegExp(r"^['\']+|['\']+$"), '').trim();
return finalSummary;
} catch (e) {
return description.replaceAll(RegExp(r"<[^>]*>"), "").substring(0, 150) + "...";
}
}
}

View File

@@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:news/cubits/getUserNewsCubit.dart';
import 'package:news/ui/screens/AddEditNews/Widgets/UserNewsWidgets.dart';
import 'package:news/ui/widgets/errorContainerWidget.dart';
import 'package:news/utils/ErrorMessageKeys.dart';
import 'package:news/utils/uiUtils.dart';
class UserAllNewsTab extends StatelessWidget {
final ScrollController controller;
final Widget contentShimmer;
final Function fetchNews;
final Function fetchMoreNews;
UserAllNewsTab({super.key, required this.controller, required this.contentShimmer, required this.fetchNews, required this.fetchMoreNews});
@override
Widget build(BuildContext context) {
return BlocBuilder<GetUserNewsCubit, GetUserNewsState>(builder: (context, state) {
if (state is GetUserNewsFetchSuccess) {
return RefreshIndicator(
onRefresh: () async => fetchNews,
child: Padding(
padding: const EdgeInsetsDirectional.only(start: 10, end: 10, bottom: 10),
child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: state.getUserNews.length,
itemBuilder: (context, index) {
return UsernewsWidgets.buildNewsContainer(
context: context,
model: state.getUserNews[index],
hasMore: state.hasMore,
hasMoreNewsFetchError: state.hasMoreFetchError,
index: index,
totalCurrentNews: state.getUserNews.length,
fetchMoreNews: fetchMoreNews);
}),
),
);
}
if (state is GetUserNewsFetchFailure) {
return ErrorContainerWidget(errorMsg: (state.errorMessage.contains(ErrorMessageKeys.noInternet)) ? UiUtils.getTranslatedLabel(context, 'internetmsg') : state.errorMessage, onRetry: fetchNews);
}
return contentShimmer;
});
}
}

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:news/cubits/GetUserDraftedNewsCubit.dart';
import 'package:news/ui/screens/AddEditNews/Widgets/UserNewsWidgets.dart';
import 'package:news/ui/widgets/errorContainerWidget.dart';
import 'package:news/utils/ErrorMessageKeys.dart';
import 'package:news/utils/uiUtils.dart';
class UserDrafterNewsTab extends StatelessWidget {
final ScrollController controller;
final Widget contentShimmer;
final Function fetchDraftedNews;
final Function fetchMoreDraftedNews;
UserDrafterNewsTab({super.key, required this.controller, required this.contentShimmer, required this.fetchDraftedNews, required this.fetchMoreDraftedNews});
@override
Widget build(BuildContext context) {
return BlocBuilder<GetUserDraftedNewsCubit, GetUserDraftedNewsState>(builder: (context, state) {
if (state is GetUserDraftedNewsFetchSuccess) {
return Padding(
padding: const EdgeInsetsDirectional.only(start: 10, end: 10, bottom: 10),
child: RefreshIndicator(
onRefresh: () async => fetchDraftedNews,
child: ListView.builder(
controller: controller,
physics: const AlwaysScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: state.GetUserDraftedNews.length,
itemBuilder: (context, index) {
return UsernewsWidgets.buildNewsContainer(
context: context,
model: state.GetUserDraftedNews[index],
hasMore: state.hasMore,
hasMoreNewsFetchError: state.hasMoreFetchError,
index: index,
totalCurrentNews: state.GetUserDraftedNews.length,
fetchMoreNews: fetchMoreDraftedNews);
}),
),
);
}
if (state is GetUserDraftedNewsFetchFailure) {
return ErrorContainerWidget(
errorMsg: (state.errorMessage.contains(ErrorMessageKeys.noInternet)) ? UiUtils.getTranslatedLabel(context, 'internetmsg') : state.errorMessage, onRetry: fetchDraftedNews);
}
return contentShimmer;
});
}
}