wip: info page and GET app login / settings
This commit is contained in:
parent
4a698e61d2
commit
67efdc0289
|
@ -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>
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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),
|
||||||
|
]),
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
|
@ -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"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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()
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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."),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
}
|
|
@ -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.');
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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"],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
}
|
||||||
|
|
|
@ -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,
|
|
||||||
}
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -1,46 +1,76 @@
|
||||||
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()),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue