Skip to content

Commit

Permalink
Json loading and saving improvements
Browse files Browse the repository at this point in the history
* Display errors if json isn't properly formatted
* Display warnings if data is missing
* Add proper naming and MIME types
* Allow exports to be saved as any file format
* Create teleoperated and autonomous tabs if no other tabs are loaded
  • Loading branch information
Gold872 committed Nov 12, 2023
1 parent 300db79 commit 4362f65
Show file tree
Hide file tree
Showing 7 changed files with 235 additions and 91 deletions.
189 changes: 145 additions & 44 deletions lib/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:convert';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:dot_cast/dot_cast.dart';
Expand All @@ -20,8 +21,8 @@ import 'package:elegant_notification/elegant_notification.dart';
import 'package:elegant_notification/resources/arrays.dart';
import 'package:file_selector/file_selector.dart';
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
Expand Down Expand Up @@ -68,24 +69,6 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {

loadLayout();

if (tabData.isEmpty) {
tabData.addAll([
TabData(name: 'Teleoperated'),
TabData(name: 'Autonomous'),
]);

grids.addAll([
DashboardGrid(
key: GlobalKey(),
onAddWidgetPressed: displayAddWidgetDialog,
),
DashboardGrid(
key: GlobalKey(),
onAddWidgetPressed: displayAddWidgetDialog,
),
]);
}

nt4Connection.addConnectedListener(() {
setState(() {
for (DashboardGrid grid in grids) {
Expand Down Expand Up @@ -341,17 +324,20 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
}

void exportLayout() async {
String initialDirectory = (await getApplicationDocumentsDirectory()).path;
const XTypeGroup jsonTypeGroup = XTypeGroup(
label: 'JSON (JavaScript Object Notation)',
extensions: ['.json'],
mimeTypes: ['application/json'],
uniformTypeIdentifiers: ['public.json'],
);

const XTypeGroup typeGroup = XTypeGroup(
label: 'Json File',
extensions: ['json'],
const XTypeGroup anyTypeGroup = XTypeGroup(
label: 'All Files',
);

final FileSaveLocation? saveLocation = await getSaveLocation(
initialDirectory: initialDirectory,
suggestedName: 'elastic-layout.json',
acceptedTypeGroups: [typeGroup],
acceptedTypeGroups: [jsonTypeGroup, anyTypeGroup],
);

if (saveLocation == null) {
Expand All @@ -362,27 +348,49 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
String jsonString = jsonEncode(jsonData);

final Uint8List fileData = Uint8List.fromList(jsonString.codeUnits);
const String mimeType = 'application/json';

final XFile jsonFile = XFile.fromData(fileData,
mimeType: mimeType, name: 'elastic-layout.json');
mimeType: 'application/json', name: 'elastic-layout.json');

jsonFile.saveTo(saveLocation.path);
await jsonFile.saveTo(saveLocation.path);
}

void importLayout() async {
const XTypeGroup typeGroup = XTypeGroup(
label: 'Json File',
extensions: ['json'],
const XTypeGroup jsonTypeGroup = XTypeGroup(
label: 'JSON (JavaScript Object Notation)',
extensions: ['.json'],
mimeTypes: ['application/json'],
uniformTypeIdentifiers: ['public.json'],
);

final XFile? file = await openFile(acceptedTypeGroups: [typeGroup]);
const XTypeGroup anyTypeGroup = XTypeGroup(
label: 'All Files',
);

final XFile? file = await openFile(acceptedTypeGroups: [
jsonTypeGroup,
anyTypeGroup,
]);

if (file == null) {
return;
}

String jsonString = await file.readAsString();
String jsonString;

try {
jsonString = await file.readAsString();
} on FileSystemException catch (e) {
showJsonLoadingError(e.message);
return;
}

try {
jsonDecode(jsonString);
} catch (e) {
showJsonLoadingError(e.toString());
return;
}

await _preferences.setString(PrefKeys.layout, jsonString);

Expand All @@ -402,33 +410,126 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
}

void loadLayoutFromJsonData(String jsonString) {
Map<String, dynamic>? jsonData = tryCast(jsonDecode(jsonString));

if (jsonData == null) {
showJsonLoadingError('Invalid JSON format, aborting.');
createDefaultTabs();
return;
}

if (!jsonData.containsKey('tabs')) {
showJsonLoadingError('JSON does not contain necessary data, aborting.');
createDefaultTabs();
return;
}

tabData.clear();
grids.clear();

Map<String, dynamic> jsonData = jsonDecode(jsonString);
for (Map<String, dynamic> data in jsonData['tabs']) {
if (tryCast(data['name']) == null) {
showJsonLoadingWarning('Tab name not specified, ignoring tab data.');
continue;
}

if (tryCast<Map>(data['grid_layout']) == null) {
showJsonLoadingWarning(
'Grid layout not specified for tab \'${data['name']}\', ignoring tab data.');
continue;
}

tabData.add(TabData(name: data['name']));

grids.add(DashboardGrid.fromJson(
grids.add(
DashboardGrid.fromJson(
key: GlobalKey(),
jsonData: data['grid_layout'],
onAddWidgetPressed: displayAddWidgetDialog));
onAddWidgetPressed: displayAddWidgetDialog,
onJsonLoadingWarning: showJsonLoadingWarning,
),
);
}

if (tabData.isEmpty || grids.isEmpty) {
tabData.add(TabData(name: 'Tab 0'));
grids.add(DashboardGrid.fromJson(
key: GlobalKey(),
jsonData: const {},
onAddWidgetPressed: displayAddWidgetDialog,
));
}
createDefaultTabs();

if (currentTabIndex >= grids.length) {
currentTabIndex = grids.length - 1;
}
}

void createDefaultTabs() {
if (tabData.isEmpty || grids.isEmpty) {
tabData.addAll([
TabData(name: 'Teleoperated'),
TabData(name: 'Autonomous'),
]);

grids.addAll([
DashboardGrid(
key: GlobalKey(),
onAddWidgetPressed: displayAddWidgetDialog,
),
DashboardGrid(
key: GlobalKey(),
onAddWidgetPressed: displayAddWidgetDialog,
),
]);
}
}

void showJsonLoadingError(String errorMessage) {
Future(() {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;

int lines = '\n'.allMatches(errorMessage).length + 1;

ElegantNotification(
background: colorScheme.background,
progressIndicatorBackground: colorScheme.background,
progressIndicatorColor: const Color(0xffFE355C),
enableShadow: false,
width: 350,
height: 100 + (lines - 1) * 10,
notificationPosition: NotificationPosition.bottomRight,
toastDuration: const Duration(seconds: 3, milliseconds: 500),
icon: const Icon(Icons.error, color: Color(0xffFE355C)),
title: Text('Error while loading JSON data',
style: textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
)),
description: Flexible(child: Text(errorMessage)),
).show(context);
});
}

void showJsonLoadingWarning(String warningMessage) {
SchedulerBinding.instance.addPostFrameCallback((_) {
ColorScheme colorScheme = Theme.of(context).colorScheme;
TextTheme textTheme = Theme.of(context).textTheme;

int lines = '\n'.allMatches(warningMessage).length + 1;

ElegantNotification(
background: colorScheme.background,
progressIndicatorBackground: colorScheme.background,
progressIndicatorColor: Colors.yellow,
enableShadow: false,
width: 350,
height: 100 + (lines - 1) * 10,
notificationPosition: NotificationPosition.bottomRight,
toastDuration: const Duration(seconds: 3, milliseconds: 500),
icon: const Icon(Icons.warning, color: Colors.yellow),
title: Text('Warning while loading JSON data',
style: textTheme.bodyMedium!.copyWith(
fontWeight: FontWeight.bold,
)),
description: Flexible(child: Text(warningMessage)),
).show(context);
});
}

void displayAddWidgetDialog() {
setState(() => addWidgetDialogVisible = true);
}
Expand Down
2 changes: 0 additions & 2 deletions lib/services/shuffleboard_nt_listener.dart
Original file line number Diff line number Diff line change
Expand Up @@ -152,8 +152,6 @@ class ShuffleboardNTListener {
.putIfAbsent('children', () => <Map<String, dynamic>>[]);

_createOrGetChild(jsonKey, widgetName);

print('Adding child: $widgetName');
} else {
currentJsonData[jsonKey]!.putIfAbsent('layout', () => false);

Expand Down
17 changes: 13 additions & 4 deletions lib/widgets/dashboard_grid.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,20 @@ class DashboardGrid extends StatelessWidget {
super.key,
required Map<String, dynamic> jsonData,
this.onAddWidgetPressed,
Function(String message)? onJsonLoadingWarning,
}) {
if (jsonData['containers'] != null) {
loadContainersFromJson(jsonData);
loadContainersFromJson(jsonData,
onJsonLoadingWarning: onJsonLoadingWarning);
}

if (jsonData['layouts'] != null) {
loadLayoutsFromJson(jsonData);
loadLayoutsFromJson(jsonData, onJsonLoadingWarning: onJsonLoadingWarning);
}
}

void loadContainersFromJson(Map<String, dynamic> jsonData) {
void loadContainersFromJson(Map<String, dynamic> jsonData,
{Function(String message)? onJsonLoadingWarning}) {
for (Map<String, dynamic> containerData in jsonData['containers']) {
_widgetContainers.add(
DraggableNT4WidgetContainer.fromJson(
Expand All @@ -56,14 +59,18 @@ class DashboardGrid extends StatelessWidget {
onDragCancel: _nt4ContainerOnDragCancel,
onResizeBegin: _nt4ContainerOnResizeBegin,
onResizeEnd: _nt4ContainerOnResizeEnd,
onJsonLoadingWarning: onJsonLoadingWarning,
),
);
}
}

void loadLayoutsFromJson(Map<String, dynamic> jsonData) {
void loadLayoutsFromJson(Map<String, dynamic> jsonData,
{Function(String warningMessage)? onJsonLoadingWarning}) {
for (Map<String, dynamic> layoutData in jsonData['layouts']) {
if (layoutData['type'] == null) {
onJsonLoadingWarning
?.call('Layout widget type not specified, ignoring data.');
continue;
}

Expand All @@ -88,6 +95,7 @@ class DashboardGrid extends StatelessWidget {
onDragCancel: _nt4ContainerOnDragCancel,
onResizeBegin: _nt4ContainerOnResizeBegin,
onResizeEnd: _nt4ContainerOnResizeEnd,
onJsonLoadingWarning: onJsonLoadingWarning,
);
},
onUpdate: _layoutContainerOnUpdate,
Expand All @@ -96,6 +104,7 @@ class DashboardGrid extends StatelessWidget {
onDragCancel: _layoutContainerOnDragCancel,
onResizeBegin: _layoutContainerOnResizeBegin,
onResizeEnd: _layoutContainerOnResizeEnd,
onJsonLoadingWarning: onJsonLoadingWarning,
);
default:
continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ abstract class DraggableLayoutContainer extends DraggableWidgetContainer {
super.onDragCancel,
super.onResizeBegin,
super.onResizeEnd,
super.onJsonLoadingWarning,
}) : super.fromJson();

@override
Expand Down
24 changes: 17 additions & 7 deletions lib/widgets/draggable_containers/draggable_list_layout.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:ui';

import 'package:collection/collection.dart';
Expand Down Expand Up @@ -53,6 +52,7 @@ class DraggableListLayout extends DraggableLayoutContainer {
super.onDragCancel,
super.onResizeBegin,
super.onResizeEnd,
super.onJsonLoadingWarning,
}) : super.fromJson();

@override
Expand Down Expand Up @@ -224,8 +224,9 @@ class DraggableListLayout extends DraggableLayoutContainer {
}

@override
void fromJson(Map<String, dynamic> jsonData) {
super.fromJson(jsonData);
void fromJson(Map<String, dynamic> jsonData,
{Function(String errorMessage)? onJsonLoadingWarning}) {
super.fromJson(jsonData, onJsonLoadingWarning: onJsonLoadingWarning);

if (jsonData.containsKey('properties') &&
jsonData['properties'] is Map<String, dynamic>) {
Expand All @@ -238,15 +239,24 @@ class DraggableListLayout extends DraggableLayoutContainer {
}
}

for (Map<String, dynamic>? childData in jsonData['children']) {
if (childData == null) {
continue;
}
if (!jsonData.containsKey('children')) {
onJsonLoadingWarning
?.call('List Layout JSON data does not contain any children');
return;
}

if (jsonData['children'] is! List<dynamic>) {
onJsonLoadingWarning
?.call('List Layout JSON data does not contain any children');
return;
}

for (Map<String, dynamic> childData in jsonData['children']) {
children.add(nt4ContainerBuilder?.call(childData) ??
DraggableNT4WidgetContainer.fromJson(
dashboardGrid: dashboardGrid,
jsonData: childData,
onJsonLoadingWarning: onJsonLoadingWarning,
));
}
}
Expand Down
Loading

0 comments on commit 4362f65

Please sign in to comment.