wip: info page and GET app login / settings

This commit is contained in:
Michael Thomas 2022-11-18 15:56:37 -05:00
parent 4a698e61d2
commit 67efdc0289
24 changed files with 1461 additions and 355 deletions

View File

@ -47,5 +47,9 @@
<true/> <true/>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Your location will be used to provide weather and map data.</string> <string>Your location will be used to provide weather and map data.</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>tel</string>
</array>
</dict> </dict>
</plist> </plist>

View File

@ -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<AppPageLayout> createState() => _AppPageLayoutState();
}
class _AppPageLayoutState extends State<AppPageLayout> {
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<SystemUiOverlayStyle>(
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,
),
),
),
],
),
),
),
),
);
}
}

View File

@ -41,7 +41,9 @@ class _MainLayoutState extends State<MainLayout> {
]), ]),
MapRoute(), MapRoute(),
EventsRoute(), EventsRoute(),
InfoRoute(), InfoPageRouter(children: [
InfoRoute()
]),
], ],
bottomNavigationBuilder: (_, tabsRouter) { bottomNavigationBuilder: (_, tabsRouter) {
return WillPopScope( return WillPopScope(
@ -87,7 +89,13 @@ class _MainLayoutState extends State<MainLayout> {
currentIndex: tabsRouter.activeIndex, currentIndex: tabsRouter.activeIndex,
selectedItemColor: Theme.of(context).primaryColor, selectedItemColor: Theme.of(context).primaryColor,
unselectedItemColor: Colors.grey[600], unselectedItemColor: Colors.grey[600],
onTap: tabsRouter.setActiveIndex, onTap: (index) {
if (tabsRouter.activeIndex == index) {
// tabs
} else {
tabsRouter.setActiveIndex(index);
}
},
), ),
), ),
); );

View File

@ -3,16 +3,21 @@ import 'package:flutter/material.dart';
import 'package:furman_now/src/screens/events/index.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/home_header.dart';
import 'package:furman_now/src/screens/home/index.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/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/index.dart';
import 'package:furman_now/src/screens/map/map_category.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/map/map_home.dart';
import 'package:furman_now/src/screens/student_id/index.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 'package:furman_now/src/utils/translucent_route.dart';
import '../layouts/main/index.dart'; import '../layouts/main/index.dart';
Route<T> mapRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> page){ Route<T> mapRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> page) {
return TranslucentRoute( return TranslucentRoute(
settings: page, settings: page,
transitionDuration: const Duration(milliseconds: 200), transitionDuration: const Duration(milliseconds: 200),
@ -24,7 +29,7 @@ Route<T> mapRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> pa
child: child, child: child,
), ),
), ),
pageBuilder: (context) => child, pageBuilder: (context, _, __) => child,
); );
} }
@ -58,7 +63,13 @@ Route<T> mapRouteBuilder<T>(BuildContext context, Widget child, CustomPage<T> pa
), ),
]), ]),
AutoRoute(path: "events", page: EventsScreen), 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),
]),
]), ]),
], ],
) )

View File

