diff --git a/.github/workflows/build-flutter.yml b/.github/workflows/build-flutter.yml index 8a3d0e07d6..7732c9d947 100644 --- a/.github/workflows/build-flutter.yml +++ b/.github/workflows/build-flutter.yml @@ -7,13 +7,14 @@ jobs: runs-on: macos-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-java@v1 + - uses: actions/checkout@v3 + - uses: actions/setup-java@v2 with: - java-version: '12.x' - - uses: subosito/flutter-action@v1 + distribution: 'zulu' + java-version: '11' + - uses: subosito/flutter-action@v2 with: - flutter-version: '2.10.4' + flutter-version: '3.3.9' - run: flutter pub get - - run: flutter analyze + - run: flutter analyze --no-fatal-infos - run: flutter build apk diff --git a/README.md b/README.md index 506e13e170..73ce81dbee 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
- +


diff --git a/android/app/build.gradle b/android/app/build.gradle index 86b65d6249..6c7287f552 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -39,6 +39,7 @@ android { targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName + vectorDrawables.useSupportLibrary = true } buildTypes { diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml index b5ab9b06a0..3120564598 100644 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ b/android/app/src/main/res/drawable-v21/launch_background.xml @@ -4,9 +4,7 @@ - - - + diff --git a/android/app/src/main/res/drawable/bugheist_logo.xml b/android/app/src/main/res/drawable/bugheist_logo.xml new file mode 100644 index 0000000000..f0a5b1773e --- /dev/null +++ b/android/app/src/main/res/drawable/bugheist_logo.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index b5ab9b06a0..3120564598 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -4,9 +4,7 @@ - - - + diff --git a/android/app/src/main/res/drawable/launch_image.png b/android/app/src/main/res/drawable/launch_image.png deleted file mode 100644 index 4a5c80c7ae..0000000000 Binary files a/android/app/src/main/res/drawable/launch_image.png and /dev/null differ diff --git a/assets/bugheist_logo.png b/assets/bugheist_logo.png deleted file mode 100644 index 0a506f169f..0000000000 Binary files a/assets/bugheist_logo.png and /dev/null differ diff --git a/assets/bugheist_logo.svg b/assets/bugheist_logo.svg new file mode 100644 index 0000000000..828c278bc0 --- /dev/null +++ b/assets/bugheist_logo.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/assets/logo_white.png b/assets/logo_white.png deleted file mode 100644 index 5a457016e1..0000000000 Binary files a/assets/logo_white.png and /dev/null differ diff --git a/assets/logo_white.svg b/assets/logo_white.svg new file mode 100644 index 0000000000..635a91fe4b --- /dev/null +++ b/assets/logo_white.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/lib/src/app.dart b/lib/src/app.dart index 114a83af5d..85ba5552dc 100644 --- a/lib/src/app.dart +++ b/lib/src/app.dart @@ -91,7 +91,7 @@ class BugHeistState extends State { primaryColor: Colors.white, visualDensity: VisualDensity.adaptivePlatformDensity, ), - home: WelcomePage(), + home: Scaffold(body: WelcomePage()), ), ), ); diff --git a/lib/src/components/appbar.dart b/lib/src/components/appbar.dart index 257536ad19..5e8f2a8cb9 100644 --- a/lib/src/components/appbar.dart +++ b/lib/src/components/appbar.dart @@ -1,12 +1,14 @@ -import 'package:bugheist/src/components/searchbar.dart'; -import 'package:bugheist/src/routes/routing.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; + +import '../routes/routing.dart'; +import '../components/searchbar.dart'; /// The app's main Appbar AppBar buildAppBar({required BuildContext context}) { return AppBar( - title: Image.asset( - 'assets/bugheist_logo.png', + title: SvgPicture.asset( + 'assets/bugheist_logo.svg', fit: BoxFit.cover, height: 30, ), diff --git a/lib/src/components/issue_intro_card.dart b/lib/src/components/issue_intro_card.dart index 3bd317936b..f56e06bcda 100644 --- a/lib/src/components/issue_intro_card.dart +++ b/lib/src/components/issue_intro_card.dart @@ -1,8 +1,9 @@ -import 'package:bugheist/src/routes/routing.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../routes/routing.dart'; import '../models/issue_model.dart'; +import '../components/issuelike.dart'; /// The card used to display issues in the list of issues on the Issue Page. class IssueCard extends StatelessWidget { @@ -35,67 +36,120 @@ class IssueCard extends StatelessWidget { children: [ Container( width: size.width, - height: 0.2 * size.height, + height: 0.214 * size.height, child: Image.network( - issue.screenshotLink!, + issue.screenshotsLink![0], fit: BoxFit.fill, ), ), Container( width: size.width, - padding: const EdgeInsets.symmetric(horizontal: 12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: const EdgeInsets.only(top: 12), - child: Text( - "Issue #${issue.id}", - overflow: TextOverflow.ellipsis, - softWrap: true, - style: GoogleFonts.ubuntu( - textStyle: TextStyle( - color: Color(0xFFDC4654), - fontSize: 17.5, + height: 0.12 * size.height, + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + child: ListTile( + // contentPadding: EdgeInsets.zero, + isThreeLine: true, + title: Text( + "Issue #${issue.id}", + overflow: TextOverflow.ellipsis, + softWrap: true, + style: GoogleFonts.ubuntu( + textStyle: TextStyle( + color: Color(0xFFDC4654), + fontSize: 17.5, + ), + fontWeight: FontWeight.bold, + ), + ), + subtitle: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.only(top: 8, bottom: 12), + child: Text( + issue.description.replaceAll("\n", " "), + overflow: TextOverflow.ellipsis, + softWrap: true, + style: GoogleFonts.aBeeZee( + textStyle: TextStyle( + fontSize: 12, + color: Color(0xFF737373), + ), ), - fontWeight: FontWeight.bold, ), ), - ), - Container( - padding: const EdgeInsets.only(top: 8, bottom: 12), - child: Text( - issue.description.replaceAll("\n", " "), + Text( + issue.created_date, overflow: TextOverflow.ellipsis, softWrap: true, style: GoogleFonts.aBeeZee( textStyle: TextStyle( - fontSize: 12, - color: Color(0xFF737373), + fontSize: 10, + color: Color(0xFFA3A3A3), ), ), ), - ), - Row( - children: [ - Container( - padding: const EdgeInsets.only(bottom: 12), - child: Text( - issue.created_date, - overflow: TextOverflow.ellipsis, - softWrap: true, - style: GoogleFonts.aBeeZee( - textStyle: TextStyle( - fontSize: 10, - color: Color(0xFFA3A3A3), - ), - ), - ), - ), - ], - ), - ], + ], + ), + trailing: IssueLikeButton( + issue: issue, + ), ), + // child: Column( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Container( + // padding: const EdgeInsets.only(top: 12), + // child: Text( + // "Issue #${issue.id}", + // overflow: TextOverflow.ellipsis, + // softWrap: true, + // style: GoogleFonts.ubuntu( + // textStyle: TextStyle( + // color: Color(0xFFDC4654), + // fontSize: 17.5, + // ), + // fontWeight: FontWeight.bold, + // ), + // ), + // ), + // Container( + // padding: const EdgeInsets.only(top: 8, bottom: 12), + // child: Text( + // issue.description.replaceAll("\n", " "), + // overflow: TextOverflow.ellipsis, + // softWrap: true, + // style: GoogleFonts.aBeeZee( + // textStyle: TextStyle( + // fontSize: 12, + // color: Color(0xFF737373), + // ), + // ), + // ), + // ), + // Row( + // children: [ + // Container( + // padding: const EdgeInsets.only(bottom: 12), + // child: Text( + // issue.created_date, + // overflow: TextOverflow.ellipsis, + // softWrap: true, + // style: GoogleFonts.aBeeZee( + // textStyle: TextStyle( + // fontSize: 10, + // color: Color(0xFFA3A3A3), + // ), + // ), + // ), + // ), + // ], + // ), + // ], + // ), ) ], ), diff --git a/lib/src/components/issueflag.dart b/lib/src/components/issueflag.dart new file mode 100644 index 0000000000..40f359250b --- /dev/null +++ b/lib/src/components/issueflag.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../models/issue_model.dart'; +import '../providers/login_provider.dart'; +import '../util/api/issues_api.dart'; +import '../util/enums/login_type.dart'; + +/// Issue flags show and toggle component. +class IssueFlagButton extends ConsumerStatefulWidget { + final Issue issue; + final Color? color; + const IssueFlagButton({ + Key? key, + this.color, + required this.issue, + }) : super(key: key); + + @override + ConsumerState createState() => + _IssueFlagButtonState(); +} + +class _IssueFlagButtonState extends ConsumerState { + late int flags; + late bool flagged; + + Future toggleIssueFlag() async { + setState(() { + if (flagged) { + flags = flags - 1; + } else { + flags = flags + 1; + } + flagged = !flagged; + }); + + try { + bool status = await IssueApiClient.toggleIssueLikes(widget.issue.id!); + if (!status) { + setState(() { + flags = widget.issue.flags!; + flagged = widget.issue.flagged!; + }); + } else { + widget.issue.likes = flags; + widget.issue.flagged = flagged; + } + } catch (e) { + print(e); + } + } + + @override + void initState() { + super.initState(); + flagged = widget.issue.flagged!; + flags = widget.issue.flags!; + } + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: () async { + if (ref.read(loginProvider.notifier).loginType == LoginType.guest) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Login to like and flag issues!"), + ), + ); + } else { + await toggleIssueFlag(); + } + }, + icon: Icon( + Icons.flag_outlined, + color: widget.color, + ), + label: Text( + "${flags}", + style: TextStyle( + color: widget.color, + ), + ), + ); + } +} diff --git a/lib/src/components/issuelike.dart b/lib/src/components/issuelike.dart new file mode 100644 index 0000000000..83e03f2e8a --- /dev/null +++ b/lib/src/components/issuelike.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../models/issue_model.dart'; +import '../util/api/issues_api.dart'; +import '../util/enums/login_type.dart'; +import '../providers/login_provider.dart'; + +/// Issue likes show and toggle component. +class IssueLikeButton extends ConsumerStatefulWidget { + final Issue issue; + final Color? color; + + const IssueLikeButton({ + Key? key, + required this.issue, + this.color, + }) : super(key: key); + + @override + ConsumerState createState() => + _IssueLikeButtonState(); +} + +class _IssueLikeButtonState extends ConsumerState { + late int likes; + late bool liked; + + Future toggleIssueLike() async { + setState(() { + if (liked) { + likes = likes - 1; + } else { + likes = likes + 1; + } + liked = !liked; + }); + + try { + bool status = await IssueApiClient.toggleIssueLikes(widget.issue.id!); + if (!status) { + setState(() { + likes = widget.issue.likes!; + liked = widget.issue.liked!; + }); + } else { + widget.issue.likes = likes; + widget.issue.liked = liked; + } + } catch (e) { + print(e); + } + } + + @override + void initState() { + super.initState(); + liked = widget.issue.liked!; + likes = widget.issue.likes!; + } + + @override + Widget build(BuildContext context) { + return TextButton.icon( + onPressed: () async { + if (ref.read(loginProvider.notifier).loginType == LoginType.guest) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text("Login to like and flag issues!"), + ), + ); + } else { + await toggleIssueLike(); + } + }, + icon: Icon( + (liked) ? Icons.favorite : Icons.favorite_border_rounded, + color: widget.color, + ), + label: Text( + "$likes", + style: TextStyle( + color: widget.color, + ), + ), + ); + } +} diff --git a/lib/src/components/searchbar.dart b/lib/src/components/searchbar.dart index b01eccb05e..28a86a0e9d 100644 --- a/lib/src/components/searchbar.dart +++ b/lib/src/components/searchbar.dart @@ -1,9 +1,9 @@ -import 'package:bugheist/src/components/issue_intro_card.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; -import '../models/issuedata_model.dart'; import '../util/api/issues_api.dart'; +import '../models/issuedata_model.dart'; +import '../components/issue_intro_card.dart'; /// The search bar of app for searching issues based on keyword. class BugHeistSearchDelegate extends SearchDelegate { diff --git a/lib/src/models/issue_model.dart b/lib/src/models/issue_model.dart index 0cd7652be8..ead212950c 100644 --- a/lib/src/models/issue_model.dart +++ b/lib/src/models/issue_model.dart @@ -14,7 +14,11 @@ class Issue { final bool isOpen; final String? userAgent; final String ocr; - final String? screenshotLink; + int? likes; + int? flags; + bool? liked; + bool? flagged; + final List? screenshotsLink; final DateTime? closedDate; final String? githubUrl; final DateTime? created; @@ -50,7 +54,11 @@ class Issue { required this.isOpen, this.userAgent, required this.ocr, - this.screenshotLink, + this.likes, + this.flags, + this.liked, + this.flagged, + this.screenshotsLink, this.closedDate, this.githubUrl, this.created, @@ -77,7 +85,11 @@ class Issue { isOpen: (responseData["status"] == "open") ? true : false, userAgent: responseData["user_agent"], ocr: responseData["ocr"], - screenshotLink: responseData["screenshot"], + likes: responseData["upvotes"], + flags: responseData["flags"], + liked: responseData["upvotted"], + flagged: responseData["flagged"], + screenshotsLink: responseData["screenshots"].cast(), closedDate: (responseData["closed_date"].runtimeType != Null) ? DateTime.parse(responseData["closed_date"]) : null, diff --git a/lib/src/models/user_model.dart b/lib/src/models/user_model.dart index 89d3c6e060..6a1975bf8f 100644 --- a/lib/src/models/user_model.dart +++ b/lib/src/models/user_model.dart @@ -42,6 +42,7 @@ class User { } } +/// The [User] instance used for anonymous login. User guestUser = User( id: 1234567890, username: "Anonymous", diff --git a/lib/src/pages/auth/forgot.dart b/lib/src/pages/auth/forgot.dart index 8940764fb7..6a8f5ef1e4 100644 --- a/lib/src/pages/auth/forgot.dart +++ b/lib/src/pages/auth/forgot.dart @@ -1,5 +1,6 @@ import 'package:bugheist/src/util/api/auth_api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; /// Page for initiating the process for recovering @@ -67,9 +68,10 @@ class _ForgotPasswordPageState extends State { vertical: 0.1 * size.height, ), child: Center( - child: Image.asset( - 'assets/logo_white.png', + child: SvgPicture.asset( + 'assets/logo_white.svg', fit: BoxFit.contain, + height: 192.0, ), ), ), diff --git a/lib/src/pages/auth/login.dart b/lib/src/pages/auth/login.dart index 2764b4073e..30a0797594 100644 --- a/lib/src/pages/auth/login.dart +++ b/lib/src/pages/auth/login.dart @@ -2,6 +2,7 @@ import 'package:bugheist/src/providers/authstate_provider.dart'; import 'package:bugheist/src/routes/routing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; /// The login page for the app. @@ -49,9 +50,10 @@ class _LoginPageState extends State { vertical: 0.075 * size.height, ), child: Center( - child: Image.asset( - 'assets/logo_white.png', + child: SvgPicture.asset( + 'assets/logo_white.svg', fit: BoxFit.contain, + height: 192.0, ), ), ), @@ -109,6 +111,7 @@ class LoginForm extends ConsumerStatefulWidget { class _LoginFormState extends ConsumerState { bool showPassword = false; bool isShowVisible = false; + bool rememberMe = false; final _formKey = GlobalKey(); late TextEditingController _userController; late TextEditingController _passwordController; @@ -123,6 +126,7 @@ class _LoginFormState extends ConsumerState { "username": username, "password": password, }, + rememberMe, parentContext, ); } @@ -212,6 +216,27 @@ class _LoginFormState extends ConsumerState { obscureText: !showPassword, ), ), + SizedBox( + height: 0.005 * widget.size.height, + ), + SizedBox( + width: 0.8 * widget.size.width, + child: Row( + children: [ + Checkbox( + value: rememberMe, + onChanged: (value) { + setState(() { + rememberMe = !rememberMe; + }); + }, + ), + Text( + "Remember me", + ), + ], + ), + ), SizedBox( height: 0.025 * widget.size.height, ), diff --git a/lib/src/pages/auth/signup.dart b/lib/src/pages/auth/signup.dart index ef24c0dabc..7f09998bd4 100644 --- a/lib/src/pages/auth/signup.dart +++ b/lib/src/pages/auth/signup.dart @@ -1,5 +1,6 @@ import 'package:bugheist/src/util/api/auth_api.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; import '../../routes/routing.dart'; @@ -49,9 +50,10 @@ class _SignUpPageState extends State { vertical: 0.085 * size.height, ), child: Center( - child: Image.asset( - 'assets/logo_white.png', + child: SvgPicture.asset( + 'assets/logo_white.svg', fit: BoxFit.contain, + height: 192.0, ), ), ), diff --git a/lib/src/pages/drawer/about.dart b/lib/src/pages/drawer/about.dart index e9df542819..fc8506afed 100644 --- a/lib/src/pages/drawer/about.dart +++ b/lib/src/pages/drawer/about.dart @@ -1,5 +1,6 @@ import 'package:bugheist/src/constants/about_constants.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; /// Page for describing the BugHeist project. @@ -28,8 +29,8 @@ class AboutPage extends StatelessWidget { children: [ Container( padding: EdgeInsets.fromLTRB(0, 36, 0, 24), - child: Image.asset( - 'assets/bugheist_logo.png', + child: SvgPicture.asset( + 'assets/bugheist_logo.svg', width: 169.42, ), ), diff --git a/lib/src/pages/drawer/legal.dart b/lib/src/pages/drawer/legal.dart index 30e6fab6ca..4f04468a6b 100644 --- a/lib/src/pages/drawer/legal.dart +++ b/lib/src/pages/drawer/legal.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_markdown/flutter_markdown.dart'; @@ -30,8 +31,8 @@ class LegalPage extends StatelessWidget { children: [ Container( padding: EdgeInsets.fromLTRB(0, 36, 0, 24), - child: Image.asset( - 'assets/bugheist_logo.png', + child: SvgPicture.asset( + 'assets/bugheist_logo.svg', width: 169.42, ), ), diff --git a/lib/src/pages/home/feed.dart b/lib/src/pages/home/feed.dart index 9ec70b9dc0..5887268136 100644 --- a/lib/src/pages/home/feed.dart +++ b/lib/src/pages/home/feed.dart @@ -1,8 +1,8 @@ -import 'package:bugheist/src/global/variables.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; -// import '../../providers/issuelist_provider.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../global/variables.dart'; /// Page for showing social activity of BugHeist, new issues, /// top premium subscribers. diff --git a/lib/src/pages/home/home.dart b/lib/src/pages/home/home.dart index 71c66aee52..8506b00424 100644 --- a/lib/src/pages/home/home.dart +++ b/lib/src/pages/home/home.dart @@ -90,6 +90,10 @@ class _HomeState extends ConsumerState { await ref.read(authStateNotifier.notifier).logout(); } + Future forgetUser() async { + await ref.read(authStateNotifier.notifier).forgetUser(); + } + @override void initState() { _selectedIndex = widget.startingIndex ?? 0; @@ -143,6 +147,7 @@ class _HomeState extends ConsumerState { // Update trhe state of the app // ... // Then close the drawer + await forgetUser(); Navigator.pushReplacementNamed( context, RouteManager.welcomePage, diff --git a/lib/src/pages/home/issues.dart b/lib/src/pages/home/issues.dart index c022ef9340..b88f0a5aa1 100644 --- a/lib/src/pages/home/issues.dart +++ b/lib/src/pages/home/issues.dart @@ -62,7 +62,7 @@ class IssuesPageState extends ConsumerState return Scaffold( body: RefreshIndicator( onRefresh: () async { - ref.refresh(issueListProvider); + ref.read(issueListProvider.notifier).refreshIssueList(); }, child: SingleChildScrollView( child: Column( @@ -109,7 +109,7 @@ class IssuesPageState extends ConsumerState if (issueList!.isEmpty) { return Center( child: Text( - "Looks like you don't have any todos :) \n Yay!", + "Looks like there aren't many bugs :) \n Yay!", textAlign: TextAlign.center, ), ); diff --git a/lib/src/pages/home/leaderboard.dart b/lib/src/pages/home/leaderboard.dart index ade8248b16..292609156f 100644 --- a/lib/src/pages/home/leaderboard.dart +++ b/lib/src/pages/home/leaderboard.dart @@ -1,10 +1,10 @@ -import 'package:bugheist/src/util/api/api.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../util/api/api.dart'; import '../../routes/routing.dart'; /// The Leaderboards dashboard page, contains the Global, diff --git a/lib/src/pages/home/profile.dart b/lib/src/pages/home/profile.dart index d1faff8b82..b4a7260095 100644 --- a/lib/src/pages/home/profile.dart +++ b/lib/src/pages/home/profile.dart @@ -1,14 +1,14 @@ -import 'package:bugheist/src/global/variables.dart'; -import 'package:bugheist/src/util/api/issues_api.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../components/issuechip.dart'; -import '../../models/issue_model.dart'; +import '../../routes/routing.dart'; +import '../../global/variables.dart'; import '../../models/user_model.dart'; +import '../../models/issue_model.dart'; +import '../../util/api/issues_api.dart'; +import '../../components/issuechip.dart'; import '../../providers/authstate_provider.dart'; -import '../../routes/routing.dart'; /// Page that displays the stats of a user registered on BugHeist, /// shows dummy data for Guest login. diff --git a/lib/src/pages/home/report_bug.dart b/lib/src/pages/home/report_bug.dart index ddcd62a5c2..2722fbf5f3 100644 --- a/lib/src/pages/home/report_bug.dart +++ b/lib/src/pages/home/report_bug.dart @@ -2,15 +2,15 @@ import 'dart:io'; import 'dart:ui'; -import 'package:bugheist/src/global/variables.dart'; -import 'package:bugheist/src/pages/home/start_hunt.dart'; -import 'package:bugheist/src/providers/login_provider.dart'; -import 'package:bugheist/src/util/api/issues_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:image_picker/image_picker.dart'; +import '../../pages/home/start_hunt.dart'; +import '../../global/variables.dart'; +import '../../providers/login_provider.dart'; +import '../../util/api/issues_api.dart'; import '../../models/issue_model.dart'; import '../../util/enums/login_type.dart'; diff --git a/lib/src/pages/issues/issue_detail.dart b/lib/src/pages/issues/issue_detail.dart index 93796de88c..5930912afa 100644 --- a/lib/src/pages/issues/issue_detail.dart +++ b/lib/src/pages/issues/issue_detail.dart @@ -1,8 +1,11 @@ -import 'package:bugheist/src/components/issuechip.dart'; -import 'package:bugheist/src/models/issue_model.dart'; import 'package:flutter/material.dart'; import 'package:google_fonts/google_fonts.dart'; +import '../../models/issue_model.dart'; +import '../../components/issuechip.dart'; +import '../../components/issueflag.dart'; +import '../../components/issuelike.dart'; + /// Popup page when an issue is clicked to be viewed. class IssueDetailPage extends StatelessWidget { static final String path = "lib/src/pages/blog/article1.dart"; @@ -28,21 +31,30 @@ class IssueDetailPage extends StatelessWidget { }, ), title: Text("Issue #${issue.id}"), + actions: [ + IssueLikeButton( + issue: issue, + color: Colors.white, + ), + IssueFlagButton( + issue: issue, + color: Colors.white, + ), + ], backgroundColor: Color(0xFFDC4654), ), body: SingleChildScrollView( + padding: EdgeInsets.all(20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.symmetric(horizontal: 20), width: size.width, color: Theme.of(context).canvasColor, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( - padding: EdgeInsets.fromLTRB(0, 12, 0, 0), child: Text( "Issue #${issue.id}", style: GoogleFonts.ubuntu( @@ -78,19 +90,18 @@ class IssueDetailPage extends StatelessWidget { ), ), Container( - height: 0.334 * size.height, width: size.width, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), ), padding: const EdgeInsets.symmetric(horizontal: 20), child: Image.network( - issue.screenshotLink!, + issue.screenshotsLink![0], fit: BoxFit.fill, ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + padding: const EdgeInsets.symmetric(vertical: 12), child: Text( "Description", overflow: TextOverflow.ellipsis, @@ -105,7 +116,6 @@ class IssueDetailPage extends StatelessWidget { ), ), Container( - padding: const EdgeInsets.symmetric(horizontal: 20), child: Text( issue.description, ), diff --git a/lib/src/pages/welcome.dart b/lib/src/pages/welcome.dart index 940a991b40..42ea761821 100644 --- a/lib/src/pages/welcome.dart +++ b/lib/src/pages/welcome.dart @@ -1,5 +1,6 @@ import 'package:bugheist/src/providers/authstate_provider.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -7,9 +8,24 @@ import '../routes/routing.dart'; /// The Landing page for unauthenticated users, or if a /// user wants to try the guest mode of the app. -class WelcomePage extends StatelessWidget { +class WelcomePage extends ConsumerStatefulWidget { const WelcomePage({Key? key}) : super(key: key); + @override + ConsumerState createState() => _WelcomePageState(); +} + +class _WelcomePageState extends ConsumerState { + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await ref.read(authStateNotifier.notifier).loadUserIfRemembered(context); + }); + } + + @override Widget build(BuildContext context) { final Size size = MediaQuery.of(context).size; @@ -32,9 +48,10 @@ class WelcomePage extends StatelessWidget { vertical: 0.1 * size.height, ), child: Center( - child: Image.asset( - 'assets/logo_white.png', + child: SvgPicture.asset( + 'assets/logo_white.svg', fit: BoxFit.contain, + height: 192.0, ), ), ), diff --git a/lib/src/providers/authstate_provider.dart b/lib/src/providers/authstate_provider.dart index 2c1c9f81de..bfaf42bd6b 100644 --- a/lib/src/providers/authstate_provider.dart +++ b/lib/src/providers/authstate_provider.dart @@ -1,6 +1,7 @@ import 'package:bugheist/src/util/api/user_api.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import './login_provider.dart'; import '../models/user_model.dart'; @@ -19,6 +20,7 @@ final authStateNotifier = class AuthNotifier extends StateNotifier> { final Reader read; AsyncValue? previousState; + final storage = new FlutterSecureStorage(); AuthNotifier(this.read, [AsyncValue? authstate]) : super( @@ -32,9 +34,63 @@ class AuthNotifier extends StateNotifier> { read(loginProvider.notifier).setGuestLogin(); } + Future loadUserIfRemembered(BuildContext context) async { + String? username = await storage.read(key: "username"); + String? password = await storage.read(key: "password"); + + if (username == null || password == null) { + return false; + } + + SnackBar authSnack = SnackBar( + content: Text("Authenticating ..."), + duration: Duration(minutes: 1), + ); + ScaffoldMessenger.of(context).showSnackBar(authSnack); + + Map userCreds = { + "username": username, + "password": password + }; + state = AsyncValue.data(AuthState.authenticating); + try { + User? authenticatedUser = await AuthApiClient.login(userCreds); + if (authenticatedUser != null) { + currentUser = authenticatedUser; + await UserApiClient.getUserInfo(currentUser!); + + state = AsyncValue.data(AuthState.loggedIn); + read(loginProvider.notifier).setUserLogin(); + Navigator.of(context).pushNamed( + RouteManager.homePage, + ); + ScaffoldMessenger.of(context).clearSnackBars(); + } + } catch (e) {} + return true; + } + + Future rememberUser( + Map userCreds + ) async { + await storage.write( + key: "username", + value: userCreds["username"], + ); + await storage.write( + key: "password", + value: userCreds["password"], + ); + } + + Future forgetUser() async { + await storage.delete(key: "username"); + await storage.delete(key: "password"); + } + /// Do a user type authentication. Future userLogin( - Map userCreds, BuildContext parentContext) async { + Map userCreds, bool rememberMe, BuildContext parentContext) async { state = AsyncValue.data(AuthState.authenticating); SnackBar authSnack = SnackBar( content: Text("Authenticating ..."), @@ -49,7 +105,9 @@ class AuthNotifier extends StateNotifier> { state = AsyncValue.data(AuthState.loggedIn); read(loginProvider.notifier).setUserLogin(); - + if (rememberMe) { + rememberUser(userCreds); + } ScaffoldMessenger.of(parentContext).clearSnackBars(); Navigator.of(parentContext).pushReplacementNamed( RouteManager.homePage, diff --git a/lib/src/providers/issuelist_provider.dart b/lib/src/providers/issuelist_provider.dart index 55f453ba84..a4d732786f 100644 --- a/lib/src/providers/issuelist_provider.dart +++ b/lib/src/providers/issuelist_provider.dart @@ -54,6 +54,20 @@ class IssueListProvider extends StateNotifier?>?> { } } + /// Function call for refreshing state. + Future refreshIssueList() async { + state = AsyncValue.loading(); + try { + final IssueData? issueData = await IssueApiClient.getAllIssues( + IssueEndPoints.issues, + ); + nxtUrl = issueData!.nextQuery; + state = AsyncValue.data(issueData.issueList); + } catch (e) { + AsyncValue.error(e); + } + } + /// Caches the current state to prevent errors. void _cacheState() { previousState = state; diff --git a/lib/src/util/api/auth_api.dart b/lib/src/util/api/auth_api.dart index c542a35b6f..da756fdf3b 100644 --- a/lib/src/util/api/auth_api.dart +++ b/lib/src/util/api/auth_api.dart @@ -5,6 +5,8 @@ import 'package:bugheist/src/util/endpoints/auth_endpoints.dart'; import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; +import '../../global/variables.dart'; + /// Class for accessing the auth client. class AuthApiClient { AuthApiClient._(); @@ -39,6 +41,9 @@ class AuthApiClient { try { response = await http.post( Uri.parse(AuthEndPoints.logout), + headers: { + "Authorization": "Token ${currentUser!.token!}", + }, ); print(response.statusCode); if (response.statusCode == 200) isLoggedOut = true; diff --git a/lib/src/util/api/issues_api.dart b/lib/src/util/api/issues_api.dart index 868ddd83a9..23577cd0aa 100644 --- a/lib/src/util/api/issues_api.dart +++ b/lib/src/util/api/issues_api.dart @@ -1,4 +1,6 @@ import 'dart:convert'; +import 'package:bugheist/src/global/variables.dart'; +import 'package:bugheist/src/models/user_model.dart'; import 'package:bugheist/src/routes/routing.dart'; import 'package:bugheist/src/util/endpoints/issue_endpoints.dart'; import 'package:flutter/material.dart'; @@ -15,8 +17,16 @@ class IssueApiClient { http.Response? response; IssueData? issueData; List? issueList; + Map? headers = (currentUser != guestUser) + ? { + "Authorization": "Token ${currentUser!.token!}", + } + : null; try { - response = await http.get(Uri.parse(paginatedUrl)); + response = await http.get( + Uri.parse(paginatedUrl), + headers: headers, + ); if (response.statusCode == 200) { issueList = []; var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)); @@ -145,6 +155,86 @@ class IssueApiClient { } } + static Future getIssueLikes(int id) async { + http.Response? response; + int? likeCount; + try { + response = await http.get( + Uri.parse(IssueEndPoints.likeIssues + "$id/"), + headers: { + "Authorization": "Token ${currentUser!.token!}", + }, + ); + if (response.statusCode == 200) { + var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)); + likeCount = decodedResponse["likes"]; + } + } catch (e) { + print(e); + } + return likeCount; + } + + static Future toggleIssueLikes(int id) async { + http.Response? response; + bool toggleSuccess = false; + try { + response = await http.post( + Uri.parse(IssueEndPoints.likeIssues + "$id/"), + headers: { + "Authorization": "Token ${currentUser!.token!}", + }, + ); + print(response.statusCode); + if (response.statusCode == 200) { + toggleSuccess = true; + } + } catch (e) { + print(e); + } + return toggleSuccess; + } + + static Future getIssueFlag(int id) async { + http.Response? response; + int? flagCount; + try { + response = await http.get( + Uri.parse(IssueEndPoints.flagIssues + "$id/"), + headers: { + "Authorization": "Token ${currentUser!.token!}", + }, + ); + if (response.statusCode == 200) { + var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)); + flagCount = decodedResponse["flags"]; + } + } catch (e) { + print("e"); + } + return flagCount; + } + + static Future toggleIssueFlag(int id) async { + http.Response? response; + bool toggleSuccess = false; + try { + response = await http.post( + Uri.parse(IssueEndPoints.flagIssues + "$id/"), + headers: { + "Authorization": "Token ${currentUser!.token!}", + }, + ); + print(response.statusCode); + if (response.statusCode == 200) { + toggleSuccess = true; + } + } catch (e) { + print(e); + } + return toggleSuccess; + } + static Future getAllUserIssues() async {} static Future getUserIssueById() async {} diff --git a/lib/src/util/endpoints/auth_endpoints.dart b/lib/src/util/endpoints/auth_endpoints.dart index 0007ae47b9..dff0170a46 100644 --- a/lib/src/util/endpoints/auth_endpoints.dart +++ b/lib/src/util/endpoints/auth_endpoints.dart @@ -2,13 +2,15 @@ class AuthEndPoints { AuthEndPoints._(); - static const String baseUrl = "https://www.bugheist.com/auth/"; + static const String baseUrl = "https://www.bugheist.com/"; - static const String emailpasswordLogin = baseUrl + "login/"; + static const String baseUrl2 = "https://www.bugheist.com/auth/"; - static const String logout = baseUrl + "logout/"; + static const String emailpasswordLogin = baseUrl + "authenticate/"; - static const String register = baseUrl + "registration/"; + static const String logout = baseUrl2 + "logout/"; - static const String reset = baseUrl + "password/reset/"; + static const String register = baseUrl2 + "registration/"; + + static const String reset = baseUrl2 + "password/reset/"; } diff --git a/lib/src/util/endpoints/issue_endpoints.dart b/lib/src/util/endpoints/issue_endpoints.dart index 550131d4b9..653e5fa3d1 100644 --- a/lib/src/util/endpoints/issue_endpoints.dart +++ b/lib/src/util/endpoints/issue_endpoints.dart @@ -4,7 +4,11 @@ class IssueEndPoints { static const String baseUrl = "https://www.bugheist.com/api/v1/"; - static const issues = baseUrl + "issues/"; + static const String issues = baseUrl + "issues/"; - static const userIssues = baseUrl + "userissues/"; + static const String userIssues = baseUrl + "userissues/"; + + static const String likeIssues = baseUrl + "issue/like/"; + + static const String flagIssues = baseUrl + "issue/flag/"; } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 69b5ae4f58..4fc759c48f 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,9 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows ) +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + set(PLUGIN_BUNDLED_LIBRARIES) foreach(plugin ${FLUTTER_PLUGIN_LIST}) @@ -14,3 +17,8 @@ foreach(plugin ${FLUTTER_PLUGIN_LIST}) list(APPEND PLUGIN_BUNDLED_LIBRARIES $) list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin)