Compare commits

...

3 Commits

9 changed files with 166 additions and 40 deletions

View File

@@ -3,6 +3,7 @@ import 'package:provider/provider.dart';
import 'pages/flood_station_page.dart'; import 'pages/flood_station_page.dart';
import 'pages/landing_page.dart'; import 'pages/landing_page.dart';
import 'pages/main_navigation_scaffold.dart';
import 'services/flood_station_provider.dart'; import 'services/flood_station_provider.dart';
import 'package:timezone/data/latest.dart' as tz; import 'package:timezone/data/latest.dart' as tz;
@@ -27,9 +28,9 @@ class MyApp extends StatelessWidget {
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true, useMaterial3: true,
), ),
initialRoute: LandingPage.routeName, initialRoute: MainNavigationScaffold.routeName,
routes: { routes: {
LandingPage.routeName: (context) => LandingPage(), MainNavigationScaffold.routeName: (context) => MainNavigationScaffold(),
FloodStationPage.routeName: (context) => FloodStationPage(), FloodStationPage.routeName: (context) => FloodStationPage(),
}, },
); );

View File

@@ -10,56 +10,79 @@ import 'flood_station_page.dart';
class LandingPage extends StatefulWidget { class LandingPage extends StatefulWidget {
const LandingPage({super.key}); const LandingPage({super.key});
static const routeName = '/';
@override @override
State<LandingPage> createState() => _LandingPageState(); State<LandingPage> createState() => _LandingPageState();
} }
class _LandingPageState extends State<LandingPage> { class _LandingPageState extends State<LandingPage> {
late FloodStationProvider floodStationProvider; late FloodStationProvider floodStationProvider;
bool _isLoading = false;
OverlayEntry? _overlayEntry; OverlayEntry? _overlayEntry;
int loadingTimes = 0;
@override
initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_isLoading = true;
showLoadingNotifier(context: context, message: 'Loading');
floodStationProvider
.loadAllStations()
.whenComplete(() => removeLoadingNotifier());
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
floodStationProvider = context.watch<FloodStationProvider>(); floodStationProvider = context.watch<FloodStationProvider>();
return Scaffold( return Column(
body: Column( children: [
children: [ StationFilter(
StationFilter( onChanged: (filterText) {
onEditingComplete: (filterText) {}, if (filterText.isEmpty) {
), floodStationProvider.filtered = false;
Expanded( setState(() {});
child: FloodStationListView( return;
stations: floodStationProvider.allStations, }
onItemTapped: (station) { showLoadingNotifier(context: context, message: 'Loading');
floodStationProvider.selectedStation = station; floodStationProvider.loadFilteredStations(filterText);
Navigator.of(context).pushNamed(FloodStationPage.routeName); floodStationProvider.filteredStationsFuture
}, ?.then((_) => removeLoadingNotifier());
), floodStationProvider.filtered = true;
), },
], ),
), _shouldShowList()
? Expanded(
child: FloodStationListView(
stations: floodStationProvider.filtered
? floodStationProvider.filteredStations
: floodStationProvider.allStations,
onItemTapped: (station) {
floodStationProvider.selectedStation = station;
Navigator.of(context).pushNamed(FloodStationPage.routeName);
},
),
)
: Expanded(
child: Center(
child: ElevatedButton(
onPressed: () {
showLoadingNotifier(context: context, message: 'Loading');
floodStationProvider
.loadAllStations()
.whenComplete(() => removeLoadingNotifier());
},
child: Text('Load all Stations'),
),
),
),
],
); );
} }
bool _shouldShowList() {
if (!floodStationProvider.filtered &&
floodStationProvider.allStations.isNotEmpty) {
return true;
}
if (floodStationProvider.filtered) {
return true;
}
return false;
}
Future<void> showLoadingNotifier({ Future<void> showLoadingNotifier({
required BuildContext context, required BuildContext context,
required String message, required String message,
}) async { }) async {
if (_overlayEntry != null) return;
OverlayState? overlayState = Overlay.of(context); OverlayState? overlayState = Overlay.of(context);
_overlayEntry = OverlayEntry( _overlayEntry = OverlayEntry(
builder: (c) { builder: (c) {
@@ -70,7 +93,10 @@ class _LandingPageState extends State<LandingPage> {
child: Center( child: Center(
child: LoadingNotifier( child: LoadingNotifier(
message: 'Loading', message: 'Loading',
onDismissed: () => removeLoadingNotifier(), onDismissed: () {
floodStationProvider.cancelFilterLoading();
removeLoadingNotifier();
},
), ),
), ),
); );

View File

@@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'landing_page.dart';
import 'map_page.dart';
class MainNavigationScaffold extends StatefulWidget {
const MainNavigationScaffold({super.key});
static const routeName = '/';
@override
State<MainNavigationScaffold> createState() => _MainNavigationScaffoldState();
}
class _MainNavigationScaffoldState extends State<MainNavigationScaffold> {
int _selectedPageIndex = 0;
final List<Widget> _pages = [
const LandingPage(),
const MapPage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
bottomNavigationBar: NavigationBar(
selectedIndex: _selectedPageIndex,
onDestinationSelected: (value) => setState(() {
_selectedPageIndex = value;
}),
destinations: [
NavigationDestination(icon: Icon(Icons.list), label: 'List'),
NavigationDestination(icon: Icon(Icons.map_outlined), label: 'Map'),
],
),
body: IndexedStack(
index: _selectedPageIndex,
children: _pages,
),
);
}
}

11
lib/pages/map_page.dart Normal file
View File

@@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
class MapPage extends StatelessWidget {
const MapPage({super.key});
static const routeName = '/map';
@override
Widget build(BuildContext context) {
return const Placeholder();
}
}

View File

@@ -21,6 +21,20 @@ class Api {
return stations; return stations;
} }
/// Fetches all stations whose label contain [label]
static Future<List<FloodStation>> fetchFilteredStations(String label) async {
List<FloodStation> stations = [];
final response =
await http.get(Uri.parse('$_rootUrl/id/stations?search=$label'));
if (response.statusCode == 200) {
final Map<String, dynamic> jsonStr = jsonDecode(response.body);
for (final str in jsonStr['items']) {
stations.add(FloodStation.fromMap(str));
}
}
return stations;
}
/// [limit] limits the number of entries that are requested from the API and [offset] returns the /// [limit] limits the number of entries that are requested from the API and [offset] returns the
/// list starting from the specified number /// list starting from the specified number

View File

@@ -1,14 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:async/async.dart';
import '../model/flood_station.dart'; import '../model/flood_station.dart';
import 'api.dart'; import 'api.dart';
class FloodStationProvider extends ChangeNotifier { class FloodStationProvider extends ChangeNotifier {
FloodStation? selectedStation; FloodStation? selectedStation;
bool filtered = false;
List<FloodStation> _allStations = []; List<FloodStation> _allStations = [];
List<FloodStation> _filteredStations = [];
List<FloodStation> get allStations => _allStations; List<FloodStation> get allStations => _allStations;
List<FloodStation> get filteredStations => _filteredStations;
CancelableOperation? _filteredStationsFuture;
CancelableOperation? get filteredStationsFuture => _filteredStationsFuture;
Future loadAllStationsInBatches({silent = false}) { Future loadAllStationsInBatches({silent = false}) {
int offset = 0; int offset = 0;
@@ -37,4 +45,26 @@ class FloodStationProvider extends ChangeNotifier {
}, },
); );
} }
Future loadFilteredStations(String filter, {silent = false}) {
if (_filteredStationsFuture != null) {
_filteredStationsFuture!.cancel();
}
final future = Api.fetchFilteredStations(filter);
_filteredStationsFuture = CancelableOperation.fromFuture(future).then(
(value) {
_filteredStations = value;
if (!silent) {
notifyListeners();
}
},
);
return future;
}
void cancelFilterLoading() {
if (_filteredStationsFuture != null) {
_filteredStationsFuture!.cancel();
}
}
} }

