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,
+ ),
+ ),
+ ],
+ );
+ }
+}