202 lines
5.8 KiB
Dart
202 lines
5.8 KiB
Dart
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();
|
||
}
|
||
} |