Skip to content

Commit

Permalink
Improvements to Remote Layout Downloading (#158)
Browse files Browse the repository at this point in the history
- Add menu to select which file to download from the robot
- Write unit tests
  • Loading branch information
Gold872 authored Dec 15, 2024
1 parent 66a882c commit 08deead
Show file tree
Hide file tree
Showing 6 changed files with 2,101 additions and 1,185 deletions.
90 changes: 88 additions & 2 deletions lib/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:elegant_notification/elegant_notification.dart';
import 'package:elegant_notification/resources/stacked_options.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flex_seed_scheme/flex_seed_scheme.dart';
import 'package:http/http.dart';
import 'package:path/path.dart' as path;
import 'package:popover/popover.dart';
import 'package:screen_retriever/screen_retriever.dart';
Expand All @@ -31,6 +32,7 @@ import 'package:elastic_dashboard/services/shuffleboard_nt_listener.dart';
import 'package:elastic_dashboard/services/update_checker.dart';
import 'package:elastic_dashboard/util/tab_data.dart';
import 'package:elastic_dashboard/widgets/custom_appbar.dart';
import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_dropdown_chooser.dart';
import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_text_input.dart';
import 'package:elastic_dashboard/widgets/dialog_widgets/dialog_toggle_switch.dart';
import 'package:elastic_dashboard/widgets/dialog_widgets/layout_drag_tile.dart';
Expand All @@ -47,6 +49,7 @@ class DashboardPage extends StatefulWidget {
final NTConnection ntConnection;
final SharedPreferences preferences;
final UpdateChecker updateChecker;
final ElasticLayoutDownloader? layoutDownloader;
final Function(Color color)? onColorChanged;
final Function(FlexSchemeVariant variant)? onThemeVariantChanged;

Expand All @@ -56,6 +59,7 @@ class DashboardPage extends StatefulWidget {
required this.preferences,
required this.version,
required this.updateChecker,
this.layoutDownloader,
this.onColorChanged,
this.onThemeVariantChanged,
});
Expand All @@ -65,7 +69,7 @@ class DashboardPage extends StatefulWidget {
}

class _DashboardPageState extends State<DashboardPage> with WindowListener {
late final SharedPreferences preferences = widget.preferences;
SharedPreferences get preferences => widget.preferences;
late final RobotNotificationsListener _robotNotificationListener;
late final ElasticLayoutDownloader _layoutDownloader;

Expand Down Expand Up @@ -278,7 +282,8 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
});
_robotNotificationListener.listen();

_layoutDownloader = ElasticLayoutDownloader();
_layoutDownloader =
widget.layoutDownloader ?? ElasticLayoutDownloader(Client());
}

@override
Expand Down Expand Up @@ -669,14 +674,95 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
return true;
}

Future<String?> _showRemoteLayoutSelection(List<String> fileNames) async {
if (!mounted) {
return null;
}
ValueNotifier<String?> currentSelection = ValueNotifier(null);
return await showDialog<String>(
context: context,
builder: (context) => AlertDialog(
title: const Text('Select Layout'),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
ValueListenableBuilder(
valueListenable: currentSelection,
builder: (_, value, child) => DialogDropdownChooser<String>(
choices: fileNames,
initialValue: value,
onSelectionChanged: (selection) =>
currentSelection.value = selection,
),
)
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('Cancel'),
),
ValueListenableBuilder(
valueListenable: currentSelection,
builder: (_, value, child) => TextButton(
onPressed: (value != null)
? () => Navigator.of(context).pop(value)
: null,
child: const Text('Download'),
),
),
],
),
);
}