@ -11,115 +11,153 @@
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: no_leading_underscores_for_library_prefixes // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'package:auto_route/auto_route.dart' as _i10; import 'package:auto_route/auto_route.dart' as _i15;
import 'package:flutter/material.dart' as _i11; import 'package:flutter/material.dart' as _i16;
import '../layouts/main/index.dart' as _i1; import '../layouts/main/index.dart' as _i1;
import '../screens/events/index.dart' as _i4; import '../screens/events/index.dart' as _i4;
import '../screens/home/home_header.dart' as _i6; import '../screens/home/home_header.dart' as _i6;
import '../screens/home/index.dart' as _i2; 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/index.dart' as _i3;
import '../screens/map/map_category.dart' as _i9; import '../screens/map/map_category.dart' as _i9;
import '../screens/map/map_home.dart' as _i8; 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 '../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 { class AppRouter extends _i15.RootStackRouter {
AppRouter([_i11.GlobalKey<_i11.NavigatorState>? navigatorKey]) AppRouter([_i16.GlobalKey<_i16.NavigatorState>? navigatorKey])
: super(navigatorKey); : super(navigatorKey);
@override @override
final Map<String, _i10.PageFactory> pagesMap = { final Map<String, _i15.PageFactory> pagesMap = {
MainLayout.name: (routeData) { MainLayout.name: (routeData) {
return _i10.MaterialPageX<dynamic>( return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i1.MainLayout()); routeData: routeData, child: const _i1.MainLayout());
}, },
HomePageRouter.name: (routeData) { HomePageRouter.name: (routeData) {
return _i10.MaterialPageX<dynamic>( return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i2.HomeScreen()); routeData: routeData, child: const _i2.HomeScreen());
}, },
MapRoute.name: (routeData) { MapRoute.name: (routeData) {
return _i10.MaterialPageX<dynamic>( return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i3.MapScreen()); routeData: routeData, child: const _i3.MapScreen());
}, },
EventsRoute.name: (routeData) { EventsRoute.name: (routeData) {
return _i10.MaterialPageX<dynamic>( return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i4.EventsScreen()); routeData: routeData, child: const _i4.EventsScreen());
}, },
InfoRoute.name: (routeData) { InfoPageRouter.name: (routeData) {
return _i10.MaterialPageX<dynamic>( return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i5.InfoScreen()); routeData: routeData, child: const _i5.HeroEmptyRouterPage());
}, },
HomeRoute.name: (routeData) { HomeRoute.name: (routeData) {
return _i10.CustomPage<dynamic>( return _i15.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i6.HomePageHeader(), child: const _i6.HomePageHeader(),
transitionsBuilder: _i10.TransitionsBuilders.fadeIn, transitionsBuilder: _i15.TransitionsBuilders.fadeIn,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
StudentIdRoute.name: (routeData) { StudentIdRoute.name: (routeData) {
return _i10.CustomPage<dynamic>( return _i15.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i7.StudentIdScreen(), child: const _i7.StudentIdScreen(),
transitionsBuilder: _i10.TransitionsBuilders.fadeIn, transitionsBuilder: _i15.TransitionsBuilders.fadeIn,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
MapHomeRoute.name: (routeData) { MapHomeRoute.name: (routeData) {
return _i10.CustomPage<dynamic>( return _i15.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: const _i8.MapHomeScreen(), child: const _i8.MapHomeScreen(),
customRouteBuilder: _i12.mapRouteBuilder, customRouteBuilder: _i17.mapRouteBuilder,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
}, },
MapCategoryRoute.name: (routeData) { MapCategoryRoute.name: (routeData) {
final args = routeData.argsAs<MapCategoryRouteArgs>(); final args = routeData.argsAs<MapCategoryRouteArgs>();
return _i10.CustomPage<dynamic>( return _i15.CustomPage<dynamic>(
routeData: routeData, routeData: routeData,
child: _i9.MapCategoryScreen(category: args.category, key: args.key), child: _i9.MapCategoryScreen(category: args.category, key: args.key),
customRouteBuilder: _i12.mapRouteBuilder, customRouteBuilder: _i17.mapRouteBuilder,
opaque: true, opaque: true,
barrierDismissible: false); barrierDismissible: false);
},
InfoRoute.name: (routeData) {
return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i10.InfoScreen());
},
HealthSafetyRoute.name: (routeData) {
return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i11.HealthSafetyScreen());
},
ContactsRoute.name: (routeData) {
return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i12.ContactsScreen());
},
HoursRoute.name: (routeData) {
return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i13.HoursScreen());
},
SettingsRoute.name: (routeData) {
return _i15.MaterialPageX<dynamic>(
routeData: routeData, child: const _i14.SettingsScreen());
} }
}; };
@override @override
List<_i10.RouteConfig> get routes => [ List<_i15.RouteConfig> get routes => [
_i10.RouteConfig(MainLayout.name, path: '/', children: [ _i15.RouteConfig(MainLayout.name, path: '/', children: [
_i10.RouteConfig(HomePageRouter.name, _i15.RouteConfig(HomePageRouter.name,
path: 'home', path: 'home',
parent: MainLayout.name, parent: MainLayout.name,
children: [ children: [
_i10.RouteConfig(HomeRoute.name, _i15.RouteConfig(HomeRoute.name,
path: '', parent: HomePageRouter.name), path: '', parent: HomePageRouter.name),
_i10.RouteConfig(StudentIdRoute.name, _i15.RouteConfig(StudentIdRoute.name,
path: 'student-id', parent: HomePageRouter.name) path: 'student-id', parent: HomePageRouter.name)
]), ]),
_i10.RouteConfig(MapRoute.name, _i15.RouteConfig(MapRoute.name,
path: 'map', path: 'map',
parent: MainLayout.name, parent: MainLayout.name,
children: [ children: [
_i10.RouteConfig(MapHomeRoute.name, _i15.RouteConfig(MapHomeRoute.name,
path: '', parent: MapRoute.name), path: '', parent: MapRoute.name),
_i10.RouteConfig(MapCategoryRoute.name, _i15.RouteConfig(MapCategoryRoute.name,
path: 'category/:id', parent: MapRoute.name) path: 'category/:id', parent: MapRoute.name)
]), ]),
_i10.RouteConfig(EventsRoute.name, _i15.RouteConfig(EventsRoute.name,
path: 'events', parent: MainLayout.name), path: 'events', parent: MainLayout.name),
_i10.RouteConfig(InfoRoute.name, _i15.RouteConfig(InfoPageRouter.name,
path: 'info', parent: MainLayout.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 /// generated route for
/// [_i1.MainLayout] /// [_i1.MainLayout]
class MainLayout extends _i10.PageRouteInfo<void> { class MainLayout extends _i15.PageRouteInfo<void> {
const MainLayout({List<_i10.PageRouteInfo>? children}) const MainLayout({List<_i15.PageRouteInfo>? children})
: super(MainLayout.name, path: '/', initialChildren: children); : super(MainLayout.name, path: '/', initialChildren: children);
static const String name = 'MainLayout'; static const String name = 'MainLayout';
@ -127,8 +165,8 @@ class MainLayout extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i2.HomeScreen] /// [_i2.HomeScreen]
class HomePageRouter extends _i10.PageRouteInfo<void> { class HomePageRouter extends _i15.PageRouteInfo<void> {
const HomePageRouter({List<_i10.PageRouteInfo>? children}) const HomePageRouter({List<_i15.PageRouteInfo>? children})
: super(HomePageRouter.name, path: 'home', initialChildren: children); : super(HomePageRouter.name, path: 'home', initialChildren: children);
static const String name = 'HomePageRouter'; static const String name = 'HomePageRouter';
@ -136,8 +174,8 @@ class HomePageRouter extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i3.MapScreen] /// [_i3.MapScreen]
class MapRoute extends _i10.PageRouteInfo<void> { class MapRoute extends _i15.PageRouteInfo<void> {
const MapRoute({List<_i10.PageRouteInfo>? children}) const MapRoute({List<_i15.PageRouteInfo>? children})
: super(MapRoute.name, path: 'map', initialChildren: children); : super(MapRoute.name, path: 'map', initialChildren: children);
static const String name = 'MapRoute'; static const String name = 'MapRoute';
@ -145,23 +183,24 @@ class MapRoute extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i4.EventsScreen] /// [_i4.EventsScreen]
class EventsRoute extends _i10.PageRouteInfo<void> { class EventsRoute extends _i15.PageRouteInfo<void> {
const EventsRoute() : super(EventsRoute.name, path: 'events'); const EventsRoute() : super(EventsRoute.name, path: 'events');
static const String name = 'EventsRoute'; static const String name = 'EventsRoute';
} }
/// generated route for /// generated route for
/// [_i5.InfoScreen] /// [_i5.HeroEmptyRouterPage]
class InfoRoute extends _i10.PageRouteInfo<void> { class InfoPageRouter extends _i15.PageRouteInfo<void> {
const InfoRoute() : super(InfoRoute.name, path: 'info'); 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 /// generated route for
/// [_i6.HomePageHeader] /// [_i6.HomePageHeader]
class HomeRoute extends _i10.PageRouteInfo<void> { class HomeRoute extends _i15.PageRouteInfo<void> {
const HomeRoute() : super(HomeRoute.name, path: ''); const HomeRoute() : super(HomeRoute.name, path: '');
static const String name = 'HomeRoute'; static const String name = 'HomeRoute';
@ -169,7 +208,7 @@ class HomeRoute extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i7.StudentIdScreen] /// [_i7.StudentIdScreen]
class StudentIdRoute extends _i10.PageRouteInfo<void> { class StudentIdRoute extends _i15.PageRouteInfo<void> {
const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id'); const StudentIdRoute() : super(StudentIdRoute.name, path: 'student-id');
static const String name = 'StudentIdRoute'; static const String name = 'StudentIdRoute';
@ -177,7 +216,7 @@ class StudentIdRoute extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i8.MapHomeScreen] /// [_i8.MapHomeScreen]
class MapHomeRoute extends _i10.PageRouteInfo<void> { class MapHomeRoute extends _i15.PageRouteInfo<void> {
const MapHomeRoute() : super(MapHomeRoute.name, path: ''); const MapHomeRoute() : super(MapHomeRoute.name, path: '');
static const String name = 'MapHomeRoute'; static const String name = 'MapHomeRoute';
@ -185,8 +224,8 @@ class MapHomeRoute extends _i10.PageRouteInfo<void> {
/// generated route for /// generated route for
/// [_i9.MapCategoryScreen] /// [_i9.MapCategoryScreen]
class MapCategoryRoute extends _i10.PageRouteInfo<MapCategoryRouteArgs> { class MapCategoryRoute extends _i15.PageRouteInfo<MapCategoryRouteArgs> {
MapCategoryRoute({required _i13.MapCategory category, _i11.Key? key}) MapCategoryRoute({required _i18.MapCategory category, _i16.Key? key})
: super(MapCategoryRoute.name, : super(MapCategoryRoute.name,
path: 'category/:id', path: 'category/:id',
args: MapCategoryRouteArgs(category: category, key: key)); args: MapCategoryRouteArgs(category: category, key: key));
@ -197,12 +236,53 @@ class MapCategoryRoute extends _i10.PageRouteInfo<MapCategoryRouteArgs> {
class MapCategoryRouteArgs { class MapCategoryRouteArgs {
const MapCategoryRouteArgs({required this.category, this.key}); const MapCategoryRouteArgs({required this.category, this.key});
final _i13.MapCategory category; final _i18.MapCategory category;
final _i11.Key? key; final _i16.Key? key;
@override @override
String toString() { String toString() {
return 'MapCategoryRouteArgs{category: $category, key: $key}'; return 'MapCategoryRouteArgs{category: $category, key: $key}';
} }
} }
/// generated route for
/// [_i10.InfoScreen]
class InfoRoute extends _i15.PageRouteInfo<void> {
const InfoRoute() : super(InfoRoute.name, path: '');
static const String name = 'InfoRoute';
}
/// generated route for
/// [_i11.HealthSafetyScreen]
class HealthSafetyRoute extends _i15.PageRouteInfo<void> {
const HealthSafetyRoute()
: super(HealthSafetyRoute.name, path: 'health-and-safety');
static const String name = 'HealthSafetyRoute';
}
/// generated route for
/// [_i12.ContactsScreen]
class ContactsRoute extends _i15.PageRouteInfo<void> {
const ContactsRoute() : super(ContactsRoute.name, path: 'contacts');
static const String name = 'ContactsRoute';
}
/// generated route for
/// [_i13.HoursScreen]
class HoursRoute extends _i15.PageRouteInfo<void> {
const HoursRoute() : super(HoursRoute.name, path: 'hours');
static const String name = 'HoursRoute';
}
/// generated route for
/// [_i14.SettingsScreen]
class SettingsRoute extends _i15.PageRouteInfo<void> {
const SettingsRoute() : super(SettingsRoute.name, path: 'settings');
static const String name = 'SettingsRoute';
}

View File

@ -1,4 +1,6 @@
import 'package:flutter/material.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/utils/date_range.dart'; import 'package:furman_now/src/utils/date_range.dart';
import 'package:furman_now/src/utils/theme.dart'; import 'package:furman_now/src/utils/theme.dart';
import 'package:furman_now/src/widgets/header.dart'; import 'package:furman_now/src/widgets/header.dart';
@ -11,98 +13,56 @@ class EventsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppPageLayout(
body: Container( title: "Events",
color: Colors.grey[100], icon: FlutterRemix.calendar_line,
child: SafeArea( content: Padding(
child: Stack( padding: const EdgeInsets.symmetric(vertical: 20),
fit: StackFit.loose, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( children: [
width: double.infinity, const HeaderWidget(title: "Today"),
height: double.infinity, Padding(
child: Align( padding: const EdgeInsets.symmetric(horizontal: 20),
alignment: Alignment.topLeft, child: EventsList()
child: Container( ),
padding: const EdgeInsets.symmetric(horizontal: 30), const HeaderWidget(title: "Tomorrow"),
width: double.infinity, Padding(
height: 100, padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: EventsList(dateRange: constructDateRange(
mainAxisAlignment: MainAxisAlignment.center, DateTime.now().add(const Duration(days: 1)),
crossAxisAlignment: CrossAxisAlignment.start, DateTime.now().add(const Duration(days: 1)),
children: [ )),
Wrap( ),
crossAxisAlignment: WrapCrossAlignment.center, ...[for(var i=2; i<7; i+=1) i].map((i) {
children: [ var date = DateTime.now().add(Duration(days: i));
Icon(Icons.calendar_month_outlined, size: 35, color: Colors.grey[700]), var dayName = DateFormat('EEEE').format(date);
const SizedBox(width: 12), return Wrap(
Text("Events", style: furmanTextStyle(TextStyle(color: Colors.grey[900], fontSize: 28, fontWeight: FontWeight.w700))), children: [
], HeaderWidget(title: dayName),
), Padding(
], padding: const EdgeInsets.symmetric(horizontal: 20),
), child: EventsList(dateRange: constructDateRange(
date,
date,
)),
), ),
), ],
), );
ScrollViewWithHeight( }),
child: Container( Center(child:
decoration: const BoxDecoration( Wrap(
color: Colors.white, direction: Axis.vertical,
borderRadius: BorderRadius.vertical(top: Radius.circular(30)), crossAxisAlignment: WrapCrossAlignment.center,
), children: const [
padding: const EdgeInsets.symmetric(vertical: 20), Text("Need more events?"),
margin: const EdgeInsets.only(top: 100), Text("Syncdin"),
width: double.infinity, Text("Athletics"),
child: Column( Text("CLPs"),
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"),
],
),
),
],
),
),
),
],
),
), ),
), ),
); );

View File

@ -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<Iterable<ContactInfo>>(
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()
),
);
},
),
)
);
}
}

View File

@ -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<Iterable<HealthSafetyInfo>>(
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()
),
);
},
),
],
),
)
);
}
}

View File

@ -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<AppState>(
builder: (context, state, _) {
return FutureBuilder<Iterable<Building>>(
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()
),
);
},
);
}
),
),
);
}
}

