Skip to content

Commit

Permalink
Merge branch 'main' into update-text-messages
Browse files Browse the repository at this point in the history
  • Loading branch information
Gold872 authored Dec 31, 2024
2 parents 6e63ab8 + 49edd4a commit 66863ca
Show file tree
Hide file tree
Showing 5 changed files with 617 additions and 675 deletions.
2 changes: 0 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ import 'package:elastic_dashboard/services/log.dart';
import 'package:elastic_dashboard/services/nt_connection.dart';
import 'package:elastic_dashboard/services/nt_widget_builder.dart';
import 'package:elastic_dashboard/services/settings.dart';
import 'package:elastic_dashboard/services/update_checker.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();
Expand Down Expand Up @@ -217,7 +216,6 @@ class _ElasticState extends State<Elastic> {
ntConnection: widget.ntConnection,
preferences: widget.preferences,
version: widget.version,
updateChecker: UpdateChecker(currentVersion: widget.version),
onColorChanged: (color) => setState(() {
teamColor = color;
widget.preferences.setInt(PrefKeys.teamColor, color.value);
Expand Down
221 changes: 189 additions & 32 deletions lib/pages/dashboard_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,46 @@ import 'package:elastic_dashboard/widgets/settings_dialog.dart';
import 'package:elastic_dashboard/widgets/tab_grid.dart';
import '../widgets/draggable_containers/models/layout_container_model.dart';

enum LayoutDownloadMode {
overwrite(
name: 'Overwrite',
description:
'Keeps existing tabs that are not defined in the remote layout. Any tabs that are defined in the remote layout will be overwritten locally.',
),
merge(
name: 'Merge',
description:
'Merge the downloaded layout with the existing one. If a new widget cannot be properly placed, it will not be added.',
),
reload(
name: 'Full Reload',
description: 'Deletes the existing layout and loads the new one.',
);

final String name;
final String description;

const LayoutDownloadMode({required this.name, required this.description});

static String get descriptions {
String result = '';
for (final value in values) {
result += '${value.name}: ';
result += value.description;

if (value != values.last) {
result += '\n\n';
}
}
return result;
}
}

class DashboardPage extends StatefulWidget {
final String version;
final NTConnection ntConnection;
final SharedPreferences preferences;
final UpdateChecker updateChecker;
final UpdateChecker? updateChecker;
final ElasticLayoutDownloader? layoutDownloader;
final Function(Color color)? onColorChanged;
final Function(FlexSchemeVariant variant)? onThemeVariantChanged;
Expand All @@ -58,7 +93,7 @@ class DashboardPage extends StatefulWidget {
required this.ntConnection,
required this.preferences,
required this.version,
required this.updateChecker,
this.updateChecker,
this.layoutDownloader,
this.onColorChanged,
this.onThemeVariantChanged,
Expand All @@ -71,6 +106,7 @@ class DashboardPage extends StatefulWidget {
class _DashboardPageState extends State<DashboardPage> with WindowListener {
SharedPreferences get preferences => widget.preferences;
late final ElasticLibListener _robotNotificationListener;
late final UpdateChecker _updateChecker;
late final ElasticLayoutDownloader _layoutDownloader;

bool _seenShuffleboardWarning = false;
Expand Down Expand Up @@ -246,11 +282,6 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
apiListener.initializeListeners();
});

if (!isWPILib) {
Future(
() => _checkForUpdates(notifyIfLatest: false, notifyIfError: false));
}

_robotNotificationListener = ElasticLibListener(
ntConnection: widget.ntConnection,
onTabSelected: (tabIdentifier) {
Expand Down Expand Up @@ -301,6 +332,14 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {

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

_updateChecker =
widget.updateChecker ?? UpdateChecker(currentVersion: widget.version);

if (!isWPILib) {
Future(
() => _checkForUpdates(notifyIfLatest: false, notifyIfError: false));
}
}

@override
Expand Down Expand Up @@ -397,7 +436,7 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
ButtonThemeData buttonTheme = ButtonTheme.of(context);

UpdateCheckerResponse updateResponse =
await widget.updateChecker.isUpdateAvailable();
await _updateChecker.isUpdateAvailable();

if (mounted) {
setState(() => lastUpdateResponse = updateResponse);
Expand Down Expand Up @@ -607,21 +646,28 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
return true;
}

void _loadLayoutFromJsonData(String jsonString) {
void _clearLayout() {
for (TabData tab in _tabData) {
tab.tabGrid.onDestroy();
}
_tabData.clear();
}

bool _loadLayoutFromJsonData(String jsonString) {
logger.info('Loading layout from json');
Map<String, dynamic>? jsonData = tryCast(jsonDecode(jsonString));

if (!_validateJsonData(jsonData)) {
_createDefaultTabs();
return;
return false;
}

if (jsonData!.containsKey('grid_size')) {
_gridSize = tryCast(jsonData['grid_size']) ?? _gridSize;
preferences.setInt(PrefKeys.gridSize, _gridSize);
}

_tabData.clear();
_clearLayout();

for (Map<String, dynamic> data in jsonData['tabs']) {
_tabData.add(
Expand All @@ -643,6 +689,8 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
if (_currentTabIndex >= _tabData.length) {
_currentTabIndex = _tabData.length - 1;
}

return true;
}

bool _mergeLayoutFromJsonData(String jsonString) {
Expand Down Expand Up @@ -690,39 +738,132 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
return true;
}

Future<String?> _showRemoteLayoutSelection(List<String> fileNames) async {
void _overwriteLayoutFromJsonData(String jsonString) {
logger.info('Overwriting layout from json');

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

if (!_validateJsonData(jsonData)) {
return;
}

int overwritten = 0;
for (Map<String, dynamic> tabJson in jsonData!['tabs']) {
String tabName = tabJson['name'];
if (!_tabData.any((tab) => tab.name == tabName)) {
_tabData.add(
TabData(
name: tabName,
tabGrid: TabGridModel.fromJson(
ntConnection: widget.ntConnection,
preferences: widget.preferences,
jsonData: tabJson['grid_layout'],
onAddWidgetPressed: _displayAddWidgetDialog,
onJsonLoadingWarning: _showJsonLoadingWarning,
),
),
);
} else {
overwritten++;
TabGridModel existingTab =
_tabData.firstWhere((tab) => tab.name == tabName).tabGrid;
existingTab.onDestroy();
existingTab.loadFromJson(
jsonData: tabJson['grid_layout'],
onJsonLoadingWarning: _showJsonLoadingWarning,
);
}
}

_showInfoNotification(
title: 'Successfully Downloaded Layout',
message:
'Remote layout has been successfully downloaded, $overwritten tabs were overwritten.',
width: 350,
);
}

Future<({String layout, LayoutDownloadMode mode})?>
_showRemoteLayoutSelection(List<String> fileNames) async {
if (!mounted) {
return null;
}
ValueNotifier<String?> currentSelection = ValueNotifier(null);
return await showDialog<String>(
ValueNotifier<String?> layoutSelection = ValueNotifier(null);
ValueNotifier<LayoutDownloadMode> modeSelection =
ValueNotifier(LayoutDownloadMode.overwrite);

bool showModes = false;
return await showDialog(
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,
),
)
],
content: SizedBox(
width: 350,
child: StatefulBuilder(
builder: (context, setState) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Layout File'),
ValueListenableBuilder(
valueListenable: layoutSelection,
builder: (_, value, child) => DialogDropdownChooser<String>(
choices: fileNames,
initialValue: value,
onSelectionChanged: (selection) =>
layoutSelection.value = selection,
),
),
const SizedBox(height: 20),
const Text('Download Mode'),
Row(
children: [
Flexible(
child: ValueListenableBuilder(
valueListenable: modeSelection,
builder: (_, value, child) =>
DialogDropdownChooser<LayoutDownloadMode>(
choices: LayoutDownloadMode.values,
initialValue: value,
nameMap: (value) => value.name,
onSelectionChanged: (selection) {
if (selection != null) {
modeSelection.value = selection;
}
},
),
),
),
const SizedBox(width: 5),
TextButton.icon(
label: const Text('Help'),
icon: const Icon(Icons.help_outline),
onPressed: () {
setState(() => showModes = !showModes);
},
),
],
),
if (showModes) ...[
const SizedBox(height: 5),
Text(LayoutDownloadMode.descriptions),
],
],
);
},
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(null),
child: const Text('Cancel'),
),
ValueListenableBuilder(
valueListenable: currentSelection,
valueListenable: layoutSelection,
builder: (_, value, child) => TextButton(
onPressed: (value != null)
? () => Navigator.of(context).pop(value)
? () => Navigator.of(context)
.pop((layout: value, mode: modeSelection.value))
: null,
child: const Text('Download'),
),
Expand Down Expand Up @@ -763,7 +904,7 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
return;
}

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

Expand All @@ -774,7 +915,7 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
LayoutDownloadResponse response = await _layoutDownloader.downloadLayout(
ntConnection: widget.ntConnection,
preferences: preferences,
layoutName: selectedLayout,
layoutName: selectedLayout.layout,
);

if (!response.successful) {
Expand All @@ -786,7 +927,23 @@ class _DashboardPageState extends State<DashboardPage> with WindowListener {
return;
}

_mergeLayoutFromJsonData(response.data);
switch (selectedLayout.mode) {
case LayoutDownloadMode.merge:
_mergeLayoutFromJsonData(response.data);
case LayoutDownloadMode.overwrite:
setState(() => _overwriteLayoutFromJsonData(response.data));
case LayoutDownloadMode.reload:
setState(() {
bool success = _loadLayoutFromJsonData(response.data);
if (success) {
_showInfoNotification(
title: 'Successfully Downloaded Layout',
message: 'Remote layout has been successfully downloaded!',
width: 350,
);
}
});
}
}

void _createDefaultTabs() {
Expand Down
17 changes: 10 additions & 7 deletions lib/widgets/dialog_widgets/dialog_dropdown_chooser.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ import 'package:flutter/material.dart';
class DialogDropdownChooser<T> extends StatefulWidget {
final List<T>? choices;
final T? initialValue;
final Function(T?) onSelectionChanged;
final void Function(T?) onSelectionChanged;
final String Function(T value)? nameMap;

const DialogDropdownChooser(
{super.key,
this.choices,
this.initialValue,
required this.onSelectionChanged});
const DialogDropdownChooser({
super.key,
this.choices,
this.initialValue,
required this.onSelectionChanged,
this.nameMap,
});

@override
State<DialogDropdownChooser<T>> createState() =>
Expand Down Expand Up @@ -47,7 +50,7 @@ class _DialogDropdownChooserState<T> extends State<DialogDropdownChooser<T>> {
items: widget.choices?.map((T item) {
return DropdownMenuItem<T>(
value: item,
child: Text(item.toString()),
child: Text(widget.nameMap?.call(item) ?? item.toString()),
);
}).toList(),
value: selectedValue,
Expand Down
Loading

0 comments on commit 66863ca

Please sign in to comment.