Files
TelemetryMonitor/lib/layout/layout_controller.dart
2026-04-21 14:40:09 -03:00

202 lines
5.8 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../config/app_config.dart';
import 'chart_config.dart';
import 'grid_config.dart';
/// Owns the dashboard's display configuration.
///
/// - Grid shape and channel-to-cell assignment (GridConfig).
/// - Per-channel display config (ChartConfig × 16).
/// - Status indicator display names (List<String> × 8).
///
/// Persistence: shared_preferences under the `layout.` prefix.
class LayoutController extends ChangeNotifier {
LayoutController._({
required this.grid,
required this.charts,
required this.statusNames,
});
GridConfig grid;
/// Indexed 0..15 corresponding to channels 1..16.
List<ChartConfig> charts;
/// Indexed 0..7 corresponding to status1..status8.
List<String> statusNames;
static Future<LayoutController> load() async {
final p = await SharedPreferences.getInstance();
final gridJson = p.getString('layout.grid');
final chartsJson = p.getString('layout.charts');
final statusJson = p.getString('layout.statusNames');
final grid = gridJson != null
? GridConfig.fromJson(jsonDecode(gridJson) as Map<String, dynamic>)
: GridConfig(
rows: AppConfig.defaultGridRows,
cols: AppConfig.defaultGridCols,
);
final charts = chartsJson != null
? (jsonDecode(chartsJson) as List)
.map((e) => ChartConfig.fromJson(e as Map<String, dynamic>))
.toList()
: List<ChartConfig>.generate(
AppConfig.channelCount,
(i) => ChartConfig(
channel: i + 1,
name: AppConfig.defaultChannelNames[i],
enabled: i < AppConfig.defaultGridRows * AppConfig.defaultGridCols,
),
);
final statusNames = statusJson != null
? (jsonDecode(statusJson) as List).cast<String>()
: List<String>.from(AppConfig.defaultStatusNames);
final ctrl = LayoutController._(
grid: grid,
charts: charts,
statusNames: statusNames,
);
// First-run convenience: auto-assign enabled channels to grid cells if
// the saved grid is empty.
if (gridJson == null) {
ctrl.grid.autoAssign(
ctrl.charts.where((c) => c.enabled).map((c) => c.channel).toList(),
);
}
return ctrl;
}
Future<void> save() async {
final p = await SharedPreferences.getInstance();
await p.setString('layout.grid', jsonEncode(grid.toJson()));
await p.setString(
'layout.charts',
jsonEncode(charts.map((c) => c.toJson()).toList()),
);
await p.setString('layout.statusNames', jsonEncode(statusNames));
notifyListeners();
}
/// Channel 1..16 → ChartConfig.
ChartConfig configFor(int channel) => charts[channel - 1];
/// Replace state from another LayoutController (used by the Layout dialog
/// when applying edits from a working copy).
void copyFrom(LayoutController other) {
grid = GridConfig(
rows: other.grid.rows,
cols: other.grid.cols,
cellChannels: List<int?>.from(other.grid.cellChannels),
);
charts = other.charts
.map((c) => ChartConfig(
channel: c.channel,
name: c.name,
enabled: c.enabled,
yMode: c.yMode,
yMin: c.yMin,
yMax: c.yMax,
))
.toList();
statusNames = List<String>.from(other.statusNames);
notifyListeners();
}
LayoutController clone() {
return LayoutController._(
grid: GridConfig(
rows: grid.rows,
cols: grid.cols,
cellChannels: List<int?>.from(grid.cellChannels),
),
charts: charts
.map((c) => ChartConfig(
channel: c.channel,
name: c.name,
enabled: c.enabled,
yMode: c.yMode,
yMin: c.yMin,
yMax: c.yMax,
))
.toList(),
statusNames: List<String>.from(statusNames),
);
}
/// Mutators that notify on change.
void setChannelEnabled(int channel, bool enabled) {
final c = configFor(channel);
if (c.enabled == enabled) return;
c.enabled = enabled;
if (!enabled) {
// Also clear from any grid cell.
for (var i = 0; i < grid.cellChannels.length; i++) {
if (grid.cellChannels[i] == channel) grid.cellChannels[i] = null;
}
}
notifyListeners();
}
void setChannelName(int channel, String name) {
final c = configFor(channel);
if (c.name == name) return;
c.name = name;
notifyListeners();
}
void setYMode(int channel, YMode mode) {
final c = configFor(channel);
if (c.yMode == mode) return;
c.yMode = mode;
notifyListeners();
}
void setYRange(int channel, double yMin, double yMax) {
final c = configFor(channel);
if (c.yMin == yMin && c.yMax == yMax) return;
c.yMin = yMin;
c.yMax = yMax;
notifyListeners();
}
/// Mouse-wheel Y zoom on a chart. Switches mode to userZoomed and updates
/// the range, keeping the pivot screen-Y fixed.
void zoomY({
required int channel,
required double pivotValue,
required double factor,
}) {
final c = configFor(channel);
final span = c.yMax - c.yMin;
final newSpan = span * factor;
final fraction = (pivotValue - c.yMin) / span;
c.yMin = pivotValue - newSpan * fraction;
c.yMax = c.yMin + newSpan;
c.yMode = YMode.userZoomed;
notifyListeners();
}
void setStatusName(int statusIdx, String name) {
if (statusNames[statusIdx] == name) return;
statusNames[statusIdx] = name;
notifyListeners();
}
void setGridSize(int rows, int cols) {
grid.resize(rows, cols);
notifyListeners();
}
void assignCellToChannel(int cellNumber, int? channel) {
grid.assign(cellNumber, channel);
notifyListeners();
}
}