View File

@ -1,84 +1,64 @@
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:furman_now/src/utils/theme.dart'; import 'package:flutter_remix/flutter_remix.dart';
import 'package:furman_now/src/widgets/info/info_card.dart'; import 'package:furman_now/src/layouts/app_page.dart';
import 'package:furman_now/src/widgets/scroll_view_height.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 { class InfoScreen extends StatelessWidget {
const InfoScreen({Key? key}) : super(key: key); const InfoScreen({Key? key}) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return AppPageLayout(
body: Container( title: "Info",
color: Colors.grey[100], icon: FlutterRemix.information_line,
child: SafeArea( content: Padding(
child: Stack( padding: const EdgeInsets.all(20),
fit: StackFit.loose, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
SizedBox( children: [
width: double.infinity, GestureDetector(
height: double.infinity, onTap: () => context.router.push(const HealthSafetyRoute()),
child: Align( child: InfoCategoryCard(
alignment: Alignment.topLeft, color: Colors.red.shade50,
child: Container( icon: Icons.local_hospital,
padding: const EdgeInsets.symmetric(horizontal: 30), title: "Health and Safety",
width: double.infinity, description: "Important contact information and links regarding student health and safety.",
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))),
],
),
],
),
),
),
), ),
ScrollViewWithHeight( ),
child: Container( const SizedBox(height: 10),
decoration: const BoxDecoration( GestureDetector(
color: Colors.white, onTap: () => context.router.push(const ContactsRoute()),
borderRadius: BorderRadius.vertical(top: Radius.circular(30)), child: InfoCategoryCard(
), color: Colors.deepPurple.shade50,
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 20), icon: Icons.phone,
margin: const EdgeInsets.only(top: 100), title: "Contacts",
width: double.infinity, description: "Contact information for offices and resources on Furman's campus.",
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 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",
),
),
],
), ),
), ),
); );