void _loadLayoutFromRobot() async {
if (preferences.getBool(PrefKeys.layoutLocked) ?? Defaults.layoutLocked) {
return;
}

LayoutDownloadResponse<List<String>> layoutsResponse =
await _layoutDownloader.getAvailableLayouts(
ntConnection: widget.ntConnection,
preferences: preferences,
);

if (!layoutsResponse.successful) {
_showNotification(
title: 'Failed to Retrieve Layout List',
message: layoutsResponse.data.firstOrNull ??
'Unable to retrieve list of available layouts',
color: const Color(0xffFE355C),
icon: const Icon(Icons.error, color: Color(0xffFE355C)),
width: 400,
);
return;
}

if (layoutsResponse.data.isEmpty) {
_showNotification(
title: 'Failed to Retrieve Layout List',
message:
'No layouts were found, ensure a valid layout json file is placed in the root directory of your deploy directory.',
color: const Color(0xffFE355C),
icon: const Icon(Icons.error, color: Color(0xffFE355C)),
width: 400,
);
return;
}

String? selectedLayout = await _showRemoteLayoutSelection(
layoutsResponse.data.sorted((a, b) => a.compareTo(b)),
);

if (selectedLayout == null) {
return;
}

LayoutDownloadResponse response = await _layoutDownloader.downloadLayout(
ntConnection: widget.ntConnection,
preferences: preferences,
layoutName: selectedLayout,
);

if (!response.successful) {
Expand Down
66 changes: 60 additions & 6 deletions lib/services/elastic_layout_downloader.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import 'dart:convert';

import 'package:dot_cast/dot_cast.dart';
import 'package:http/http.dart';
import 'package:shared_preferences/shared_preferences.dart';

import 'package:elastic_dashboard/services/nt_connection.dart';
import 'package:elastic_dashboard/services/settings.dart';

typedef LayoutDownloadResponse = ({bool successful, String data});
typedef LayoutDownloadResponse<T> = ({bool successful, T data});

class ElasticLayoutDownloader {
final Client client = Client();
final Client client;

ElasticLayoutDownloader(this.client);

Future<LayoutDownloadResponse> downloadLayout({
Future<LayoutDownloadResponse<String>> downloadLayout({
required NTConnection ntConnection,
required SharedPreferences preferences,
required String layoutName,
}) async {
if (!ntConnection.isNT4Connected) {
return (
Expand All @@ -22,8 +28,9 @@ class ElasticLayoutDownloader {
}
String robotIP =
preferences.getString(PrefKeys.ipAddress) ?? Defaults.ipAddress;
String escapedName = Uri.encodeComponent('$layoutName.json');
Uri robotUri = Uri.parse(
'http://$robotIP:5800/elastic-layout.json',
'http://$robotIP:5800/$escapedName',
);
Response response;
try {
Expand All @@ -33,13 +40,60 @@ class ElasticLayoutDownloader {
}
if (response.statusCode < 200 || response.statusCode >= 300) {
String errorMessage = switch (response.statusCode) {
404 =>
'File "elastic-layout.json" was not found, ensure that you have deployed a file named "elastic_layout.json" in the deploy directory',
404 => 'File "$layoutName.json" was not found',
_ => 'Request returned status code ${response.statusCode}',
};

return (successful: false, data: errorMessage);
}
return (successful: true, data: response.body);
}

Future<LayoutDownloadResponse<List<String>>> getAvailableLayouts({
required NTConnection ntConnection,
required SharedPreferences preferences,
}) async {
if (!ntConnection.isNT4Connected) {
return (
successful: false,
data: <String>[
'Cannot fetch remote layouts while disconnected from the robot'
]
);
}
String robotIP =
preferences.getString(PrefKeys.ipAddress) ?? Defaults.ipAddress;
Uri robotUri = Uri.parse(
'http://$robotIP:5800/?format=json',
);
Response response;
try {
response = await client.get(robotUri);
} on ClientException catch (e) {
print('Houston we have a problem\n\n ${e.message}');
return (successful: false, data: [e.message]);
}
Map<String, dynamic>? responseJson = tryCast(jsonDecode(response.body));
if (responseJson == null) {
return (successful: false, data: ['Response was not a json object']);
}
if (!responseJson.containsKey('files')) {
return (
successful: false,
data: ['Response json does not contain files list']
);
}

List<String> fileNames = [];
for (Map<String, dynamic> fileData in responseJson['files']) {
String? name = fileData['name'];
if (name == null) {
continue;
}
if (name.endsWith('json')) {
fileNames.add(name.substring(0, name.length - '.json'.length));
}
}
return (successful: true, data: fileNames);
}
}
35 changes: 22 additions & 13 deletions lib/widgets/tab_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -78,17 +78,22 @@ class TabGridModel extends ChangeNotifier {

bool valid = true;

for (NTWidgetContainerModel container
in _widgetModels.whereType<NTWidgetContainerModel>()) {
for (WidgetContainerModel container in _widgetModels) {
String? title = container.title;
String? type = container.childModel.type;
String? topic = container.childModel.topic;
if (container is NTWidgetContainerModel) {
String? type = container.childModel.type;
String? topic = container.childModel.topic;

if (title == widgetData['title'] &&
type == widgetData['type'] &&
topic == widgetData['properties']['topic']) {
valid = false;
break;
}
}
bool validLocation = isValidLocation(newWidgetLocation);

if (title == widgetData['title'] &&
type == widgetData['type'] &&
topic == widgetData['properties']['topic'] ||
!validLocation) {
if (!validLocation) {
valid = false;
break;
}
Expand Down Expand Up @@ -119,14 +124,18 @@ class TabGridModel extends ChangeNotifier {

bool valid = true;

for (LayoutContainerModel container
in _widgetModels.whereType<LayoutContainerModel>()) {
for (WidgetContainerModel container in _widgetModels) {
String? title = container.title;
String type = container.type;
if (container is ListLayoutModel) {
String type = container.type;
if (title == widgetData['title'] && type == widgetData['type']) {
valid = false;
break;
}
}
bool validLocation = isValidLocation(newWidgetLocation);

if (title == widgetData['title'] && type == widgetData['type'] ||
!validLocation) {
if (!validLocation) {
valid = false;
break;
}
Expand Down
Loading

0 comments on commit 08deead

Please sign in to comment.