feat: added datepicker, better refresh, and lots of fixes
This commit is contained in:
parent
48476f6fe4
commit
fa129f17fb
@ -40,23 +40,6 @@ class _MapPageState extends State<MapPage> {
|
|||||||
// _fetchPoints();
|
// _fetchPoints();
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Future<void> _fetchPoints() async {
|
|
||||||
final api = OwntracksApi(baseUrl: '', username: '', pass: '');
|
|
||||||
print(await api.getDevices());
|
|
||||||
final points = await api.fetchPointsForDevice(
|
|
||||||
user: 'yxk',
|
|
||||||
device: 'meow',
|
|
||||||
from: DateTime.now().subtract(const Duration(days: 1)));
|
|
||||||
setState(() {
|
|
||||||
_points = points.unwrapOr([])
|
|
||||||
.map((point) => LatLng(point.lat, point.lon))
|
|
||||||
.toList();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
@ -71,6 +54,9 @@ class _MapPageState extends State<MapPage> {
|
|||||||
future: getPath(),
|
future: getPath(),
|
||||||
builder: (something, tempPath) => BlocBuilder<SettingsCubit, SettingsState>(
|
builder: (something, tempPath) => BlocBuilder<SettingsCubit, SettingsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
if (tempPath.data == null) {
|
||||||
|
return const Center(child: Text('Loading Map...'));
|
||||||
|
}
|
||||||
return FlutterMap(
|
return FlutterMap(
|
||||||
options: const MapOptions(
|
options: const MapOptions(
|
||||||
initialCenter: LatLng(48.3285, 9.8942),
|
initialCenter: LatLng(48.3285, 9.8942),
|
||||||
@ -81,9 +67,9 @@ class _MapPageState extends State<MapPage> {
|
|||||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||||
tileProvider: CachedTileProvider(
|
tileProvider: CachedTileProvider(
|
||||||
maxStale: const Duration(days: 30),
|
maxStale: const Duration(days: 30),
|
||||||
store: HiveCacheStore(tempPath.data, hiveBoxName: 'HiveCacheStore')),
|
store: HiveCacheStore(tempPath.data!, hiveBoxName: 'HiveCacheStore')),
|
||||||
),
|
),
|
||||||
...state.activeDevices.map((id) => UserPath(device: id, settings: state)),
|
...state.activeDevices.map((id) => UserPath(key: ValueKey(id), device: id, settings: state)),
|
||||||
const MapCompass.cupertino(rotationDuration: Duration(milliseconds: 600)),
|
const MapCompass.cupertino(rotationDuration: Duration(milliseconds: 600)),
|
||||||
// CurrentLocationLayer(), TODO: add permission
|
// CurrentLocationLayer(), TODO: add permission
|
||||||
RichAttributionWidget(
|
RichAttributionWidget(
|
||||||
@ -104,7 +90,7 @@ class _MapPageState extends State<MapPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class UserPath extends StatefulWidget {
|
class UserPath extends StatefulWidget {
|
||||||
UserPath({required this.device, required this.settings});
|
UserPath({super.key, required this.device, required this.settings});
|
||||||
|
|
||||||
(String, String) device;
|
(String, String) device;
|
||||||
SettingsState settings;
|
SettingsState settings;
|
||||||
@ -116,10 +102,12 @@ class UserPath extends StatefulWidget {
|
|||||||
class _UserPathState extends State<UserPath> {
|
class _UserPathState extends State<UserPath> {
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext ctx) {
|
Widget build(BuildContext ctx) {
|
||||||
|
print('rebuilding widget for ${widget.device}');
|
||||||
return BlocProvider(
|
return BlocProvider(
|
||||||
create: (context) {
|
create: (context) {
|
||||||
final bloc = UserPathBloc(widget.device, widget.settings);
|
final bloc = UserPathBloc(widget.device, widget.settings);
|
||||||
bloc.add(UserPathFullUpdate());
|
bloc.add(UserPathFullUpdate());
|
||||||
|
// ONLY WORKS because gets rebuilt every time with settings update
|
||||||
return bloc;
|
return bloc;
|
||||||
},
|
},
|
||||||
child: BlocListener<LocationSubscribeCubit, LocationUpdateState>(
|
child: BlocListener<LocationSubscribeCubit, LocationUpdateState>(
|
||||||
@ -128,12 +116,15 @@ class _UserPathState extends State<UserPath> {
|
|||||||
UserPathBloc userPathBloc = context.read<UserPathBloc>();
|
UserPathBloc userPathBloc = context.read<UserPathBloc>();
|
||||||
if (state case LocationUpdateReceived(:final position, :final deviceId)) {
|
if (state case LocationUpdateReceived(:final position, :final deviceId)) {
|
||||||
if (userPathBloc.deviceId == state.deviceId) {
|
if (userPathBloc.deviceId == state.deviceId) {
|
||||||
context.read<UserPathBloc>().add(UserPathLiveSubscriptionUpdate(position));
|
userPathBloc.add(UserPathLiveSubscriptionUpdate(position));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: BlocBuilder<UserPathBloc, UserPathState>(
|
child: BlocBuilder<UserPathBloc, UserPathState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
// TODO: change once smarter rebuilds are ready
|
||||||
|
context.read<UserPathBloc>().add(UserPathLoginDataChanged(widget.settings));
|
||||||
|
|
||||||
print("rebuild");
|
print("rebuild");
|
||||||
final _istate = state as MainUserPathState;
|
final _istate = state as MainUserPathState;
|
||||||
// make markers
|
// make markers
|
||||||
@ -211,6 +202,7 @@ class _UserPathState extends State<UserPath> {
|
|||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
PolylineLayer(
|
PolylineLayer(
|
||||||
|
key: ValueKey('${widget.device}_historyLines'),
|
||||||
polylines: [
|
polylines: [
|
||||||
/*
|
/*
|
||||||
Polyline(
|
Polyline(
|
||||||
@ -225,7 +217,7 @@ class _UserPathState extends State<UserPath> {
|
|||||||
...polylines
|
...polylines
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
PolylineLayer(polylines: [
|
PolylineLayer(key: ValueKey('${widget.device}_liveLines'), polylines: [
|
||||||
Polyline(
|
Polyline(
|
||||||
points: state.livePoints.map((e) => LatLng(e.lat, e.lon)).toList(),
|
points: state.livePoints.map((e) => LatLng(e.lat, e.lon)).toList(),
|
||||||
strokeWidth: 4.0,
|
strokeWidth: 4.0,
|
||||||
@ -247,41 +239,32 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
context: context,
|
context: context,
|
||||||
builder: (bsContext) {
|
builder: (bsContext) {
|
||||||
return Container(
|
return Container(
|
||||||
height: MediaQuery.of(bsContext).size.height * 0.5,
|
// height: MediaQuery.of(bsContext).size.height * 0.5,
|
||||||
width: MediaQuery.of(bsContext).size.width,
|
width: MediaQuery.of(bsContext).size.width,
|
||||||
|
height: 500,
|
||||||
decoration: const BoxDecoration(
|
decoration: const BoxDecoration(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)),
|
borderRadius: BorderRadius.only(topLeft: Radius.circular(30), topRight: Radius.circular(30)),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(32),
|
padding: const EdgeInsets.all(32),
|
||||||
child: Column(
|
child: StreamBuilder<UserPathState>(
|
||||||
mainAxisSize: MainAxisSize.min,
|
stream: context.read<UserPathBloc>().stream,
|
||||||
|
builder: (sheetContext, state) {
|
||||||
|
final istate = state.data as MainUserPathState? ?? context.read<UserPathBloc>().state as MainUserPathState;
|
||||||
|
if (istate.livePoints.isEmpty) {
|
||||||
|
return Text("Couldn't find ${user.$1}:${user.$2}'s Location");
|
||||||
|
}
|
||||||
|
final curLocation = istate.livePoints.last;
|
||||||
|
return Column(
|
||||||
|
// mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Text(
|
||||||
// Wrap non-grid content in Flexible to manage space dynamically
|
|
||||||
child: Text(
|
|
||||||
'${user.$1}:${user.$2}',
|
'${user.$1}:${user.$2}',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 24,
|
fontSize: 24,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
StreamBuilder<UserPathState>(
|
|
||||||
stream: context.read<UserPathBloc>().stream,
|
|
||||||
builder: (sheetContext, state) {
|
|
||||||
final istate =
|
|
||||||
state.data as MainUserPathState? ?? context.read<UserPathBloc>().state as MainUserPathState;
|
|
||||||
if (istate.livePoints.isEmpty) {
|
|
||||||
return Text("Couldn't find ${user.$1}:${user.$2}'s Location");
|
|
||||||
}
|
|
||||||
final curLocation = istate.livePoints.last;
|
|
||||||
return Flexible(
|
|
||||||
// Use Flexible for dynamic content
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Container(
|
Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black,
|
color: Colors.black,
|
||||||
@ -326,11 +309,7 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
}
|
}
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
crossAxisCount: 2,
|
crossAxisCount: 1, crossAxisSpacing: 4.0, mainAxisSpacing: 4.0, mainAxisExtent: 100),
|
||||||
crossAxisSpacing: 4.0,
|
|
||||||
mainAxisSpacing: 4.0,
|
|
||||||
childAspectRatio: 2.8,
|
|
||||||
),
|
|
||||||
itemCount: locations.length,
|
itemCount: locations.length,
|
||||||
itemBuilder: (BuildContext context, int index) {
|
itemBuilder: (BuildContext context, int index) {
|
||||||
// calculate distance and bearing
|
// calculate distance and bearing
|
||||||
@ -357,7 +336,7 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
border: Border.all(color: Colors.pink, width: 2),
|
border: Border.all(color: Colors.pink, width: 2),
|
||||||
borderRadius: BorderRadius.circular(10),
|
borderRadius: BorderRadius.circular(10),
|
||||||
),
|
),
|
||||||
padding: EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -366,11 +345,11 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
"${locations[index].key.$1}:${locations[index].key.$2}",
|
"${locations[index].key.$1}:${locations[index].key.$2}",
|
||||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
Spacer(),
|
const Spacer(),
|
||||||
Text(formatDistance(distance ~/ 1)),
|
Text(formatDistance(distance ~/ 1)),
|
||||||
Transform.rotate(
|
Transform.rotate(
|
||||||
angle: bearing * (math.pi / 180),
|
angle: bearing * (math.pi / 180),
|
||||||
child: Icon(
|
child: const Icon(
|
||||||
Icons.arrow_upward,
|
Icons.arrow_upward,
|
||||||
color: Colors.blue,
|
color: Colors.blue,
|
||||||
),
|
),
|
||||||
@ -384,11 +363,10 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
builder: (context, _) {
|
builder: (context, _) {
|
||||||
return Text(
|
return Text(
|
||||||
"${formatDuration(DateTime.now().difference(locations[index].value.timestamp))} ago",
|
"${formatDuration(DateTime.now().difference(locations[index].value.timestamp))} ago",
|
||||||
style: const TextStyle(fontSize: 12)
|
style: const TextStyle(fontSize: 12));
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Spacer(),
|
const Spacer(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -401,12 +379,9 @@ showUserLocationModalBottomSheet(BuildContext context, (String, String) user) {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
@ -8,9 +8,7 @@ import 'package:rust_core/option.dart';
|
|||||||
import 'package:ws/ws.dart';
|
import 'package:ws/ws.dart';
|
||||||
|
|
||||||
String _responseNiceErrorString(http.Response response) {
|
String _responseNiceErrorString(http.Response response) {
|
||||||
return 'Request failed: ${response.statusCode}: ${response.body.length > 500
|
return 'Request failed: ${response.statusCode}: ${response.body.length > 500 ? response.body.substring(0, 500) : response.body}';
|
||||||
? response.body.substring(0, 500)
|
|
||||||
: response.body}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const API_PREFIX = '/api/0';
|
const API_PREFIX = '/api/0';
|
||||||
@ -26,8 +24,7 @@ class OwntracksApi {
|
|||||||
final String username;
|
final String username;
|
||||||
final String pass;
|
final String pass;
|
||||||
|
|
||||||
static Map<String, String> _createBasicAuthHeader(String username,
|
static Map<String, String> _createBasicAuthHeader(String username, String password,
|
||||||
String password,
|
|
||||||
[Map<String, String>? additionalHeaders]) {
|
[Map<String, String>? additionalHeaders]) {
|
||||||
final credentials = base64Encode(utf8.encode('$username:$password'));
|
final credentials = base64Encode(utf8.encode('$username:$password'));
|
||||||
final headers = {
|
final headers = {
|
||||||
@ -42,6 +39,36 @@ class OwntracksApi {
|
|||||||
return headers;
|
return headers;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Result<Point>> fetchLastPoint({
|
||||||
|
required String user,
|
||||||
|
required String device,
|
||||||
|
}) async {
|
||||||
|
final queryParams = {
|
||||||
|
'user': user,
|
||||||
|
'device': device,
|
||||||
|
'fields': 'lat,lon,isotst',
|
||||||
|
};
|
||||||
|
final uri = Uri.parse('$baseUrl$API_PREFIX/last').replace(queryParameters: queryParams);
|
||||||
|
|
||||||
|
var auth_header = _createBasicAuthHeader(username, pass);
|
||||||
|
|
||||||
|
final response = await http.get(
|
||||||
|
uri,
|
||||||
|
headers: auth_header,
|
||||||
|
);
|
||||||
|
|
||||||
|
// print('${response.statusCode}: ${response.body.length > 500 ? response.body.substring(0, 500) : response.body}');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final point = Point.fromJson((json.decode(response.body) as List<dynamic>)[0]);
|
||||||
|
// print(point);
|
||||||
|
return Ok(point);
|
||||||
|
} else {
|
||||||
|
return bail("couldn't get point data for device '$user:$device': ${_responseNiceErrorString(response)}");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Method to fetch points from a device within a specified date range
|
// Method to fetch points from a device within a specified date range
|
||||||
Future<Result<List<Point>>> fetchPointsForDevice({
|
Future<Result<List<Point>>> fetchPointsForDevice({
|
||||||
required String user,
|
required String user,
|
||||||
@ -57,8 +84,7 @@ class OwntracksApi {
|
|||||||
'to': (to ?? DateTime.now()).toIso8601String(),
|
'to': (to ?? DateTime.now()).toIso8601String(),
|
||||||
'fields': 'lat,lon,isotst',
|
'fields': 'lat,lon,isotst',
|
||||||
};
|
};
|
||||||
final uri = Uri.parse('$baseUrl$API_PREFIX/locations')
|
final uri = Uri.parse('$baseUrl$API_PREFIX/locations').replace(queryParameters: queryParams);
|
||||||
.replace(queryParameters: queryParams);
|
|
||||||
|
|
||||||
var auth_header = _createBasicAuthHeader(username, pass);
|
var auth_header = _createBasicAuthHeader(username, pass);
|
||||||
|
|
||||||
@ -67,18 +93,13 @@ class OwntracksApi {
|
|||||||
headers: auth_header,
|
headers: auth_header,
|
||||||
);
|
);
|
||||||
|
|
||||||
print(
|
print('${response.statusCode}: ${response.body.length > 500 ? response.body.substring(0, 500) : response.body}');
|
||||||
'${response.statusCode}: ${response.body.length > 500 ? response.body
|
|
||||||
.substring(0, 500) : response.body}');
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final jsonData =
|
final jsonData = (json.decode(response.body) as Map<String, dynamic>)['data'] as List;
|
||||||
(json.decode(response.body) as Map<String, dynamic>)['data'] as List;
|
|
||||||
return Ok(jsonData.map((point) => Point.fromJson(point)).toList());
|
return Ok(jsonData.map((point) => Point.fromJson(point)).toList());
|
||||||
} else {
|
} else {
|
||||||
return bail(
|
return bail("couldn't get point data for device '$user:$device': ${_responseNiceErrorString(response)}");
|
||||||
"couldn't get point data for device '$user:$device': ${_responseNiceErrorString(
|
|
||||||
response)}");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,9 +112,7 @@ class OwntracksApi {
|
|||||||
final response = await http.get(uri, headers: authHeader);
|
final response = await http.get(uri, headers: authHeader);
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
return bail(
|
return bail("couldn't get user list from server: ${_responseNiceErrorString(response)}");
|
||||||
"couldn't get user list from server: ${_responseNiceErrorString(
|
|
||||||
response)}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final users = List<String>.from(json.decode(response.body)['results']);
|
final users = List<String>.from(json.decode(response.body)['results']);
|
||||||
@ -101,14 +120,10 @@ class OwntracksApi {
|
|||||||
Map<String, List<String>> map = {};
|
Map<String, List<String>> map = {};
|
||||||
|
|
||||||
for (String user in users) {
|
for (String user in users) {
|
||||||
var response = await http.get(
|
var response = await http.get(uri.replace(queryParameters: {'user': user}), headers: authHeader);
|
||||||
uri.replace(queryParameters: {'user': user}),
|
|
||||||
headers: authHeader);
|
|
||||||
|
|
||||||
if (response.statusCode != 200) {
|
if (response.statusCode != 200) {
|
||||||
return bail(
|
return bail("couldn't get devices list for user '$user': ${_responseNiceErrorString(response)}");
|
||||||
"couldn't get devices list for user '$user': ${_responseNiceErrorString(
|
|
||||||
response)}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
map[user] = List<String>.from(jsonDecode(response.body)['results']);
|
map[user] = List<String>.from(jsonDecode(response.body)['results']);
|
||||||
@ -139,8 +154,7 @@ class OwntracksApi {
|
|||||||
final client = WebSocketClient(kIsWeb
|
final client = WebSocketClient(kIsWeb
|
||||||
? WebSocketOptions.common(connectionRetryInterval: retryInterval)
|
? WebSocketOptions.common(connectionRetryInterval: retryInterval)
|
||||||
: WebSocketOptions.vm(
|
: WebSocketOptions.vm(
|
||||||
connectionRetryInterval: retryInterval,
|
connectionRetryInterval: retryInterval, headers: _createBasicAuthHeader(username, pass)..addAll(headers)));
|
||||||
headers: _createBasicAuthHeader(username, pass)..addAll(headers)));
|
|
||||||
|
|
||||||
// Listen to messages
|
// Listen to messages
|
||||||
client.stream.listen(onMessage);
|
client.stream.listen(onMessage);
|
||||||
|
@ -2,21 +2,23 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:anyhow/base.dart';
|
import 'package:anyhow/base.dart';
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:duration_picker/duration_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
import 'package:hydrated_bloc/hydrated_bloc.dart';
|
||||||
import 'package:ot_viewer_app/owntracks_api.dart';
|
import 'package:ot_viewer_app/owntracks_api.dart';
|
||||||
|
import 'package:ot_viewer_app/util.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
class SettingsState {
|
class SettingsState {
|
||||||
SettingsState({
|
SettingsState(
|
||||||
this.url = '',
|
{this.url = '',
|
||||||
this.username = '',
|
this.username = '',
|
||||||
this.password = '',
|
this.password = '',
|
||||||
this.usersToDevices = const {},
|
this.usersToDevices = const {},
|
||||||
this.activeDevices = const {},
|
this.activeDevices = const {},
|
||||||
});
|
this.historyTime = const Duration(days: 1)});
|
||||||
|
|
||||||
final String url;
|
final String url;
|
||||||
final String username;
|
final String username;
|
||||||
@ -24,14 +26,12 @@ class SettingsState {
|
|||||||
final Map<String, List<String>> usersToDevices;
|
final Map<String, List<String>> usersToDevices;
|
||||||
final Set<(String, String)> activeDevices;
|
final Set<(String, String)> activeDevices;
|
||||||
|
|
||||||
// Copy constructor
|
final Duration historyTime;
|
||||||
SettingsState.copy(SettingsState source)
|
|
||||||
: url = source.url,
|
@override
|
||||||
username = source.username,
|
String toString() {
|
||||||
password = source.password,
|
return 'SettingsState{url: $url, username: $username, password: $password, usersToDevices: $usersToDevices, activeDevices: $activeDevices, historyTime: $historyTime}';
|
||||||
usersToDevices = Map.from(source.usersToDevices)
|
}
|
||||||
.map((key, value) => MapEntry(key, List.from(value))),
|
|
||||||
activeDevices = Set.from(source.activeDevices);
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) =>
|
bool operator ==(Object other) =>
|
||||||
@ -42,7 +42,8 @@ class SettingsState {
|
|||||||
username == other.username &&
|
username == other.username &&
|
||||||
password == other.password &&
|
password == other.password &&
|
||||||
usersToDevices == other.usersToDevices &&
|
usersToDevices == other.usersToDevices &&
|
||||||
activeDevices == other.activeDevices;
|
activeDevices == other.activeDevices &&
|
||||||
|
historyTime == other.historyTime;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -50,48 +51,9 @@ class SettingsState {
|
|||||||
username.hashCode ^
|
username.hashCode ^
|
||||||
password.hashCode ^
|
password.hashCode ^
|
||||||
usersToDevices.hashCode ^
|
usersToDevices.hashCode ^
|
||||||
activeDevices.hashCode;
|
activeDevices.hashCode ^
|
||||||
|
historyTime.hashCode;
|
||||||
@override
|
|
||||||
String toString() {
|
|
||||||
return 'SettingsState{url: $url, username: $username, password: $password, usersToDevices: $usersToDevices, activeDevices: $activeDevices}';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
|
||||||
Future<void> loadFromSharedPrefs() async {
|
|
||||||
final sp = await SharedPreferences.getInstance();
|
|
||||||
final url = sp.getString('url') ?? '';
|
|
||||||
final username = sp.getString('username') ?? '';
|
|
||||||
final password = sp.getString('password') ?? '';
|
|
||||||
|
|
||||||
// Decode the JSON string and then manually convert to the expected type
|
|
||||||
final usersToDevicesJson =
|
|
||||||
jsonDecode(sp.getString('usersToDevices') ?? '{}')
|
|
||||||
as Map<String, dynamic>;
|
|
||||||
final Map<String, List<String>> usersToDevices =
|
|
||||||
usersToDevicesJson.map((key, value) {
|
|
||||||
// Ensure the value is cast to a List<String>
|
|
||||||
final List<String> list = List<String>.from(value);
|
|
||||||
return MapEntry(key, list);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Decode the JSON string for activeDevices. Adjust this part as needed based on your actual data structure
|
|
||||||
final activeDevicesJson =
|
|
||||||
jsonDecode(sp.getString('activeDevices') ?? '[]') as List;
|
|
||||||
final List<(String, String)> activeDevices = List.from(activeDevicesJson);
|
|
||||||
|
|
||||||
emit(SettingsState(
|
|
||||||
url: url,
|
|
||||||
username: username,
|
|
||||||
password: password,
|
|
||||||
usersToDevices: usersToDevices,
|
|
||||||
activeDevices: activeDevices,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
*/
|
|
||||||
|
|
||||||
class SettingsCubit extends HydratedCubit<SettingsState> {
|
class SettingsCubit extends HydratedCubit<SettingsState> {
|
||||||
SettingsCubit() : super(SettingsState());
|
SettingsCubit() : super(SettingsState());
|
||||||
@ -134,8 +96,7 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
|
|||||||
@override
|
@override
|
||||||
SettingsState? fromJson(Map<String, dynamic> json) {
|
SettingsState? fromJson(Map<String, dynamic> json) {
|
||||||
// print("fromjson $json");
|
// print("fromjson $json");
|
||||||
final usersToDevicesMap =
|
final usersToDevicesMap = (json['usersToDevices'] as Map<String, dynamic>?)?.map((key, value) {
|
||||||
(json['usersToDevices'] as Map<String, dynamic>?)?.map((key, value) {
|
|
||||||
final List<String> list = List<String>.from(value as List);
|
final List<String> list = List<String>.from(value as List);
|
||||||
return MapEntry(key, list);
|
return MapEntry(key, list);
|
||||||
}) ??
|
}) ??
|
||||||
@ -146,14 +107,15 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
|
|||||||
username: (json['username'] ?? '') as String,
|
username: (json['username'] ?? '') as String,
|
||||||
password: (json['password'] ?? '') as String,
|
password: (json['password'] ?? '') as String,
|
||||||
usersToDevices: usersToDevicesMap,
|
usersToDevices: usersToDevicesMap,
|
||||||
activeDevices: Set<(String, String)>.from(
|
activeDevices: Set<(String, String)>.from(List<List<String>>.from(json['activeDevices']).map((e) {
|
||||||
List<List<String>>.from(json['activeDevices']).map((e) {
|
|
||||||
if (e.length != 2) {
|
if (e.length != 2) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return (e[0], e[1]);
|
return (e[0], e[1]);
|
||||||
}
|
}
|
||||||
}).where((element) => element != null)));
|
}).where((element) => element != null)),
|
||||||
|
historyTime: Duration(hours: (json['historyTime'] ?? '24') as int),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -165,6 +127,7 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
|
|||||||
'password': state.password,
|
'password': state.password,
|
||||||
'usersToDevices': state.usersToDevices,
|
'usersToDevices': state.usersToDevices,
|
||||||
'activeDevices': state.activeDevices.map((e) => [e.$1, e.$2]).toList(),
|
'activeDevices': state.activeDevices.map((e) => [e.$1, e.$2]).toList(),
|
||||||
|
'historyTime': state.historyTime.inHours,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +137,7 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
|
|||||||
String? password,
|
String? password,
|
||||||
Map<String, List<String>>? usersToDevices,
|
Map<String, List<String>>? usersToDevices,
|
||||||
Set<(String, String)>? activeDevices,
|
Set<(String, String)>? activeDevices,
|
||||||
|
Duration? historyTime,
|
||||||
}) async {
|
}) async {
|
||||||
final currentState = state;
|
final currentState = state;
|
||||||
|
|
||||||
@ -184,6 +148,7 @@ class SettingsCubit extends HydratedCubit<SettingsState> {
|
|||||||
password: password ?? currentState.password,
|
password: password ?? currentState.password,
|
||||||
usersToDevices: usersToDevices ?? currentState.usersToDevices,
|
usersToDevices: usersToDevices ?? currentState.usersToDevices,
|
||||||
activeDevices: activeDevices ?? currentState.activeDevices,
|
activeDevices: activeDevices ?? currentState.activeDevices,
|
||||||
|
historyTime: historyTime ?? currentState.historyTime,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -228,8 +193,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.redAccent.withOpacity(0.8), width: 2),
|
||||||
color: Colors.redAccent.withOpacity(0.8), width: 2),
|
|
||||||
borderRadius: BorderRadius.circular(10)),
|
borderRadius: BorderRadius.circular(10)),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
@ -238,12 +202,10 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
TextField(
|
TextField(
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText:
|
labelText: 'URL (e.g. https://your-owntracks.host.com)',
|
||||||
'URL (e.g. https://your-owntracks.host.com)',
|
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// Adds a border to the TextField
|
// Adds a border to the TextField
|
||||||
prefixIcon: Icon(
|
prefixIcon: Icon(Icons.account_tree), // Adds an icon to the left
|
||||||
Icons.account_tree), // Adds an icon to the left
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -254,8 +216,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
labelText: 'Username',
|
labelText: 'Username',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// Adds a border to the TextField
|
// Adds a border to the TextField
|
||||||
prefixIcon:
|
prefixIcon: Icon(Icons.person), // Adds an icon to the left
|
||||||
Icon(Icons.person), // Adds an icon to the left
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
@ -268,8 +229,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
labelText: 'Password',
|
labelText: 'Password',
|
||||||
border: OutlineInputBorder(),
|
border: OutlineInputBorder(),
|
||||||
// Adds a border to the TextField
|
// Adds a border to the TextField
|
||||||
prefixIcon:
|
prefixIcon: Icon(Icons.lock), // Adds an icon to the left
|
||||||
Icon(Icons.lock), // Adds an icon to the left
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(
|
const SizedBox(
|
||||||
@ -278,8 +238,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
Row(
|
Row(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.warning,
|
const Icon(Icons.warning, color: Colors.red, size: 16),
|
||||||
color: Colors.red, size: 16),
|
|
||||||
// Small red exclamation mark icon
|
// Small red exclamation mark icon
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
// Space between icon and text
|
// Space between icon and text
|
||||||
@ -287,8 +246,7 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
'Password saved locally',
|
'Password saved locally',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
color: Colors.red,
|
color: Colors.red,
|
||||||
fontSize:
|
fontSize: 12, // Small font size for the warning text
|
||||||
12, // Small font size for the warning text
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@ -303,34 +261,60 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
|
|
||||||
context.read<SettingsCubit>().fetchDevices();
|
context.read<SettingsCubit>().fetchDevices();
|
||||||
},
|
},
|
||||||
icon: Icon(Icons.save),
|
icon: const Icon(Icons.save),
|
||||||
label: Text('Save'))
|
label: const Text('Save'))
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () async {
|
||||||
|
final settingsCubit = context.read<SettingsCubit>();
|
||||||
|
var resultingDuration = await showDurationPicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: state.historyTime,
|
||||||
|
snapToMins: 60,
|
||||||
|
baseUnit: BaseUnit.hour,
|
||||||
|
);
|
||||||
|
if (resultingDuration != null) {
|
||||||
|
if (resultingDuration.inHours == 0) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context)
|
||||||
|
.showSnackBar(const SnackBar(content: Text('Please pick a duration of 1h or more!')));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
settingsCubit.updateSettings(historyTime: resultingDuration);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultingDuration.inDays > 4) {
|
||||||
|
if (context.mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(const SnackBar(
|
||||||
|
content: Text('Warning:\nLong durations might cause slowdowns and high data usage!')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text('Set Time to Load Points (current: ${formatDuration(state.historyTime)})')),
|
||||||
|
const SizedBox(height: 16),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.deepPurple.withOpacity(0.8), width: 2),
|
||||||
color: Colors.deepPurple.withOpacity(0.8), width: 2),
|
|
||||||
borderRadius: BorderRadius.circular(10)),
|
borderRadius: BorderRadius.circular(10)),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: List<Widget>.from([
|
children: List<Widget>.from([
|
||||||
Center(
|
Center(
|
||||||
child: ElevatedButton.icon(
|
child: ElevatedButton.icon(
|
||||||
onPressed: () =>
|
onPressed: () => context.read<SettingsCubit>().fetchDevices(),
|
||||||
context.read<SettingsCubit>().fetchDevices(),
|
|
||||||
icon: const Icon(Icons.refresh),
|
icon: const Icon(Icons.refresh),
|
||||||
label: const Text('Refresh Devices'),
|
label: const Text('Refresh Devices'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
]) +
|
]) +
|
||||||
List<Widget>.from(
|
List<Widget>.from(state.usersToDevices.entries.map((entry) {
|
||||||
state.usersToDevices.entries.map((entry) {
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
@ -349,26 +333,20 @@ class _SettingsPageState extends State<SettingsPage> {
|
|||||||
const Divider(),
|
const Divider(),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
padding: const EdgeInsets.only(left: 8),
|
||||||
child: Column(children:
|
child: Column(children: List<Widget>.from(entry.value.map((device) {
|
||||||
List<Widget>.from(entry.value.map((device) {
|
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Checkbox(
|
Checkbox(
|
||||||
value: state.activeDevices
|
value: state.activeDevices.contains((entry.key, device)),
|
||||||
.contains((entry.key, device)),
|
|
||||||
onChanged: (newVal) {
|
onChanged: (newVal) {
|
||||||
var cubit =
|
var cubit = context.read<SettingsCubit>();
|
||||||
context.read<SettingsCubit>();
|
Set<(String, String)> newSet = Set.from(state.activeDevices);
|
||||||
Set<(String, String)> newSet =
|
|
||||||
Set.from(state.activeDevices);
|
|
||||||
if (newVal ?? false) {
|
if (newVal ?? false) {
|
||||||
newSet.add((entry.key, device));
|
newSet.add((entry.key, device));
|
||||||
} else {
|
} else {
|
||||||
newSet
|
newSet.remove((entry.key, device));
|
||||||
.remove((entry.key, device));
|
|
||||||
}
|
}
|
||||||
cubit.updateSettings(
|
cubit.updateSettings(activeDevices: newSet);
|
||||||
activeDevices: newSet);
|
|
||||||
}),
|
}),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text("${entry.key}:$device"),
|
Text("${entry.key}:$device"),
|
||||||
|
@ -22,20 +22,30 @@ class UserPathBloc extends Bloc<UserPathEvent, UserPathState> {
|
|||||||
SettingsState settingsState;
|
SettingsState settingsState;
|
||||||
Option<WebSocketClient> _ws = None;
|
Option<WebSocketClient> _ws = None;
|
||||||
|
|
||||||
OwntracksApi get _api => OwntracksApi(
|
OwntracksApi get _api =>
|
||||||
baseUrl: settingsState.url,
|
OwntracksApi(baseUrl: settingsState.url, username: settingsState.username, pass: settingsState.password);
|
||||||
username: settingsState.username,
|
|
||||||
pass: settingsState.password);
|
|
||||||
|
|
||||||
UserPathBloc(this.deviceId, this.settingsState)
|
UserPathBloc(this.deviceId, this.settingsState)
|
||||||
: super(MainUserPathState(
|
: super(MainUserPathState(
|
||||||
initialPoints: const IListConst([]),
|
initialPoints: const IListConst([]),
|
||||||
livePoints: const IListConst([]),
|
livePoints: const IListConst([]),
|
||||||
from: DateTime.now().subtract(const Duration(days: 1)),
|
from: DateTime.now().subtract(settingsState.historyTime),
|
||||||
to: DateTime.now().add(const Duration(days: 365 * 100)))) {
|
to: DateTime.now())) /*.add(const Duration(days: 365 * 100)))) */ {
|
||||||
on<UserPathLoginDataChanged>((event, emit) {
|
on<UserPathLoginDataChanged>((event, emit) {
|
||||||
|
if (event.newSettings == settingsState) {
|
||||||
|
print('settings states the same, returning. states:\nold: $settingsState\nnew: ${event.newSettings}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
settingsState = event.newSettings;
|
settingsState = event.newSettings;
|
||||||
// TODO: restart live connections
|
|
||||||
|
emit(MainUserPathState.copy(
|
||||||
|
state as MainUserPathState,
|
||||||
|
from: DateTime.now().subtract(settingsState.historyTime),
|
||||||
|
));
|
||||||
|
|
||||||
|
print('settings states differ, doing full update');
|
||||||
|
|
||||||
|
add(UserPathFullUpdate());
|
||||||
});
|
});
|
||||||
|
|
||||||
on<UserPathLiveSubscriptionUpdate>((event, emit) {
|
on<UserPathLiveSubscriptionUpdate>((event, emit) {
|
||||||
@ -51,21 +61,28 @@ class UserPathBloc extends Bloc<UserPathEvent, UserPathState> {
|
|||||||
print("fpu");
|
print("fpu");
|
||||||
if (state is MainUserPathState) {
|
if (state is MainUserPathState) {
|
||||||
final istate = state as MainUserPathState;
|
final istate = state as MainUserPathState;
|
||||||
final history = await _api.fetchPointsForDevice(
|
var history = await _api.fetchPointsForDevice(
|
||||||
user: deviceId.$1,
|
user: deviceId.$1,
|
||||||
device: deviceId.$2,
|
device: deviceId.$2,
|
||||||
from: istate.from,
|
from: istate.from,
|
||||||
to: istate.to,
|
to: istate.to,
|
||||||
);
|
);
|
||||||
|
|
||||||
final Result<List<Point>> livePoints =
|
history = await history.toFutureResult().map((h) async {
|
||||||
history.map((ok) => ok.isNotEmpty ? [ok.last] : []);
|
if (h.isEmpty) {
|
||||||
|
final last = await _api.fetchLastPoint(user: deviceId.$1, device: deviceId.$2);
|
||||||
|
if (last.isOk()) {
|
||||||
|
return [last.unwrap()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return h;
|
||||||
|
});
|
||||||
|
|
||||||
|
final Result<List<Point>> livePoints = history.map((ok) => ok.isNotEmpty ? [ok.last] : []);
|
||||||
|
|
||||||
emit(MainUserPathState(
|
emit(MainUserPathState(
|
||||||
initialPoints:
|
initialPoints: history.expect("Couldn't retrieve path history for $deviceId").lock,
|
||||||
history.expect("Couldn't retrieve path history for $deviceId").lock,
|
livePoints: livePoints.expect("Couldn\'t retrieve last (current) point").lock,
|
||||||
livePoints:
|
|
||||||
livePoints.expect("Couldn\'t retrieve last (current) point").lock,
|
|
||||||
from: istate.from,
|
from: istate.from,
|
||||||
to: istate.to));
|
to: istate.to));
|
||||||
}
|
}
|
||||||
|
@ -97,6 +97,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.2"
|
version: "3.2.2"
|
||||||
|
duration_picker:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: duration_picker
|
||||||
|
sha256: "052b34dac04c29f3849bb3817a26c5aebe9e5f0697c3a374be87db2b84d75753"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.1"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
@ -55,6 +55,7 @@ dependencies:
|
|||||||
flutter_map_math: ^0.1.7
|
flutter_map_math: ^0.1.7
|
||||||
intl: ^0.19.0
|
intl: ^0.19.0
|
||||||
get_it: ^7.6.7
|
get_it: ^7.6.7
|
||||||
|
duration_picker: ^1.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
Loading…
Reference in New Issue
Block a user