View File

@ -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<SettingsScreen> createState() => _SettingsScreenState();
}
class _SettingsScreenState extends State<SettingsScreen> {
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."),
),
)
],
),
),
);
}
}

View File

@ -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<List<Building>> fetchBuildings() async {
var buildings = (await _fetchBuildings()).toList();
await _fetchBuildingHours(buildings);
return buildings;
}
static Future<Iterable<Building>> _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<dynamic>).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<void> _fetchBuildingHours(List<Building> 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<dynamic>)) {
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<BuildingHours> hoursList = <BuildingHours>[];
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<String, dynamic> 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<BuildingHours> {
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<String, dynamic> 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;
}
}

View File

@ -0,0 +1,77 @@
class DaysOfWeek {
final Weekday startDay;
final Weekday endDay;
final Map<Weekday, bool> 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<Weekday, bool> 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,
}

View File

@ -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<bool> _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<bool> _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.');
}
}
} }

View File

@ -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<Iterable<ContactInfo>> 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<dynamic>).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<String, dynamic> 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"],
);
}
}

View File

@ -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<Iterable<HealthSafetyInfo>> 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<dynamic>).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<String, dynamic> 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<String, IconData> 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,
}

View File

@ -1,8 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter/painting.dart'; import 'package:flutter/painting.dart';
import 'package:furman_now/src/services/days_of_week.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:palette_generator/palette_generator.dart';
class RestaurantService { class RestaurantService {
static Future<List<Restaurant>> fetchRestaurants() async { static Future<List<Restaurant>> fetchRestaurants() async {
@ -13,7 +15,7 @@ class RestaurantService {
static Future<Iterable<Restaurant>> _fetchRestaurants() async { static Future<Iterable<Restaurant>> _fetchRestaurants() async {
final response = await http 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 (response.statusCode == 200) {
// If the server did return a 200 OK response, // If the server did return a 200 OK response,
@ -25,20 +27,20 @@ class RestaurantService {
} else { } else {
// If the server did not return a 200 OK response, // If the server did not return a 200 OK response,
// then throw an exception. // then throw an exception.
throw Exception('Failed to load athletics events.'); throw Exception('Failed to load restaurants.');
} }
} }
static Future<void> _fetchRestaurantHours(List<Restaurant> restaurants) async { static Future<void> _fetchRestaurantHours(List<Restaurant> restaurants) async {
final response = await http 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 (response.statusCode == 200) {
// If the server did return a 200 OK response, // If the server did return a 200 OK response,
// then parse the JSON. // then parse the JSON.
final hoursListJson = jsonDecode(response.body); final hoursListJson = jsonDecode(response.body);
for (var hoursJson in (hoursListJson["results"] as List<dynamic>)) { for (var hoursJson in (hoursListJson["results"] as List<dynamic>)) {
var hours = Hours.fromJson(hoursJson); var hours = RestaurantHours.fromJson(hoursJson);
restaurants.firstWhere((restaurant) => restaurant.id == hours.id).hoursList.add(hours); restaurants.firstWhere((restaurant) => restaurant.id == hours.id).hoursList.add(hours);
} }
} else { } else {
@ -58,7 +60,8 @@ class Restaurant {
int frequency; int frequency;
int busyness; int busyness;
String? url; String? url;
List<Hours> hoursList = <Hours>[]; List<RestaurantHours> hoursList = <RestaurantHours>[];
Color? backgroundColor;
Restaurant({ Restaurant({
required this.id, required this.id,
@ -96,6 +99,18 @@ class Restaurant {
return NetworkImage(imageUri); return NetworkImage(imageUri);
} }
Future<void> _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<String, dynamic> json) { factory Restaurant.fromJson(Map<String, dynamic> json) {
return Restaurant( return Restaurant(
id: int.parse(json["id"]), id: int.parse(json["id"]),
@ -110,7 +125,7 @@ class Restaurant {
} }
} }
class Hours { class RestaurantHours {
int id; int id;
String? meal; String? meal;
Duration? startTime; Duration? startTime;
@ -118,7 +133,7 @@ class Hours {
DaysOfWeek daysOfWeek; DaysOfWeek daysOfWeek;
int dayOrder; int dayOrder;
Hours({ RestaurantHours({
required this.id, required this.id,
this.meal, this.meal,
this.startTime, this.startTime,
@ -136,8 +151,8 @@ class Hours {
return Duration(hours: hours, minutes: minutes, seconds: seconds); return Duration(hours: hours, minutes: minutes, seconds: seconds);
} }
factory Hours.fromJson(Map<String, dynamic> json) { factory RestaurantHours.fromJson(Map<String, dynamic> json) {
return Hours( return RestaurantHours(
id: int.parse(json["id"]), id: int.parse(json["id"]),
meal: json["meal"], meal: json["meal"],
startTime: (json["start"] != null) ? _getDurationFromString(json["start"]) : null, startTime: (json["start"] != null) ? _getDurationFromString(json["start"]) : null,
@ -147,81 +162,3 @@ class Hours {
); );
} }
} }
class DaysOfWeek {
final Weekday startDay;
final Weekday endDay;
final Map<Weekday, bool> 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<Weekday, bool> 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,
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; 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/event.dart';
import 'package:furman_now/src/services/events/events_service.dart'; import 'package:furman_now/src/services/events/events_service.dart';
import 'package:furman_now/src/services/restaurants/restaurant_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 { class AppState extends ChangeNotifier {
late Future<List<Event>> events; late Future<List<Event>> events;
late Future<List<Restaurant>> restaurants; late Future<List<Restaurant>> restaurants;
late Future<List<Building>> buildings;
AppState() { AppState() {
refresh(); refresh();
@ -14,20 +16,22 @@ class AppState extends ChangeNotifier {
void refresh() { void refresh() {
events = EventsService.fetchEvents(); events = EventsService.fetchEvents();
restaurants = RestaurantService.fetchRestaurants(); restaurants = RestaurantService.fetchRestaurants();
buildings = BuildingsService.fetchBuildings();
notifyListeners(); notifyListeners();
} }
@override @override
int get hashCode => int get hashCode =>
events.hashCode ^ events.hashCode ^
restaurants.hashCode; restaurants.hashCode ^
buildings.hashCode;
@override @override
bool operator ==(Object other) => bool operator ==(Object other) =>
identical(this, other) || identical(this, other) ||
other is AppState && other is AppState &&
runtimeType == other.runtimeType && runtimeType == other.runtimeType &&
events == other.events && events == other.events &&
restaurants == other.restaurants; restaurants == other.restaurants &&
buildings == other.buildings;
} }

View File

@ -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;
}
}

View File

@ -8,26 +8,44 @@ ThemeData _baseTheme = ThemeData(
unselectedItemColor: Colors.grey[500], unselectedItemColor: Colors.grey[500],
), ),
textTheme: TextTheme( textTheme: TextTheme(
headline1: TextStyle( headlineLarge: TextStyle(
color: Colors.grey[800], color: Colors.grey[800],
fontWeight: FontWeight.w900, fontWeight: FontWeight.w900,
fontSize: 22, 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], color: Colors.grey[500],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 16, fontSize: 16,
), ),
subtitle2: TextStyle( labelMedium: TextStyle(
color: Colors.grey[500], color: Colors.grey[500],
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
fontSize: 14, fontSize: 14,
), ),
), ),
pageTransitionsTheme: const PageTransitionsTheme(
builders: {
TargetPlatform.android:
CupertinoPageTransitionsBuilder(),
TargetPlatform.iOS:
CupertinoPageTransitionsBuilder(),
},
),
); );
ThemeData myFurmanTheme = _baseTheme.copyWith( ThemeData myFurmanTheme = _baseTheme.copyWith(
textTheme: GoogleFonts.interTextTheme(_baseTheme.textTheme), textTheme: GoogleFonts.interTextTheme(_baseTheme.textTheme),
); );
var furmanTextStyle = (TextStyle baseStyle) => GoogleFonts.inter(textStyle: baseStyle); var furmanTextStyle = (TextStyle baseStyle) =>
GoogleFonts.inter(textStyle: baseStyle);

View File

@ -1,45 +1,75 @@
import 'package:flutter/material.dart'; 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 { class InfoCard extends StatelessWidget {
final Color color;
final IconData icon;
final String title; final String title;
final String description; final IconData icon;
final List<InfoCardItem> items;
final void Function()? onClick;
const InfoCard({ const InfoCard({
required this.color,
required this.icon,
required this.title, required this.title,
required this.description, required this.icon,
required this.items,
this.onClick,
Key? key Key? key
}) : super(key: key); }) : super(key: key);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( return ConditionalParentWidget(
decoration: BoxDecoration( condition: onClick != null,
color: color, conditionalBuilder: (child) => GestureDetector(
borderRadius: BorderRadius.circular(10), onTap: onClick,
child: child,
), ),
child: Row( child: Container(
children: [ decoration: BoxDecoration(
Padding( color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10),
),
child: Row(
children: [
Padding(
padding: const EdgeInsets.only(left: 15, right: 5), padding: const EdgeInsets.only(left: 15, right: 5),
child: Icon(icon, size: 40,) child: Icon(
), icon,
Flexible( size: 40,
child: Container( color: Colors.grey.shade800,
padding: const EdgeInsets.symmetric(horizontal: 15, vertical: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: Theme.of(context).textTheme.titleLarge,),
Text(description),
]
), ),
), ),
) 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()),
]
),
),
)
],
),
), ),
); );
} }

View File

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

View File

@ -13,18 +13,23 @@ class ScrollViewWithHeight extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) { return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
return SingleChildScrollView( return CustomScrollView(
controller: controller, controller: controller,
physics: const BouncingScrollPhysics( physics: const BouncingScrollPhysics(
parent: AlwaysScrollableScrollPhysics() parent: AlwaysScrollableScrollPhysics(),
), ),
child: ConstrainedBox( slivers: [
constraints: constraints.copyWith( SliverFillRemaining(
minHeight: constraints.maxHeight, hasScrollBody: false,
maxHeight: double.infinity child: ConstrainedBox(
constraints: constraints.copyWith(
minHeight: constraints.maxHeight,
maxHeight: double.infinity
),
child: child,
),
), ),
child: child, ],
),
); );
}); });
} }

View File

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