diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index e5b82d3..e669925 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -47,5 +47,9 @@ NSLocationWhenInUseUsageDescription Your location will be used to provide weather and map data. + LSApplicationQueriesSchemes + + tel + diff --git a/lib/src/layouts/app_page.dart b/lib/src/layouts/app_page.dart new file mode 100644 index 0000000..1ecbb29 --- /dev/null +++ b/lib/src/layouts/app_page.dart @@ -0,0 +1,150 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:furman_now/src/utils/conditional_parent_widget.dart'; +import 'package:furman_now/src/utils/theme.dart'; +import 'package:furman_now/src/widgets/scroll_view_height.dart'; +import 'package:transparent_pointer/transparent_pointer.dart'; + +class AppPageLayout extends StatefulWidget { + final IconData icon; + final String title; + final Color? backgroundColor; + final Widget content; + final void Function()? iconTapAction; + final bool darkStatusBar; + + const AppPageLayout({ + required this.title, + required this.icon, + this.backgroundColor, + required this.content, + this.iconTapAction, + this.darkStatusBar = true, + Key? key, + }) : super(key: key); + + @override + State createState() => _AppPageLayoutState(); +} + +class _AppPageLayoutState extends State { + final ScrollController _controller = ScrollController(); + double overscrollBoxHeight = 0; + + @override + void initState() { + super.initState(); + + _controller.addListener(updateScrollPosition); + } + + updateScrollPosition() { + if (_controller.position.pixels > _controller.position.maxScrollExtent) { + setState(() { + overscrollBoxHeight = + _controller.position.pixels - _controller.position.maxScrollExtent; + }); + } else { + if (overscrollBoxHeight != 0) { + setState(() { + overscrollBoxHeight = 0; + }); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarColor: Colors.transparent, + systemNavigationBarContrastEnforced: true, + statusBarIconBrightness: widget.darkStatusBar + ? Brightness.dark + : Brightness.light, + statusBarBrightness: widget.darkStatusBar + ? Brightness.light + : Brightness.dark, + ), + child: Container( + color: widget.backgroundColor ?? Colors.grey[100], + child: SafeArea( + child: Stack( + fit: StackFit.loose, + children: [ + SizedBox( + width: double.infinity, + height: double.infinity, + child: Align( + alignment: Alignment.topLeft, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 30), + width: double.infinity, + height: 100, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ConditionalParentWidget( + condition: widget.iconTapAction != null, + conditionalBuilder: (child) => GestureDetector( + onTap: widget.iconTapAction!, + child: child, + ), + child: Icon( + widget.icon, + size: 35, + color: Colors.grey[700] + ), + ), + const SizedBox(width: 12), + Text( + widget.title, + style: furmanTextStyle(TextStyle( + color: Colors.grey[900], + fontSize: 28, fontWeight: FontWeight.w700 + )), + ), + ], + ), + ], + ), + ), + ), + ), + // makes the overscroll color at the bottom match + // that of the navbar + Align( + alignment: Alignment.bottomCenter, + child: Container( + height: 30 + overscrollBoxHeight, + color: Colors.grey.shade50, + ), + ), + TransparentPointer( + child: ScrollViewWithHeight( + controller: _controller, + child: Container( + decoration: const BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.all(Radius.circular(30)), + ), + // padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), + margin: const EdgeInsets.only(top: 100), + width: double.infinity, + child: widget.content, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/src/layouts/main/index.dart b/lib/src/layouts/main/index.dart index 09357c0..77c9cb6 100644 --- a/lib/src/layouts/main/index.dart +++ b/lib/src/layouts/main/index.dart @@ -41,7 +41,9 @@ class _MainLayoutState extends State { ]), MapRoute(), EventsRoute(), - InfoRoute(), + InfoPageRouter(children: [ + InfoRoute() + ]), ], bottomNavigationBuilder: (_, tabsRouter) { return WillPopScope( @@ -87,7 +89,13 @@ class _MainLayoutState extends State { currentIndex: tabsRouter.activeIndex, selectedItemColor: Theme.of(context).primaryColor, unselectedItemColor: Colors.grey[600], - onTap: tabsRouter.setActiveIndex, + onTap: (index) { + if (tabsRouter.activeIndex == index) { + // tabs + } else { + tabsRouter.setActiveIndex(index); + } + }, ), ), ); diff --git a/lib/src/routes/index.dart b/lib/src/routes/index.dart index 9ec44e9..71a4a52 100644 --- a/lib/src/routes/index.dart +++ b/lib/src/routes/index.dart @@ -3,16 +3,21 @@ import 'package:flutter/material.dart'; import 'package:furman_now/src/screens/events/index.dart'; import 'package:furman_now/src/screens/home/home_header.dart'; import 'package:furman_now/src/screens/home/index.dart'; +import 'package:furman_now/src/screens/info/contacts.dart'; +import 'package:furman_now/src/screens/info/health_safety.dart'; +import 'package:furman_now/src/screens/info/hours.dart'; import 'package:furman_now/src/screens/info/index.dart'; +import 'package:furman_now/src/screens/settings/settings.dart'; import 'package:furman_now/src/screens/map/index.dart'; import 'package:furman_now/src/screens/map/map_category.dart'; import 'package:furman_now/src/screens/map/map_home.dart'; import 'package:furman_now/src/screens/student_id/index.dart'; +import 'package:furman_now/src/utils/hero_empty_router_page.dart'; import 'package:furman_now/src/utils/translucent_route.dart'; import '../layouts/main/index.dart'; -Route mapRouteBuilder(BuildContext context, Widget child, CustomPage page){ +Route mapRouteBuilder(BuildContext context, Widget child, CustomPage page) { return TranslucentRoute( settings: page, transitionDuration: const Duration(milliseconds: 200), @@ -24,7 +29,7 @@ Route mapRouteBuilder(BuildContext context, Widget child, CustomPage pa child: child, ), ), - pageBuilder: (context) => child, + pageBuilder: (context, _, __) => child, ); } @@ -58,7 +63,13 @@ Route mapRouteBuilder(BuildContext context, Widget child, CustomPage pa ), ]), AutoRoute(path: "events", page: EventsScreen), - AutoRoute(path: "info", page: InfoScreen), + AutoRoute(path: "info", name: "InfoPageRouter", page: HeroEmptyRouterPage, children: [ + AutoRoute(path: "", page: InfoScreen), + AutoRoute(path: "health-and-safety", page: HealthSafetyScreen), + AutoRoute(path: "contacts", page: ContactsScreen), + AutoRoute(path: "hours", page: HoursScreen), + AutoRoute(path: "settings", page: SettingsScreen), + ]), ]), ], ) diff --git a/lib/src/routes/index.gr.dart b/lib/src/routes/index.gr.dart index bbb2765..61af402 100644 --- a/lib/src/routes/index.gr.dart +++ b/lib/src/routes/index.gr.dart @@ -11,115 +11,153 @@ // ignore_for_file: type=lint // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'package:auto_route/auto_route.dart' as _i10; -import 'package:flutter/material.dart' as _i11; +import 'package:auto_route/auto_route.dart' as _i15; +import 'package:flutter/material.dart' as _i16; import '../layouts/main/index.dart' as _i1; import '../screens/events/index.dart' as _i4; import '../screens/home/home_header.dart' as _i6; import '../screens/home/index.dart' as _i2; -import '../screens/info/index.dart' as _i5; +import '../screens/info/contacts.dart' as _i12; +import '../screens/info/health_safety.dart' as _i11; +import '../screens/info/hours.dart' as _i13; +import '../screens/info/index.dart' as _i10; import '../screens/map/index.dart' as _i3; import '../screens/map/map_category.dart' as _i9; import '../screens/map/map_home.dart' as _i8; -import '../screens/map/state.dart' as _i13; +import '../screens/map/state.dart' as _i18; +import '../screens/settings/settings.dart' as _i14; import '../screens/student_id/index.dart' as _i7; -import 'index.dart' as _i12; +import '../utils/hero_empty_router_page.dart' as _i5; +import 'index.dart' as _i17; -class AppRouter extends _i10.RootStackRouter { - AppRouter([_i11.GlobalKey<_i11.NavigatorState>? navigatorKey]) +class AppRouter extends _i15.RootStackRouter { + AppRouter([_i16.GlobalKey<_i16.NavigatorState>? navigatorKey]) : super(navigatorKey); @override - final Map pagesMap = { + final Map pagesMap = { MainLayout.name: (routeData) { - return _i10.MaterialPageX( + return _i15.MaterialPageX( routeData: routeData, child: const _i1.MainLayout()); }, HomePageRouter.name: (routeData) { - return _i10.MaterialPageX( + return _i15.MaterialPageX( routeData: routeData, child: const _i2.HomeScreen()); }, MapRoute.name: (routeData) { - return _i10.MaterialPageX( + return _i15.MaterialPageX( routeData: routeData, child: const _i3.MapScreen()); }, EventsRoute.name: (routeData) { - return _i10.MaterialPageX( + return _i15.MaterialPageX( routeData: routeData, child: const _i4.EventsScreen()); }, - InfoRoute.name: (routeData) { - return _i10.MaterialPageX( - routeData: routeData, child: const _i5.InfoScreen()); + InfoPageRouter.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i5.HeroEmptyRouterPage()); }, HomeRoute.name: (routeData) { - return _i10.CustomPage( + return _i15.CustomPage( routeData: routeData, child: const _i6.HomePageHeader(), - transitionsBuilder: _i10.TransitionsBuilders.fadeIn, + transitionsBuilder: _i15.TransitionsBuilders.fadeIn, opaque: true, barrierDismissible: false); }, StudentIdRoute.name: (routeData) { - return _i10.CustomPage( + return _i15.CustomPage( routeData: routeData, child: const _i7.StudentIdScreen(), - transitionsBuilder: _i10.TransitionsBuilders.fadeIn, + transitionsBuilder: _i15.TransitionsBuilders.fadeIn, opaque: true, barrierDismissible: false); }, MapHomeRoute.name: (routeData) { - return _i10.CustomPage( + return _i15.CustomPage( routeData: routeData, child: const _i8.MapHomeScreen(), - customRouteBuilder: _i12.mapRouteBuilder, + customRouteBuilder: _i17.mapRouteBuilder, opaque: true, barrierDismissible: false); }, MapCategoryRoute.name: (routeData) { final args = routeData.argsAs(); - return _i10.CustomPage( + return _i15.CustomPage( routeData: routeData, child: _i9.MapCategoryScreen(category: args.category, key: args.key), - customRouteBuilder: _i12.mapRouteBuilder, + customRouteBuilder: _i17.mapRouteBuilder, opaque: true, barrierDismissible: false); + }, + InfoRoute.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i10.InfoScreen()); + }, + HealthSafetyRoute.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i11.HealthSafetyScreen()); + }, + ContactsRoute.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i12.ContactsScreen()); + }, + HoursRoute.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i13.HoursScreen()); + }, + SettingsRoute.name: (routeData) { + return _i15.MaterialPageX( + routeData: routeData, child: const _i14.SettingsScreen()); } }; @override - List<_i10.RouteConfig> get routes => [ - _i10.RouteConfig(MainLayout.name, path: '/', children: [ - _i10.RouteConfig(HomePageRouter.name, + List<_i15.RouteConfig> get routes => [ + _i15.RouteConfig(MainLayout.name, path: '/', children: [ + _i15.RouteConfig(HomePageRouter.name, path: 'home', parent: MainLayout.name, children: [ - _i10.RouteConfig(HomeRoute.name, + _i15.RouteConfig(HomeRoute.name, path: '', parent: HomePageRouter.name), - _i10.RouteConfig(StudentIdRoute.name, + _i15.RouteConfig(StudentIdRoute.name, path: 'student-id', parent: HomePageRouter.name) ]), - _i10.RouteConfig(MapRoute.name, + _i15.RouteConfig(MapRoute.name, path: 'map', parent: MainLayout.name, children: [ - _i10.RouteConfig(MapHomeRoute.name, + _i15.RouteConfig(MapHomeRoute.name, path: '', parent: MapRoute.name), - _i10.RouteConfig(MapCategoryRoute.name, + _i15.RouteConfig(MapCategoryRoute.name, path: 'category/:id', parent: MapRoute.name) ]), - _i10.RouteConfig(EventsRoute.name, + _i15.RouteConfig(EventsRoute.name, path: 'events', parent: MainLayout.name), - _i10.RouteConfig(InfoRoute.name, - path: 'info', parent: MainLayout.name) + _i15.RouteConfig(InfoPageRouter.name, + path: 'info', + parent: MainLayout.name, + children: [ + _i15.RouteConfig(InfoRoute.name, + path: '', parent: InfoPageRouter.name), + _i15.RouteConfig(HealthSafetyRoute.name, + path: 'health-and-safety', parent: InfoPageRouter.name), + _i15.RouteConfig(ContactsRoute.name, + path: 'contacts', parent: InfoPageRouter.name), + _i15.RouteConfig(HoursRoute.name, + path: 'hours', parent: InfoPageRouter.name), + _i15.RouteConfig(SettingsRoute.name, + path: 'settings', parent: InfoPageRouter.name) + ]) ]) ]; } /// generated route for /// [_i1.MainLayout] -class MainLayout extends _i10.PageRouteInfo { - const MainLayout({List<_i10.PageRouteInfo>? children}) +class MainLayout extends _i15.PageRouteInfo { + const MainLayout({List<_i15.PageRouteInfo>? children}) : super(MainLayout.name, path: '/', initialChildren: children); static const String name = 'MainLayout'; @@ -127,8 +165,8 @@ class MainLayout extends _i10.PageRouteInfo { /// generated route for /// [_i2.HomeScreen] -class HomePageRouter extends _i10.PageRouteInfo { - const HomePageRouter({List<_i10.PageRouteInfo>? children}) +class HomePageRouter extends _i15.PageRouteInfo { + const HomePageRouter({List<_i15.PageRouteInfo>? children}) : super(HomePageRouter.name, path: 'home', initialChildren: children); static const String name = 'HomePageRouter'; @@ -136,8 +174,8 @@ class HomePageRouter extends _i10.PageRouteInfo { /// generated route for /// [_i3.MapScreen] -class MapRoute extends _i10.PageRouteInfo { - const MapRoute({List<_i10.PageRouteInfo>? children}) +class MapRoute extends _i15.PageRouteInfo { + const MapRoute({List<_i15.PageRouteInfo>? children}) : super(MapRoute.name, path: 'map', initialChildren: children); static const String name = 'MapRoute'; @@ -145,23 +183,24 @@ class MapRoute extends _i10.PageRouteInfo { /// generated route for /// [_i4.EventsScreen] -class EventsRoute extends _i10.PageRouteInfo { +class EventsRoute extends _i15.PageRouteInfo { const EventsRoute() : super(EventsRoute.name, path: 'events'); static const String name = 'EventsRoute'; } /// generated route for -/// [_i5.InfoScreen] -class InfoRoute extends _i10.PageRouteInfo { - const InfoRoute() : super(InfoRoute.name, path: 'info'); +/// [_i5.HeroEmptyRouterPage] +class InfoPageRouter extends _i15.PageRouteInfo { + const InfoPageRouter({List<_i15.PageRouteInfo>? children}) + : super(InfoPageRouter.name, path: 'info', initialChildren: children); - static const String name = 'InfoRoute'; + static const String name = 'InfoPageRouter'; } /// generated route for /// [_i6.HomePageHeader] -class HomeRoute extends _i10.PageRouteInfo { +class HomeRoute extends _i15.PageRouteInfo { const HomeRoute() : super(HomeRoute.name, path: ''); static const String name = 'HomeRoute'; @@ -169,7 +208,7 @@ class HomeRoute extends _i10.PageRouteInfo { /// generated route for /// [_i7.StudentIdScreen] -class StudentIdRoute extends _i10.PageRouteInfo { +class StudentIdRoute extends _i15.PageRouteInfo { const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id'); static const String name = 'StudentIdRoute'; @@ -177,7 +216,7 @@ class StudentIdRoute extends _i10.PageRouteInfo { /// generated route for /// [_i8.MapHomeScreen] -class MapHomeRoute extends _i10.PageRouteInfo { +class MapHomeRoute extends _i15.PageRouteInfo { const MapHomeRoute() : super(MapHomeRoute.name, path: ''); static const String name = 'MapHomeRoute'; @@ -185,8 +224,8 @@ class MapHomeRoute extends _i10.PageRouteInfo { /// generated route for /// [_i9.MapCategoryScreen] -class MapCategoryRoute extends _i10.PageRouteInfo { - MapCategoryRoute({required _i13.MapCategory category, _i11.Key? key}) +class MapCategoryRoute extends _i15.PageRouteInfo { + MapCategoryRoute({required _i18.MapCategory category, _i16.Key? key}) : super(MapCategoryRoute.name, path: 'category/:id', args: MapCategoryRouteArgs(category: category, key: key)); @@ -197,12 +236,53 @@ class MapCategoryRoute extends _i10.PageRouteInfo { class MapCategoryRouteArgs { const MapCategoryRouteArgs({required this.category, this.key}); - final _i13.MapCategory category; + final _i18.MapCategory category; - final _i11.Key? key; + final _i16.Key? key; @override String toString() { return 'MapCategoryRouteArgs{category: $category, key: $key}'; } } + +/// generated route for +/// [_i10.InfoScreen] +class InfoRoute extends _i15.PageRouteInfo { + const InfoRoute() : super(InfoRoute.name, path: ''); + + static const String name = 'InfoRoute'; +} + +/// generated route for +/// [_i11.HealthSafetyScreen] +class HealthSafetyRoute extends _i15.PageRouteInfo { + const HealthSafetyRoute() + : super(HealthSafetyRoute.name, path: 'health-and-safety'); + + static const String name = 'HealthSafetyRoute'; +} + +/// generated route for +/// [_i12.ContactsScreen] +class ContactsRoute extends _i15.PageRouteInfo { + const ContactsRoute() : super(ContactsRoute.name, path: 'contacts'); + + static const String name = 'ContactsRoute'; +} + +/// generated route for +/// [_i13.HoursScreen] +class HoursRoute extends _i15.PageRouteInfo { + const HoursRoute() : super(HoursRoute.name, path: 'hours'); + + static const String name = 'HoursRoute'; +} + +/// generated route for +/// [_i14.SettingsScreen] +class SettingsRoute extends _i15.PageRouteInfo { + const SettingsRoute() : super(SettingsRoute.name, path: 'settings'); + + static const String name = 'SettingsRoute'; +} diff --git a/lib/src/screens/events/index.dart b/lib/src/screens/events/index.dart index bff7bac..5f6dd95 100644 --- a/lib/src/screens/events/index.dart +++ b/lib/src/screens/events/index.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; import 'package:furman_now/src/utils/date_range.dart'; import 'package:furman_now/src/utils/theme.dart'; import 'package:furman_now/src/widgets/header.dart'; @@ -11,98 +13,56 @@ class EventsScreen extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - body: Container( - color: Colors.grey[100], - child: SafeArea( - child: Stack( - fit: StackFit.loose, - children: [ - SizedBox( - width: double.infinity, - height: double.infinity, - child: Align( - alignment: Alignment.topLeft, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 30), - width: double.infinity, - height: 100, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Icon(Icons.calendar_month_outlined, size: 35, color: Colors.grey[700]), - const SizedBox(width: 12), - Text("Events", style: furmanTextStyle(TextStyle(color: Colors.grey[900], fontSize: 28, fontWeight: FontWeight.w700))), - ], - ), - ], - ), + return AppPageLayout( + title: "Events", + icon: FlutterRemix.calendar_line, + content: Padding( + padding: const EdgeInsets.symmetric(vertical: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const HeaderWidget(title: "Today"), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: EventsList() + ), + const HeaderWidget(title: "Tomorrow"), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: EventsList(dateRange: constructDateRange( + DateTime.now().add(const Duration(days: 1)), + DateTime.now().add(const Duration(days: 1)), + )), + ), + ...[for(var i=2; i<7; i+=1) i].map((i) { + var date = DateTime.now().add(Duration(days: i)); + var dayName = DateFormat('EEEE').format(date); + return Wrap( + children: [ + HeaderWidget(title: dayName), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: EventsList(dateRange: constructDateRange( + date, + date, + )), ), - ), - ), - ScrollViewWithHeight( - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(30)), - ), - padding: const EdgeInsets.symmetric(vertical: 20), - margin: const EdgeInsets.only(top: 100), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const HeaderWidget(title: "Today"), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: EventsList() - ), - const HeaderWidget(title: "Tomorrow"), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: EventsList(dateRange: constructDateRange( - DateTime.now().add(const Duration(days: 1)), - DateTime.now().add(const Duration(days: 1)), - )), - ), - ...[for(var i=2; i<7; i+=1) i].map((i) { - var date = DateTime.now().add(Duration(days: i)); - var dayName = DateFormat('EEEE').format(date); - return Wrap( - children: [ - HeaderWidget(title: dayName), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: EventsList(dateRange: constructDateRange( - date, - date, - )), - ), - ], - ); - }), - Center(child: - Wrap( - direction: Axis.vertical, - crossAxisAlignment: WrapCrossAlignment.center, - children: const [ - Text("Need more events?"), - Text("Syncdin"), - Text("Athletics"), - Text("CLPs"), - ], - ), - ), - ], - ), - ), - ), - ], - ), + ], + ); + }), + Center(child: + Wrap( + direction: Axis.vertical, + crossAxisAlignment: WrapCrossAlignment.center, + children: const [ + Text("Need more events?"), + Text("Syncdin"), + Text("Athletics"), + Text("CLPs"), + ], + ), + ), + ], ), ), ); diff --git a/lib/src/screens/info/contacts.dart b/lib/src/screens/info/contacts.dart new file mode 100644 index 0000000..44ce728 --- /dev/null +++ b/lib/src/screens/info/contacts.dart @@ -0,0 +1,63 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; +import 'package:furman_now/src/services/info/contacts_service.dart'; +import 'package:furman_now/src/widgets/info/info_card.dart'; + +class ContactsScreen extends StatelessWidget { + const ContactsScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppPageLayout( + title: "Contacts", + icon: FlutterRemix.arrow_left_line, + backgroundColor: Colors.deepPurple.shade50, + iconTapAction: () => context.router.pop(), + content: Padding( + padding: const EdgeInsets.all(20), + child: FutureBuilder>( + future: ContactsService.fetchContactInfo(), + builder: (context, snapshot) { + if (snapshot.hasData) { + var items = snapshot.data!; + return Column( + children: items.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 15), + child: InfoCard( + title: item.name, + icon: item.buildingId != 0 + ? FlutterRemix.building_line + : FlutterRemix.contacts_line, + items: [ + InfoCardItem( + name: item.phone, + icon: FlutterRemix.phone_line + ), + ], + onClick: () => item.launch(), + ), + ); + }).toList()); + } + else if (snapshot.hasError) { + return Text( + '${snapshot.error}', + style: const TextStyle(color: Colors.red) + ); + } + // By default, show a loading spinner. + return const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: CircularProgressIndicator() + ), + ); + }, + ), + ) + ); + } +} diff --git a/lib/src/screens/info/health_safety.dart b/lib/src/screens/info/health_safety.dart new file mode 100644 index 0000000..ac90fa8 --- /dev/null +++ b/lib/src/screens/info/health_safety.dart @@ -0,0 +1,82 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; +import 'package:furman_now/src/services/info/health_safety_service.dart'; +import 'package:furman_now/src/widgets/info/info_card.dart'; + +class HealthSafetyScreen extends StatelessWidget { + const HealthSafetyScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppPageLayout( + title: "Health and Safety", + icon: FlutterRemix.arrow_left_line, + backgroundColor: Colors.red.shade50, + iconTapAction: () => context.router.pop(), + content: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Center( + child: Padding( + padding: const EdgeInsets.only(top: 15, bottom: 35), + child: Text( + "If you are in an emergency, dial 911", + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: Colors.red.shade800, + ), + ), + ), + ), + FutureBuilder>( + future: HealthSafetyService.fetchHealthSafetyInfo(), + builder: (context, snapshot) { + if (snapshot.hasData) { + var items = snapshot.data!.where((item) => + item.type != HealthSafetyInfoType.removed + ); + var iconMap = { + HealthSafetyInfoType.phone: FlutterRemix.phone_line, + HealthSafetyInfoType.link: FlutterRemix.external_link_line, + HealthSafetyInfoType.app: FlutterRemix.apps_line, + }; + return Column( + children: items.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 15), + child: InfoCard( + title: item.name, + icon: item.icon, + items: [ + InfoCardItem( + name: item.linkText, + icon: iconMap[item.type] ?? FlutterRemix.link + ), + ], + onClick: () => item.launch(), + ), + ); + }).toList()); + } + else if (snapshot.hasError) { + return Text( + '${snapshot.error}', style: const TextStyle(color: Colors.red),); + } + // By default, show a loading spinner. + return const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: CircularProgressIndicator() + ), + ); + }, + ), + ], + ), + ) + ); + } +} diff --git a/lib/src/screens/info/hours.dart b/lib/src/screens/info/hours.dart new file mode 100644 index 0000000..eb6d7e4 --- /dev/null +++ b/lib/src/screens/info/hours.dart @@ -0,0 +1,72 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; +import 'package:furman_now/src/services/buildings/buildings_service.dart'; +import 'package:furman_now/src/store/index.dart'; +import 'package:furman_now/src/widgets/info/info_card.dart'; +import 'package:provider/provider.dart'; + +class HoursScreen extends StatelessWidget { + const HoursScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AppPageLayout( + title: "Hours", + icon: FlutterRemix.arrow_left_line, + backgroundColor: Colors.blue.shade50, + iconTapAction: () => context.router.pop(), + content: Padding( + padding: const EdgeInsets.all(20), + child: Consumer( + builder: (context, state, _) { + return FutureBuilder>( + future: state.buildings, + builder: (context, snapshot) { + if (snapshot.hasData) { + var items = snapshot.data!.where( + (building) => building.hoursList.isNotEmpty + ); + return Column( + children: items.map((item) { + return Padding( + padding: const EdgeInsets.only(bottom: 15), + child: InfoCard( + title: item.name, + icon: FlutterRemix.building_line, + items: item.hoursList.map((hours) => + InfoCardItem( + name: "" + "${hours.daysOfWeek} from " + "${hours.startTime.format(context)} - " + "${hours.endTime.format(context)}", + icon: FlutterRemix.time_line, + ), + ).toList(), + ), + ); + }).toList(), + ); + } + else if (snapshot.hasError) { + return Text( + '${snapshot.error}', + style: const TextStyle(color: Colors.red), + ); + } + // By default, show a loading spinner. + return const Center( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 25), + child: CircularProgressIndicator() + ), + ); + }, + ); + } + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/screens/info/index.dart b/lib/src/screens/info/index.dart index 84f8ea3..d5e5fd5 100644 --- a/lib/src/screens/info/index.dart +++ b/lib/src/screens/info/index.dart @@ -1,84 +1,64 @@ +import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; -import 'package:furman_now/src/utils/theme.dart'; -import 'package:furman_now/src/widgets/info/info_card.dart'; -import 'package:furman_now/src/widgets/scroll_view_height.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; +import 'package:furman_now/src/routes/index.gr.dart'; +import 'package:furman_now/src/widgets/info/info_category_card.dart'; class InfoScreen extends StatelessWidget { const InfoScreen({Key? key}) : super(key: key); @override Widget build(BuildContext context) { - return Scaffold( - body: Container( - color: Colors.grey[100], - child: SafeArea( - child: Stack( - fit: StackFit.loose, - children: [ - SizedBox( - width: double.infinity, - height: double.infinity, - child: Align( - alignment: Alignment.topLeft, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 30), - width: double.infinity, - height: 100, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - Icon(Icons.info_outline, size: 35, color: Colors.grey[700]), - const SizedBox(width: 12), - Text("Info", style: furmanTextStyle(TextStyle(color: Colors.grey[900], fontSize: 28, fontWeight: FontWeight.w700))), - ], - ), - ], - ), - ), - ), + return AppPageLayout( + title: "Info", + icon: FlutterRemix.information_line, + content: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + GestureDetector( + onTap: () => context.router.push(const HealthSafetyRoute()), + child: InfoCategoryCard( + color: Colors.red.shade50, + icon: Icons.local_hospital, + title: "Health and Safety", + description: "Important contact information and links regarding student health and safety.", ), - ScrollViewWithHeight( - child: Container( - decoration: const BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.vertical(top: Radius.circular(30)), - ), - padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), - margin: const EdgeInsets.only(top: 100), - width: double.infinity, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - InfoCard( - color: Colors.red.shade50, - icon: Icons.local_hospital, - title: "Health and Safety", - description: "Important contact information and links regarding student health and safety.", - ), - const SizedBox(height: 10), - InfoCard( - color: Colors.deepPurple.shade50, - icon: Icons.phone, - title: "Contacts", - description: "Important contact information and links regarding student health and safety.", - ), - const SizedBox(height: 10), - InfoCard( - color: Colors.blue.shade50, - icon: Icons.access_time, - title: "Hours", - description: "Important contact information and links regarding student health and safety.", - ), - ], - ), - ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => context.router.push(const ContactsRoute()), + child: InfoCategoryCard( + color: Colors.deepPurple.shade50, + icon: Icons.phone, + title: "Contacts", + description: "Contact information for offices and resources on Furman's campus.", ), - ], - ), + ), + const SizedBox(height: 10), + GestureDetector( + onTap: () => context.router.push(const HoursRoute()), + child: InfoCategoryCard( + color: Colors.blue.shade50, + icon: Icons.access_time, + title: "Hours", + description: "Check the hours for buildings and services on Furman's campus.", + ), + ), + const SizedBox(height: 10), + const Spacer(), + GestureDetector( + onTap: () => context.router.push(const SettingsRoute()), + child: InfoCategoryCard( + color: Colors.grey.shade100, + icon: Icons.settings, + title: "Settings", + description: "Account settings, GET login, dark mode", + ), + ), + ], ), ), ); diff --git a/lib/src/screens/settings/settings.dart b/lib/src/screens/settings/settings.dart new file mode 100644 index 0000000..b549f8d --- /dev/null +++ b/lib/src/screens/settings/settings.dart @@ -0,0 +1,42 @@ +import 'package:auto_route/auto_route.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:furman_now/src/layouts/app_page.dart'; +import 'package:furman_now/src/services/get_app/user/user_service.dart'; + +class SettingsScreen extends StatefulWidget { + const SettingsScreen({Key? key}) : super(key: key); + + @override + State createState() => _SettingsScreenState(); +} + +class _SettingsScreenState extends State { + final UserService _service = UserService(); + + @override + Widget build(BuildContext context) { + return AppPageLayout( + title: "Settings", + icon: FlutterRemix.arrow_left_line, + backgroundColor: Colors.grey.shade100, + iconTapAction: () => context.router.pop(), + content: Padding( + padding: const EdgeInsets.all(20), + child: Column( + children: [ + GestureDetector( + onTap: () { + _service.login(); + }, + child: Container( + padding: EdgeInsets.all(20), + child: Text("Log in with GET app."), + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/src/services/buildings/buildings_service.dart b/lib/src/services/buildings/buildings_service.dart new file mode 100644 index 0000000..503339f --- /dev/null +++ b/lib/src/services/buildings/buildings_service.dart @@ -0,0 +1,151 @@ +import 'dart:convert'; + +import 'package:flutter/material.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:http/http.dart' as http; + +class BuildingsService { + static Future> fetchBuildings() async { + var buildings = (await _fetchBuildings()).toList(); + await _fetchBuildingHours(buildings); + return buildings; + } + + static Future> _fetchBuildings() async { + final response = await http + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/buildingGet.php")); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + final buildingsJson = jsonDecode(response.body); + return (buildingsJson["results"] as List).map((json) => + Building.fromJson(json) + ); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load buildings.'); + } + } + + static Future _fetchBuildingHours(List buildings) async { + final response = await http + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/hoursGet.php")); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + final hoursListJson = jsonDecode(response.body); + for (var hoursJson in (hoursListJson["results"] as List)) { + try { + var hours = BuildingHours.fromJson(hoursJson); + buildings.firstWhere((building) => building.id == hours.id).hoursList.add(hours); + } on ArgumentError { + // if json parse fails, just move on + // this is terribly hacky but the server is a mess I cannot fix + } + for (var building in buildings) { building.hoursList.sort(); } + } + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load building hours.'); + } + } +} + +class Building { + int id; + String name; + String? shortName; + String category; + String? location; + LatLng mapLocation; + int frequency; + List hoursList = []; + + Building({ + required this.id, + required this.name, + this.shortName, + required this.category, + this.location, + required this.mapLocation, + required this.frequency, + }); + + // bool get isOpen { + // for(var hours in hoursList) { + // var now = DateTime.now(); + // var today = Weekday.values[now.weekday - 1]; + // if (hours.daysOfWeek.days[today] == true) { + // var currentTime = Duration(hours: now.hour, minutes: now.minute, seconds: now.second); + // if(hours.startTime != null && hours.endTime != null) { + // if (hours.startTime! < currentTime && hours.endTime! > currentTime) { + // return true; + // } + // } + // } + // } + // return false; + // } + + factory Building.fromJson(Map json) { + return Building( + id: int.parse(json["buildingID"]), + name: json["name"], + shortName: json["nickname"], + category: json["category"], + location: json["location"], + mapLocation: LatLng(double.parse(json["latitude"]), double.parse(json["longitude"])), + frequency: int.parse(json["frequency"]), + ); + } +} + +class BuildingHours implements Comparable { + final int id; + final String? meal; + final TimeOfDay startTime; + final TimeOfDay endTime; + final String daysOfWeek; + final int dayOrder; + + BuildingHours({ + required this.id, + this.meal, + required this.startTime, + required this.endTime, + required this.daysOfWeek, + required this.dayOrder, + }); + + static TimeOfDay _getDurationFromString(String s) { + var timeComponents = s.split(":"); + var hour = int.parse(timeComponents[0]); + var minute = int.parse(timeComponents[1]); + + return TimeOfDay(hour: hour, minute: minute); + } + + factory BuildingHours.fromJson(Map json) { + if (json["Start"] == null || json["End"] == null) { + throw ArgumentError("Start and end time cannot be null"); + } + + return BuildingHours( + id: int.parse(json["buildingID"]), + meal: json["meal"], + startTime: _getDurationFromString(json["Start"]), + endTime: _getDurationFromString(json["End"]), + daysOfWeek: json["day"], + dayOrder: int.parse(json["dayorder"]), + ); + } + + @override + int compareTo(BuildingHours other) { + return dayOrder - other.dayOrder; + } +} diff --git a/lib/src/services/days_of_week.dart b/lib/src/services/days_of_week.dart new file mode 100644 index 0000000..f248e4f --- /dev/null +++ b/lib/src/services/days_of_week.dart @@ -0,0 +1,77 @@ +class DaysOfWeek { + final Weekday startDay; + final Weekday endDay; + final Map days; + + DaysOfWeek({ + required this.startDay, + required this.endDay, + required this.days, + }); + + static Weekday _getDayFromShortName(String shortName) { + switch(shortName.toLowerCase()) { + case "mon": + return Weekday.monday; + case "tue": + return Weekday.tuesday; + case "wed": + return Weekday.wednesday; + case "thu": + return Weekday.thursday; + case "fri": + return Weekday.friday; + case "sat": + return Weekday.saturday; + case "sun": + return Weekday.sunday; + default: + throw "Invalid date short name."; + } + } + + factory DaysOfWeek.parse(String s) { + final Map days = { for (var e in Weekday.values) e : false }; + + // single day + if (!s.contains("-")) { + var day = _getDayFromShortName(s); + days.update(day, (val) => true); + return DaysOfWeek(startDay: day, endDay: day, days: days); + } + + // date range + var dayNames = s.split("-"); + final startDay = _getDayFromShortName(dayNames[0]); + final endDay = _getDayFromShortName(dayNames[1]); + int i = startDay.index; + + // loop through to add all dates in date range to list + while(i != endDay.index) { + days.update(Weekday.values[i], (val) => true); + + if (i < Weekday.values.length - 1) { + i++; + } else { + i = 0; + } + } + days.update(endDay, (val) => true); + + return DaysOfWeek( + startDay: startDay, + endDay: endDay, + days: days, + ); + } +} + +enum Weekday { + monday, + tuesday, + wednesday, + thursday, + friday, + saturday, + sunday, +} diff --git a/lib/src/services/get_app/user/user_service.dart b/lib/src/services/get_app/user/user_service.dart index 7f26ba3..a4190b9 100644 --- a/lib/src/services/get_app/user/user_service.dart +++ b/lib/src/services/get_app/user/user_service.dart @@ -1,3 +1,78 @@ -class UserService { +import 'dart:convert'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; +import 'package:furman_now/src/services/get_app/user/in_app_browser.dart'; + +import 'package:http/http.dart' as http; + +class UserService { + final String servicesUrl = "https://services.get.cbord.com/GETServices/services/json"; + final GetLoginInAppBrowser browser = GetLoginInAppBrowser(); + + void login() { + var options = InAppBrowserClassOptions( + crossPlatform: InAppBrowserOptions( + hideUrlBar: true, + ), + ); + + browser.loadStart.subscribe((args) async { + if (args!.url != null) { + bool success = await _getAuthSessionFromUrl(args.url!); + print(success); + if (success) { + browser.close(); + } + } + }); + + browser.openUrlRequest( + urlRequest: URLRequest( + url: Uri.parse("https://get.cbord.com/furman/full/login.php?mobileapp=1"), + ), + options: options, + ); + } + + Future _getAuthSessionFromUrl(Uri url) async { + print(url); + if ( + url.origin == "https://get.cbord.com" && + url.path.contains("mobileapp_login_validator.php") + ) { + var sessionId = url.queryParameters["sessionId"]; + if (sessionId != null) { + return _validateSession(sessionId); + } + } + return false; + } + + Future _validateSession(String sessionId) async { + final response = await http.post( + Uri.parse("$servicesUrl/user"), + headers: { + "Content-Type": "application/json", + }, + body: """{ + "method": "retrieve", + "params": { + "sessionId": "$sessionId" + } + }""" + ); + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + final json = jsonDecode(response.body); + print(json); + // make sure there wasn't an exception and the data was actually loaded + return json["exception"] == null; + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + print(response.statusCode); + throw Exception('Failed to load user info.'); + } + } } \ No newline at end of file diff --git a/lib/src/services/info/contacts_service.dart b/lib/src/services/info/contacts_service.dart new file mode 100644 index 0000000..dc4c747 --- /dev/null +++ b/lib/src/services/info/contacts_service.dart @@ -0,0 +1,73 @@ +import 'dart:convert'; + +import 'package:external_app_launcher/external_app_launcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class ContactsService { + static Future> fetchContactInfo() async { + final response = await http + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/contactsGet.php")); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + final healthSafetyJson = jsonDecode(response.body); + return (healthSafetyJson["results"] as List).map((json) => + ContactInfo.fromJson(json) + ); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load contact info.'); + } + } +} + +class ContactInfo { + int id; + int buildingId; + String name; + String? room; + String number; + + ContactInfo({ + required this.id, + required this.buildingId, + required this.name, + this.room, + required this.number, + }); + + String get phone { + return number.replaceAllMapped( + RegExp(r'(\d{3})(\d{3})(\d+)'), + (Match m) => "(${m[1]}) ${m[2]}-${m[3]}" + ); + } + + void launch() async { + var url = Uri(scheme: "tel", path: number); + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + } else { + throw "Could not launch $url"; + } + } + + factory ContactInfo.fromJson(Map json) { + return ContactInfo( + id: int.parse(json["id"]), + buildingId: int.parse(json["buildingID"]), + name: json["name"], + room: (json["room"] as String).isNotEmpty ? json["room"] : null, + number: json["number"], + ); + } +} + diff --git a/lib/src/services/info/health_safety_service.dart b/lib/src/services/info/health_safety_service.dart new file mode 100644 index 0000000..583dd08 --- /dev/null +++ b/lib/src/services/info/health_safety_service.dart @@ -0,0 +1,126 @@ +import 'dart:convert'; + +import 'package:external_app_launcher/external_app_launcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_remix/flutter_remix.dart'; +import 'package:http/http.dart' as http; +import 'package:url_launcher/url_launcher.dart'; + +class HealthSafetyService { + static Future> fetchHealthSafetyInfo() async { + final response = await http + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/healthSafetyGet.php")); + + if (response.statusCode == 200) { + // If the server did return a 200 OK response, + // then parse the JSON. + final healthSafetyJson = jsonDecode(response.body); + return (healthSafetyJson["results"] as List).map((json) => + HealthSafetyInfo.fromJson(json) + ); + } else { + // If the server did not return a 200 OK response, + // then throw an exception. + throw Exception('Failed to load health and safety info.'); + } + } +} + +class HealthSafetyInfo { + int id; + String name; + HealthSafetyInfoType type; + String content; + IconData icon; + + HealthSafetyInfo({ + required this.id, + required this.name, + required this.type, + required this.content, + required this.icon, + }); + + String get linkText { + switch(type) { + case HealthSafetyInfoType.link: + return "External Link"; + case HealthSafetyInfoType.app: + return "Open App"; + case HealthSafetyInfoType.phone: + return content.replaceAllMapped( + RegExp(r'(\d{3})(\d{3})(\d+)'), + (Match m) => "(${m[1]}) ${m[2]}-${m[3]}" + ); + case HealthSafetyInfoType.removed: + return "Removed"; + } + } + + void launch() async { + if( + type == HealthSafetyInfoType.link || + type == HealthSafetyInfoType.phone + ) { + var url = (type == HealthSafetyInfoType.link) + ? Uri.parse(content) + : Uri(scheme: "tel", path: content); + if (await canLaunchUrl(url)) { + await launchUrl( + url, + mode: LaunchMode.externalApplication, + ); + } else { + throw "Could not launch $url"; + } + } else if (type == HealthSafetyInfoType.app) { + await LaunchApp.openApp( + androidPackageName: content.split("https://")[1], + iosUrlScheme: 'pulsesecure://', + appStoreLink: 'itms-apps://itunes.apple.com/us/app/pulse-secure/id945832041', + ); + } + } + + factory HealthSafetyInfo.fromJson(Map json) { + getTypeFromString(String typeName) { + switch(typeName) { + case "phone": + return HealthSafetyInfoType.phone; + case "link": + return HealthSafetyInfoType.link; + case "app": + return HealthSafetyInfoType.app; + case "removed": + return HealthSafetyInfoType.removed; + default: + throw "Invalid health safety info type value \"$typeName\""; + } + } + + Map iconMap = { + "shield": FlutterRemix.shield_star_line, + "staroflife": FlutterRemix.star_line, + "car": FlutterRemix.car_line, + "bandage.fill": Icons.healing, + "person.circle": FlutterRemix.user_line, + "heart.circle": FlutterRemix.heart_line, + }; + + return HealthSafetyInfo( + id: int.parse(json["id"]), + name: json["name"], + type: getTypeFromString(json["type"]), + content: json["content"], + icon: iconMap[json["icon"]] ?? FlutterRemix.heart_line, + ); + } +} + +enum HealthSafetyInfoType { + app, + phone, + link, + removed, +} + diff --git a/lib/src/services/restaurants/restaurant_service.dart b/lib/src/services/restaurants/restaurant_service.dart index 7998d77..4a62009 100644 --- a/lib/src/services/restaurants/restaurant_service.dart +++ b/lib/src/services/restaurants/restaurant_service.dart @@ -1,8 +1,10 @@ import 'dart:convert'; import 'package:flutter/painting.dart'; +import 'package:furman_now/src/services/days_of_week.dart'; import 'package:latlong2/latlong.dart'; import 'package:http/http.dart' as http; +import 'package:palette_generator/palette_generator.dart'; class RestaurantService { static Future> fetchRestaurants() async { @@ -13,7 +15,7 @@ class RestaurantService { static Future> _fetchRestaurants() async { final response = await http - .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantGet.php")); + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantGet.php")); if (response.statusCode == 200) { // If the server did return a 200 OK response, @@ -25,20 +27,20 @@ class RestaurantService { } else { // If the server did not return a 200 OK response, // then throw an exception. - throw Exception('Failed to load athletics events.'); + throw Exception('Failed to load restaurants.'); } } static Future _fetchRestaurantHours(List restaurants) async { final response = await http - .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantHoursGet.php")); + .get(Uri.parse("https://cs.furman.edu/~csdaemon/FUNow/restaurantHoursGet.php")); if (response.statusCode == 200) { // If the server did return a 200 OK response, // then parse the JSON. final hoursListJson = jsonDecode(response.body); for (var hoursJson in (hoursListJson["results"] as List)) { - var hours = Hours.fromJson(hoursJson); + var hours = RestaurantHours.fromJson(hoursJson); restaurants.firstWhere((restaurant) => restaurant.id == hours.id).hoursList.add(hours); } } else { @@ -58,7 +60,8 @@ class Restaurant { int frequency; int busyness; String? url; - List hoursList = []; + List hoursList = []; + Color? backgroundColor; Restaurant({ required this.id, @@ -96,6 +99,18 @@ class Restaurant { return NetworkImage(imageUri); } + Future _generateColor() async { + var paletteGenerator = await PaletteGenerator.fromImageProvider( + image, + maximumColorCount: 1, + ); + if (paletteGenerator.colors.toList().isNotEmpty) { + var hslColor = HSLColor.fromColor(paletteGenerator.colors.toList()[0]); + var newColor = HSLColor.fromAHSL(1, hslColor.hue, 40/100, 90/100); + backgroundColor = newColor.toColor(); + } + } + factory Restaurant.fromJson(Map json) { return Restaurant( id: int.parse(json["id"]), @@ -110,7 +125,7 @@ class Restaurant { } } -class Hours { +class RestaurantHours { int id; String? meal; Duration? startTime; @@ -118,7 +133,7 @@ class Hours { DaysOfWeek daysOfWeek; int dayOrder; - Hours({ + RestaurantHours({ required this.id, this.meal, this.startTime, @@ -136,8 +151,8 @@ class Hours { return Duration(hours: hours, minutes: minutes, seconds: seconds); } - factory Hours.fromJson(Map json) { - return Hours( + factory RestaurantHours.fromJson(Map json) { + return RestaurantHours( id: int.parse(json["id"]), meal: json["meal"], startTime: (json["start"] != null) ? _getDurationFromString(json["start"]) : null, @@ -147,81 +162,3 @@ class Hours { ); } } - -class DaysOfWeek { - final Weekday startDay; - final Weekday endDay; - final Map days; - - DaysOfWeek({ - required this.startDay, - required this.endDay, - required this.days, - }); - - static Weekday _getDayFromShortName(String shortName) { - switch(shortName.toLowerCase()) { - case "mon": - return Weekday.monday; - case "tue": - return Weekday.tuesday; - case "wed": - return Weekday.wednesday; - case "thu": - return Weekday.thursday; - case "fri": - return Weekday.friday; - case "sat": - return Weekday.saturday; - case "sun": - return Weekday.sunday; - default: - throw "Invalid date short name."; - } - } - - factory DaysOfWeek.parse(String s) { - final Map days = { for (var e in Weekday.values) e : false }; - - // single day - if (!s.contains("-")) { - var day = _getDayFromShortName(s); - days.update(day, (val) => true); - return DaysOfWeek(startDay: day, endDay: day, days: days); - } - - // date range - var dayNames = s.split("-"); - final startDay = _getDayFromShortName(dayNames[0]); - final endDay = _getDayFromShortName(dayNames[1]); - int i = startDay.index; - - // loop through to add all dates in date range to list - while(i != endDay.index) { - days.update(Weekday.values[i], (val) => true); - - if (i < Weekday.values.length - 1) { - i++; - } else { - i = 0; - } - } - days.update(endDay, (val) => true); - - return DaysOfWeek( - startDay: startDay, - endDay: endDay, - days: days, - ); - } -} - -enum Weekday { - monday, - tuesday, - wednesday, - thursday, - friday, - saturday, - sunday, -} diff --git a/lib/src/store/index.dart b/lib/src/store/index.dart index 69080bf..c81e5ea 100644 --- a/lib/src/store/index.dart +++ b/lib/src/store/index.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:furman_now/src/services/buildings/buildings_service.dart'; import 'package:furman_now/src/services/events/event.dart'; import 'package:furman_now/src/services/events/events_service.dart'; import 'package:furman_now/src/services/restaurants/restaurant_service.dart'; @@ -6,6 +7,7 @@ import 'package:furman_now/src/services/restaurants/restaurant_service.dart'; class AppState extends ChangeNotifier { late Future> events; late Future> restaurants; + late Future> buildings; AppState() { refresh(); @@ -14,20 +16,22 @@ class AppState extends ChangeNotifier { void refresh() { events = EventsService.fetchEvents(); restaurants = RestaurantService.fetchRestaurants(); + buildings = BuildingsService.fetchBuildings(); notifyListeners(); } @override int get hashCode => - events.hashCode ^ - restaurants.hashCode; + events.hashCode ^ + restaurants.hashCode ^ + buildings.hashCode; @override bool operator ==(Object other) => - identical(this, other) || - other is AppState && - runtimeType == other.runtimeType && - events == other.events && - restaurants == other.restaurants; - + identical(this, other) || + other is AppState && + runtimeType == other.runtimeType && + events == other.events && + restaurants == other.restaurants && + buildings == other.buildings; } \ No newline at end of file diff --git a/lib/src/utils/conditional_parent_widget.dart b/lib/src/utils/conditional_parent_widget.dart new file mode 100644 index 0000000..9c0c69b --- /dev/null +++ b/lib/src/utils/conditional_parent_widget.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; + +/// Conditionally wrap a subtree with a parent widget without breaking the code tree. +/// +/// [condition]: the condition depending on which the subtree [child] is wrapped with the parent. +/// [child]: The subtree that should always be build. +/// [conditionalBuilder]: builds the parent with the subtree [child]. +/// +/// ___________ +/// Usage: +/// ```dart +/// return ConditionalParentWidget( +/// condition: shouldIncludeParent, +/// child: Widget1( +/// child: Widget2( +/// child: Widget3(), +/// ), +/// ), +/// conditionalBuilder: (Widget child) => SomeParentWidget(child: child), +///); +/// ``` +/// +/// ___________ +/// Instead of: +/// ```dart +/// Widget child = Widget1( +/// child: Widget2( +/// child: Widget3(), +/// ), +/// ); +/// +/// return shouldIncludeParent ? SomeParentWidget(child: child) : child; +/// ``` +/// +class ConditionalParentWidget extends StatelessWidget { + const ConditionalParentWidget({ + required this.child, + required this.condition, + required this.conditionalBuilder, + Key? key, + }) : super(key: key); + + final Widget child; + final bool condition; + final Widget Function(Widget child) conditionalBuilder; + + @override + Widget build(BuildContext context) { + return condition ? conditionalBuilder(child) : child; + } +} diff --git a/lib/src/utils/theme.dart b/lib/src/utils/theme.dart index adc98f5..3c062ec 100644 --- a/lib/src/utils/theme.dart +++ b/lib/src/utils/theme.dart @@ -8,26 +8,44 @@ ThemeData _baseTheme = ThemeData( unselectedItemColor: Colors.grey[500], ), textTheme: TextTheme( - headline1: TextStyle( + headlineLarge: TextStyle( color: Colors.grey[800], fontWeight: FontWeight.w900, fontSize: 22, ), - subtitle1: TextStyle( + titleLarge: TextStyle( + color: Colors.grey[800], + fontSize: 18, + fontWeight: FontWeight.w700, + ), + titleSmall: TextStyle( + color: Colors.grey[800], + fontWeight: FontWeight.w700, + ), + labelLarge: TextStyle( color: Colors.grey[500], fontWeight: FontWeight.w500, fontSize: 16, ), - subtitle2: TextStyle( + labelMedium: TextStyle( color: Colors.grey[500], fontWeight: FontWeight.w500, fontSize: 14, ), ), + pageTransitionsTheme: const PageTransitionsTheme( + builders: { + TargetPlatform.android: + CupertinoPageTransitionsBuilder(), + TargetPlatform.iOS: + CupertinoPageTransitionsBuilder(), + }, + ), ); ThemeData myFurmanTheme = _baseTheme.copyWith( textTheme: GoogleFonts.interTextTheme(_baseTheme.textTheme), ); -var furmanTextStyle = (TextStyle baseStyle) => GoogleFonts.inter(textStyle: baseStyle); +var furmanTextStyle = (TextStyle baseStyle) => + GoogleFonts.inter(textStyle: baseStyle); diff --git a/lib/src/widgets/info/info_card.dart b/lib/src/widgets/info/info_card.dart index f8cb714..1a04851 100644 --- a/lib/src/widgets/info/info_card.dart +++ b/lib/src/widgets/info/info_card.dart @@ -1,46 +1,76 @@ import 'package:flutter/material.dart'; +import 'package:furman_now/src/utils/conditional_parent_widget.dart'; +import 'package:furman_now/src/widgets/text_with_icon.dart'; + +class InfoCardItem { + final String name; + final IconData icon; + + InfoCardItem({ + required this.name, + required this.icon, + }); +} class InfoCard extends StatelessWidget { - final Color color; - final IconData icon; final String title; - final String description; + final IconData icon; + final List items; + final void Function()? onClick; const InfoCard({ - required this.color, - required this.icon, required this.title, - required this.description, + required this.icon, + required this.items, + this.onClick, Key? key }) : super(key: key); @override Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(10), + return ConditionalParentWidget( + condition: onClick != null, + conditionalBuilder: (child) => GestureDetector( + onTap: onClick, + child: child, ), - child: Row( - children: [ - Padding( + child: Container( + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Padding( padding: const EdgeInsets.only(left: 15, right: 5), - child: Icon(icon, size: 40,) - ), - Flexible( - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(title, style: Theme.of(context).textTheme.titleLarge,), - Text(description), - ] + child: Icon( + icon, + size: 40, + color: Colors.grey.shade800, ), ), - ) - ], + Flexible( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(title, style: Theme.of(context).textTheme.titleSmall), + const SizedBox(height: 6), + ...(items.map((item) => + TextWithIcon( + icon: item.icon, + text: item.name, + size: TextWithIconSize.small, + ) + ).toList()), + ] + ), + ), + ) + ], + ), ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/info/info_category_card.dart b/lib/src/widgets/info/info_category_card.dart new file mode 100644 index 0000000..28bb1db --- /dev/null +++ b/lib/src/widgets/info/info_category_card.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'package:furman_now/src/utils/theme.dart'; + +class InfoCategoryCard extends StatelessWidget { + final Color color; + final IconData icon; + final String title; + final String description; + + const InfoCategoryCard({ + required this.color, + required this.icon, + required this.title, + required this.description, + Key? key + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(10), + ), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 15, right: 5), + child: Icon( + icon, + size: 40, + color: Colors.grey.shade800, + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 15, + vertical: 12, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: Theme.of(context).textTheme.titleLarge + ), + const SizedBox(height: 1), + Text( + description, + style: furmanTextStyle(TextStyle( + color: Colors.grey.shade700, + )), + ), + ], + ), + ), + ) + ], + ), + ); + } +} \ No newline at end of file diff --git a/lib/src/widgets/scroll_view_height.dart b/lib/src/widgets/scroll_view_height.dart index 5758b66..4084690 100644 --- a/lib/src/widgets/scroll_view_height.dart +++ b/lib/src/widgets/scroll_view_height.dart @@ -13,18 +13,23 @@ class ScrollViewWithHeight extends StatelessWidget { @override Widget build(BuildContext context) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { - return SingleChildScrollView( + return CustomScrollView( controller: controller, physics: const BouncingScrollPhysics( - parent: AlwaysScrollableScrollPhysics() + parent: AlwaysScrollableScrollPhysics(), ), - child: ConstrainedBox( - constraints: constraints.copyWith( - minHeight: constraints.maxHeight, - maxHeight: double.infinity + slivers: [ + SliverFillRemaining( + hasScrollBody: false, + child: ConstrainedBox( + constraints: constraints.copyWith( + minHeight: constraints.maxHeight, + maxHeight: double.infinity + ), + child: child, + ), ), - child: child, - ), + ], ); }); } diff --git a/lib/src/widgets/text_with_icon.dart b/lib/src/widgets/text_with_icon.dart new file mode 100644 index 0000000..cd7cfe3 --- /dev/null +++ b/lib/src/widgets/text_with_icon.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; + +enum TextWithIconSize { + small, + large, +} + +class TextWithIcon extends StatelessWidget { + final IconData icon; + final String text; + final TextWithIconSize size; + + const TextWithIcon({ + super.key, + required this.icon, + required this.text, + this.size = TextWithIconSize.large, + }); + + @override + Widget build(BuildContext context) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(right: 5.0), + child: Icon( + icon, + size: size == TextWithIconSize.large ? 24 : 20, + color: Colors.grey[500], + ), + ), + Flexible( + child: Text( + text, + style: size == TextWithIconSize.large + ? Theme.of(context).textTheme.labelLarge + : Theme.of(context).textTheme.labelMedium, + ), + ), + ], + ); + } +}