diff --git a/BasithP/Preview after build.mp4 b/BasithP/Preview after build.mp4 new file mode 100644 index 0000000..9d7bf5d Binary files /dev/null and b/BasithP/Preview after build.mp4 differ diff --git a/BasithP/Release/app-release.apk b/BasithP/Release/app-release.apk new file mode 100644 index 0000000..654a99a Binary files /dev/null and b/BasithP/Release/app-release.apk differ diff --git a/BasithP/lib/api/news.dart b/BasithP/lib/api/news.dart new file mode 100644 index 0000000..0a3ff17 --- /dev/null +++ b/BasithP/lib/api/news.dart @@ -0,0 +1,29 @@ +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import 'apikey.dart'; +import '/models/modal_article.dart'; + +class News { + // String url = "https://newsapi.org/v2/everything?country=us&apiKey=$apiKey"; + String url = "https://newsapi.org/v2/top-headlines?country=us&apiKey=$apiKey"; + + Future> getNews() async { + var response = await http.Client().get(Uri.parse(url)); + + if (response.statusCode == 200) { + Map _json = json.decode(response.body); + List body = _json['articles']; + List
articles = []; + for (var element in body) { + try { + articles.add(Article.fromJson(element)); + } catch (_) {} + } + return articles; + } else { + throw Exception("There was an error calling the API"); + } + } +} diff --git a/BasithP/lib/config/routes.dart b/BasithP/lib/config/routes.dart new file mode 100644 index 0000000..1fee3d9 --- /dev/null +++ b/BasithP/lib/config/routes.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +import '../pages/home_page/home_page.dart'; +import '../pages/profile_page/profile_page.dart'; + +const String homePage = 'home'; +const String profilePage = 'profile'; + +Route controller(RouteSettings settings) { + switch (settings.name) { + case homePage: + return MaterialPageRoute(builder: (context) => const HomePage()); + case profilePage: + return MaterialPageRoute(builder: (context) => const ProfilePage()); + default: + throw ('This route name does not exists'); + } +} diff --git a/BasithP/lib/config/themes/theme.dart b/BasithP/lib/config/themes/theme.dart new file mode 100644 index 0000000..b5c2bba --- /dev/null +++ b/BasithP/lib/config/themes/theme.dart @@ -0,0 +1,35 @@ +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/painting.dart'; +import 'package:google_fonts/google_fonts.dart'; + +var themeData = ThemeData.dark().copyWith( + scaffoldBackgroundColor: const Color(0xff111214), + appBarTheme: AppBarTheme( + centerTitle: true, + backgroundColor: Colors.red[900], + ), + textTheme: ThemeData.dark().textTheme.copyWith( + headline3: GoogleFonts.lato( + color: const Color(0xffFEFFFF), + fontWeight: FontWeight.bold, + height: 1.4, + ), + headline5: GoogleFonts.lato( + color: const Color(0xffFEFFFF), + fontWeight: FontWeight.bold, + ), + headline6: GoogleFonts.lato( + color: Colors.grey[300], + fontWeight: FontWeight.bold, + fontSize: 18, + ), + bodyText1: GoogleFonts.lato( + color: Colors.grey, + ), + bodyText2: GoogleFonts.lato( + color: const Color(0xffFEFFFF), + fontSize: 18, + ), + ), +); diff --git a/BasithP/lib/generated_plugin_registrant.dart b/BasithP/lib/generated_plugin_registrant.dart new file mode 100644 index 0000000..438d9d4 --- /dev/null +++ b/BasithP/lib/generated_plugin_registrant.dart @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +// ignore_for_file: directives_ordering +// ignore_for_file: lines_longer_than_80_chars + +import 'package:url_launcher_web/url_launcher_web.dart'; + +import 'package:flutter_web_plugins/flutter_web_plugins.dart'; + +// ignore: public_member_api_docs +void registerPlugins(Registrar registrar) { + UrlLauncherPlugin.registerWith(registrar); + registrar.registerMessageHandler(); +} diff --git a/BasithP/lib/main.dart b/BasithP/lib/main.dart new file mode 100644 index 0000000..e3ffbce --- /dev/null +++ b/BasithP/lib/main.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'config/themes/theme.dart'; +import 'config/routes.dart' as route; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + SystemChrome.setPreferredOrientations([ + DeviceOrientation.portraitUp, + DeviceOrientation.portraitDown, + ]); + runApp(const BifNewsApp()); +} + +class BifNewsApp extends StatelessWidget { + const BifNewsApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'BiF News', + theme: themeData, + debugShowCheckedModeBanner: false, + onGenerateRoute: route.controller, + initialRoute: route.homePage, + ); + } +} diff --git a/BasithP/lib/models/modal_article.dart b/BasithP/lib/models/modal_article.dart new file mode 100644 index 0000000..ae9fbb8 --- /dev/null +++ b/BasithP/lib/models/modal_article.dart @@ -0,0 +1,97 @@ +// To parse this JSON data, do +// +// final welcome = welcomeFromJson(jsonString); + +import 'dart:convert'; + +Welcome welcomeFromJson(String str) => Welcome.fromJson(json.decode(str)); + +String welcomeToJson(Welcome data) => json.encode(data.toJson()); + +class Welcome { + Welcome({ + required this.status, + required this.totalResults, + required this.articles, + }); + + String status; + int totalResults; + List
articles; + + factory Welcome.fromJson(Map json) => Welcome( + status: json["status"], + totalResults: json["totalResults"], + articles: List
.from(json["articles"].map((x) => Article.fromJson(x))), + ); + + Map toJson() => { + "status": status, + "totalResults": totalResults, + "articles": List.from(articles.map((x) => x.toJson())), + }; +} + +class Article { + Article({ + required this.source, + required this.author, + required this.title, + required this.description, + required this.url, + required this.urlToImage, + required this.publishedAt, + required this.content, + }); + + Source source; + String author; + String title; + String description; + String url; + String urlToImage; + DateTime publishedAt; + String content; + + factory Article.fromJson(Map json) => Article( + source: Source.fromJson(json["source"]), + author: json["author"], + title: json["title"], + description: json["description"], + url: json["url"], + urlToImage: json["urlToImage"], + publishedAt: DateTime.parse(json["publishedAt"]), + content: json["content"], + ); + + Map toJson() => { + "source": source.toJson(), + "author": author, + "title": title, + "description": description, + "url": url, + "urlToImage": urlToImage, + "publishedAt": publishedAt.toIso8601String(), + "content": content, + }; +} + +class Source { + Source({ + required this.id, + required this.name, + }); + + String id; + String name; + + factory Source.fromJson(Map json) => Source( + id: json["id"], + name: json["name"], + ); + + Map toJson() => { + "id": id, + "name": name, + }; +} diff --git a/BasithP/lib/pages/home_page/home_page.dart b/BasithP/lib/pages/home_page/home_page.dart new file mode 100644 index 0000000..06ae7c8 --- /dev/null +++ b/BasithP/lib/pages/home_page/home_page.dart @@ -0,0 +1,73 @@ +import 'package:flutter/material.dart'; + +import '/config/routes.dart' as route; +import '/utils/vars.dart'; +import '/widgets/circle_avatar_with_shadow.dart'; +import 'widgets/all_news_section.dart'; + +class HomePage extends StatelessWidget { + const HomePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final mdQry = MediaQuery.of(context); + final appBar = AppBar( + leading: const Icon(Icons.search_rounded), + elevation: 0, + title: Text( + 'THE BiF NEWS', + style: appBarTextStyle, + ), + actions: [ + GestureDetector( + onTap: () => Navigator.pushNamed(context, route.profilePage), + child: const Hero( + tag: 'profilePic', + child: CircleAvatarWithShadow( + image: AssetImage('assets/images/1.png'), + radius: 15, + ), + ), + ), + const SizedBox(width: 10) + ], + ); + + final List categories = ['All', 'Business', 'Entertainment', 'Science', 'Sports', 'Technology']; + + return Scaffold( + appBar: appBar, + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 66, + child: ListView.builder( + padding: const EdgeInsets.all(15), + scrollDirection: Axis.horizontal, + physics: const BouncingScrollPhysics(), + itemCount: categories.length, + shrinkWrap: true, + itemBuilder: (ctx, index){ + return Container( + padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 15), + margin: const EdgeInsets.only(right: 10), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.withOpacity(0.4)), + borderRadius: BorderRadius.circular(50), + ), + child: Text(categories[index]), + ); + },), + ), + AllNewsSection(mdQry: mdQry), + ], + ), + ), + ), + ); + } +} diff --git a/BasithP/lib/pages/home_page/widgets/all_news_section.dart b/BasithP/lib/pages/home_page/widgets/all_news_section.dart new file mode 100644 index 0000000..f25cb10 --- /dev/null +++ b/BasithP/lib/pages/home_page/widgets/all_news_section.dart @@ -0,0 +1,72 @@ +import 'package:bif_news_app/models/modal_article.dart'; +import 'package:flutter/material.dart'; + +import 'news_card.dart'; +import '/api/news.dart'; + +class AllNewsSection extends StatefulWidget { + const AllNewsSection({ + Key? key, + required this.mdQry, + }) : super(key: key); + + final MediaQueryData mdQry; + + @override + State createState() => _AllNewsSectionState(); +} + +class _AllNewsSectionState extends State { + late Future> _article; + + @override + void initState() { + _article = News().getNews(); + super.initState(); + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(20).copyWith(bottom: 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'All stories', + style: Theme.of(context).textTheme.headline5, + ), + const SizedBox(height: 10), + FutureBuilder>( + future: _article, + builder: (context, AsyncSnapshot snapshot) { + if (snapshot.hasData) { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: snapshot.data!.length, + shrinkWrap: true, + itemBuilder: (context, index) { + var data = snapshot.data![index]!; + return NewsCard( + title: data.title, + screenWidth: widget.mdQry.size.width, + description: data.description, + url: data.url, + urlToImage: data.urlToImage, + author: data.author, + publishedAt: data.publishedAt, + ); + }, + ); + } else if (snapshot.hasError) { + return Text("${snapshot.error}"); + } else { + return const CircularProgressIndicator(); + } + }, + ), + ], + ), + ); + } +} diff --git a/BasithP/lib/pages/home_page/widgets/news_card.dart b/BasithP/lib/pages/home_page/widgets/news_card.dart new file mode 100644 index 0000000..fbc7d1e --- /dev/null +++ b/BasithP/lib/pages/home_page/widgets/news_card.dart @@ -0,0 +1,84 @@ +import 'package:flutter/material.dart'; + +class NewsCard extends StatefulWidget { + const NewsCard({ + Key? key, + required this.title, + required this.screenWidth, + required this.description, + required this.url, + required this.urlToImage, + required this.publishedAt, + required this.author, + }) : super(key: key); + + final String title; + final String description; + final String url; + final String urlToImage; + final double screenWidth; + final DateTime publishedAt; + final String author; + + @override + State createState() => _NewsCardState(); +} + +class _NewsCardState extends State { + @override + Widget build(BuildContext context) { + return Container( + height: 129, + width: widget.screenWidth, + margin: const EdgeInsets.only(bottom: 15), + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(15), + color: const Color(0xff1F2022), + ), + clipBehavior: Clip.antiAlias, + child: Row( + children: [ + SizedBox( + width: 130, + height: 139, + child: Image( + image: NetworkImage(widget.urlToImage), + fit: BoxFit.cover, + ), + ), + Padding( + padding: const EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox( + width: widget.screenWidth - 194, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.title, + style: Theme.of(context).textTheme.headline6, + maxLines: 2, + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + const SizedBox(height: 10), + Text(widget.author, + style: Theme.of(context).textTheme.bodyText2!.copyWith(color: Colors.grey[200], fontSize: 14),), + Text(widget.publishedAt.toString().substring(11, 16) + ", " + widget.publishedAt.toString().substring(0, 10), + style:Theme.of(context).textTheme.bodyText1,) + ], + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +} diff --git a/BasithP/lib/pages/profile_page/profile_page.dart b/BasithP/lib/pages/profile_page/profile_page.dart new file mode 100644 index 0000000..2c92ec9 --- /dev/null +++ b/BasithP/lib/pages/profile_page/profile_page.dart @@ -0,0 +1,139 @@ +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'widgets/list_tile_text.dart'; +import 'widgets/custom_card.dart'; +import '/widgets/circle_avatar_with_shadow.dart'; +import '/utils/vars.dart'; + +class ProfilePage extends StatelessWidget { + static const routName = '/profile'; + const ProfilePage({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + leading: IconButton( + onPressed: () { + Navigator.pop(context); + }, + icon: const Icon(Icons.arrow_back_ios_new_rounded), + ), + elevation: 0, + title: Text( + 'PROFILE', + style: appBarTextStyle, + ), + centerTitle: true, + ), + body: SafeArea( + child: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Expanded( + child: Text( + 'Hello,\nmy name', + style: Theme.of(context).textTheme.headline3, + ), + ), + const Hero( + tag: 'profilePic', + child: CircleAvatarWithShadow( + image: AssetImage('assets/images/1.png'), + radius: 70, + ), + ), + const SizedBox(width: 5), + ], + ), + Text( + 'is Basith.\nI\'m a Developer.', + style: Theme.of(context).textTheme.headline3, + ), + const SizedBox(height: 20), + buildCustomCard( + context, + Icons.sentiment_satisfied_alt_outlined, + 'About', + [ + Text( + 'I\'m a developer focused on flutter and a computer science student', + style: Theme.of(context).textTheme.bodyText2!.copyWith(height: 1.8), + ), + ], + ), + buildCustomCard( + context, + Icons.code_outlined, + 'Languages', + const [ + ListTileText('Flutter'), + ListTileText('Python'), + ListTileText('HTML'), + ListTileText('CSS'), + ], + ), + buildCustomCard( + context, + Icons.sports_soccer_outlined, + 'Hobbies', + const [ + ListTileText('Reading'), + ListTileText('Make videos'), + ], + ), + buildCustomCard( + context, + Icons.public_outlined, + 'Social', + [ + InkWell( + child: const ListTileText( + 'GitHub', + icon: Icons.call_made_rounded, + ), + onTap: () => _launchURL('https://github.com/Basith-P'), + ), + InkWell( + child: const ListTileText( + 'LinkedIn', + icon: Icons.call_made_rounded, + ), + onTap: () => _launchURL('https://www.linkedin.com/in/basithp9/'), + ), + InkWell( + child: const ListTileText( + 'Instagram', + icon: Icons.call_made_rounded, + ), + onTap: () => _launchURL('https://www.instagram.com/basith_nst/'), + ), + InkWell( + child: const ListTileText( + 'YouTube', + icon: Icons.call_made_rounded, + ), + onTap: () => _launchURL( + 'https://www.youtube.com/channel/UCe-QNDe5ywUCHE2MSY_3q7Q?sub_confirmation=1'), + ), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +_launchURL(String url) async => + await canLaunch(url) ? await launch(url) : throw 'Could not launch $url'; diff --git a/BasithP/lib/pages/profile_page/widgets/custom_card.dart b/BasithP/lib/pages/profile_page/widgets/custom_card.dart new file mode 100644 index 0000000..d1269b7 --- /dev/null +++ b/BasithP/lib/pages/profile_page/widgets/custom_card.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +Widget buildCustomCard(BuildContext context, IconData icon, String title, List children) { + return Container( + margin: const EdgeInsets.symmetric(vertical: 10), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: const Color(0xff1F2022), + borderRadius: BorderRadius.circular(20), + ), + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: ExpansionTile( + title: Row( + children: [ + Icon(icon), + const SizedBox(width: 10), + Text( + title, + style: Theme.of(context).textTheme.headline5, + ), + ], + ), + childrenPadding: const EdgeInsets.all(15).copyWith(top: 0), + children: children, + ), + ), + ); +} diff --git a/BasithP/lib/pages/profile_page/widgets/list_tile_text.dart b/BasithP/lib/pages/profile_page/widgets/list_tile_text.dart new file mode 100644 index 0000000..5dac027 --- /dev/null +++ b/BasithP/lib/pages/profile_page/widgets/list_tile_text.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; + +class ListTileText extends StatelessWidget { + final String title; + final IconData? icon; + + const ListTileText(this.title, {Key? key, this.icon}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + ListTile( + trailing: Icon( + icon, + color: Colors.white60, + ), + title: Text( + title, + style: Theme.of(context).textTheme.bodyText2, + ), + ), + Divider( + color: Colors.grey.withOpacity(0.1), + ), + ], + ); + } +} diff --git a/BasithP/lib/utils/vars.dart b/BasithP/lib/utils/vars.dart new file mode 100644 index 0000000..d7b544c --- /dev/null +++ b/BasithP/lib/utils/vars.dart @@ -0,0 +1,8 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +var appBarTextStyle = GoogleFonts.nunito( + fontWeight: FontWeight.bold, + fontSize: 24, + // letterSpacing: 0.5, +); diff --git a/BasithP/lib/widgets/circle_avatar_with_shadow.dart b/BasithP/lib/widgets/circle_avatar_with_shadow.dart new file mode 100644 index 0000000..8cea304 --- /dev/null +++ b/BasithP/lib/widgets/circle_avatar_with_shadow.dart @@ -0,0 +1,30 @@ +import 'package:flutter/material.dart'; + +class CircleAvatarWithShadow extends StatelessWidget { + final ImageProvider image; + final double radius; + + const CircleAvatarWithShadow({Key? key, required this.image, required this.radius}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(2), + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + boxShadow: [ + // BoxShadow( + // color: Colors.black, + // blurRadius: 10, + // ), + ], + ), + child: CircleAvatar( + radius: radius, + backgroundImage: image, + ), + ); + } +} diff --git a/BasithP/pubspec.yaml b/BasithP/pubspec.yaml new file mode 100644 index 0000000..fd220a5 --- /dev/null +++ b/BasithP/pubspec.yaml @@ -0,0 +1,28 @@ +name: bif_news_app +description: A new Flutter project. + +publish_to: "none" + +version: 1.0.0+1 + +environment: + sdk: ">=2.12.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + cupertino_icons: ^1.0.2 + google_fonts: ^2.1.0 + url_launcher: ^6.0.12 + http: ^0.13.4 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.0 + +flutter: + uses-material-design: true + + assets: + - assets/images/