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 × 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 charts; /// Indexed 0..7 corresponding to status1..status8. List statusNames; static Future 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) : GridConfig( rows: AppConfig.defaultGridRows, cols: AppConfig.defaultGridCols, ); final charts = chartsJson != null ? (jsonDecode(chartsJson) as List) .map((e) => ChartConfig.fromJson(e as Map)) .toList() : List.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() : List.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 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.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.from(other.statusNames); notifyListeners(); } LayoutController clone() { return LayoutController._( grid: GridConfig( rows: grid.rows, cols: grid.cols, cellChannels: List.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.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(); } }