View File

@@ -70,6 +70,7 @@ class ReadingGraph extends StatelessWidget {
reservedSize: 40, reservedSize: 40,
showTitles: true, showTitles: true,
maxIncluded: false, maxIncluded: false,
minIncluded: false,
getTitlesWidget: (value, meta) => SideTitleWidget( getTitlesWidget: (value, meta) => SideTitleWidget(
meta: meta, meta: meta,
child: Text(getDate(value)), child: Text(getDate(value)),

View File

@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class StationFilter extends StatefulWidget { class StationFilter extends StatefulWidget {
const StationFilter({super.key, required this.onEditingComplete}); const StationFilter({super.key, required this.onChanged});
final void Function(String filterText) onEditingComplete; final void Function(String filterText) onChanged;
@override @override
State<StationFilter> createState() => StationFilterState(); State<StationFilter> createState() => StationFilterState();
@@ -22,15 +22,15 @@ class StationFilterState extends State<StationFilter> {
child: IconButton( child: IconButton(
onPressed: () { onPressed: () {
filterController.clear(); filterController.clear();
setState(() {});
widget.onChanged(filterController.text);
}, },
icon: Icon(Icons.delete), icon: Icon(Icons.delete),
), ),
), ),
label: Text('Filter'), label: Text('Filter'),
), ),
onChanged: (_) => setState(() {}), onChanged: (_) => widget.onChanged(filterController.text),
onEditingComplete: () => widget.onEditingComplete(filterController.text),
); );
} }
} }

View File

@@ -16,6 +16,8 @@ dependencies:
intl: ^0.20.2 intl: ^0.20.2
provider: ^6.1.2 provider: ^6.1.2
timezone: ^0.10.0 timezone: ^0.10.0
async: ^2.11.0
flutter_osm_plugin: ^1.3.6
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: