import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:my_app/appeditor/custom/AppEditorWidgetContext.dart'; // Reads tilt variables or the JSON output of the page accelerometer action. Widget buildCustomWidget(BuildContext context, AppEditorWidgetContext app) { return LevelWidget(app: app); } class LevelWidget extends StatefulWidget { const LevelWidget({super.key, required this.app}); final AppEditorWidgetContext app; @override State createState() => _LevelWidgetState(); } class _LevelWidgetState extends State { late String _xVariable; late String _yVariable; late Stream _xValues; late Stream _yValues; String? _sensorVariable; double? _tilt(dynamic data, String axis) { if (data is! Map || data['status'] != 'active' || data['sensor'] != 'accelerometer') return null; final x = (data['x'] as num?)?.toDouble(); final y = (data['y'] as num?)?.toDouble(); final z = (data['z'] as num?)?.toDouble(); if (x == null || y == null || z == null || !x.isFinite || !y.isFinite || !z.isFinite || x * x + y * y + z * z < 0.01) return null; return axis == 'x' ? math.atan2(x, math.sqrt(y * y + z * z)) * 180 / math.pi : math.atan2(y, math.sqrt(x * x + z * z)) * 180 / math.pi; } void _bind() { _xVariable = widget.app.params['xVariable'] as String? ?? 'level_x'; _yVariable = widget.app.params['yVariable'] as String? ?? 'level_y'; _sensorVariable = widget.app.params['sensorVariable'] as String?; if (_sensorVariable != null && _sensorVariable!.isNotEmpty) { _xValues = widget.app.watchVar(_sensorVariable!).map((data) => _tilt(data, 'x')); _yValues = widget.app.watchVar(_sensorVariable!).map((data) => _tilt(data, 'y')); } else { _xValues = widget.app.watchVar(_xVariable); _yValues = widget.app.watchVar(_yVariable); } } @override void initState() { super.initState(); _bind(); } @override void didUpdateWidget(covariant LevelWidget oldWidget) { super.didUpdateWidget(oldWidget); final args = widget.app.params; if (_sensorVariable != args['sensorVariable'] || _xVariable != (args['xVariable'] as String? ?? 'level_x') || _yVariable != (args['yVariable'] as String? ?? 'level_y') || !identical(widget.app, oldWidget.app)) { _bind(); } } double _number(dynamic value, double fallback) { final result = value is num ? value.toDouble() : double.tryParse('$value'); return result != null && result.isFinite ? result : fallback; } Future _publish(String name, double value) async { try { await widget.app.addVar(name: name, value: value, overwrite: true); } catch (error) { widget.app.showMessage('ثبت شیب ممکن نشد: $error'); } } Future _reset() async { await _publish(_xVariable, 0); if (!mounted) return; await _publish(_yVariable, 0); if (!mounted || !widget.app.hasAction('onReset')) return; try { await widget.app.runAction('onReset'); } catch (error) { widget.app.showMessage('اجرای عملکرد ممکن نشد: $error'); } } @override Widget build(BuildContext context) { final args = widget.app.params; final manual = (_sensorVariable == null || _sensorVariable!.isEmpty) && args['manual'] != false; final maxTilt = _number(args['maxTilt'], 30).abs().clamp(1.0, 90.0); final tolerance = _number(args['tolerance'], 1).abs().clamp(0.0, maxTilt); return StreamBuilder( stream: _xValues, builder: (context, xSnapshot) => StreamBuilder( stream: _yValues, builder: (context, ySnapshot) { final x = _number(xSnapshot.data, _number(args['initialX'], 0)); final y = _number(ySnapshot.data, _number(args['initialY'], 0)); final hasData = xSnapshot.hasData && ySnapshot.hasData; final centered = hasData && x.abs() <= tolerance && y.abs() <= tolerance; final color = centered ? Colors.green : Colors.orange; final dx = (x / maxTilt).clamp(-1.0, 1.0) * 80; final dy = -(y / maxTilt).clamp(-1.0, 1.0) * 80; final distance = math.sqrt(dx * dx + dy * dy); final scale = distance > 80 ? 80 / distance : 1.0; return Card( child: Padding( padding: const EdgeInsets.all(16), child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( args['title'] as String? ?? 'تراز دو محوره', style: Theme.of(context).textTheme.titleLarge, ), const SizedBox(height: 8), Text( manual ? 'حالت دستی — داده سنسور خوانده نمی‌شود' : (_sensorVariable?.isNotEmpty == true ? 'شیب از شتاب‌سنج؛ گوشی را ثابت نگه دارید' : 'شیب از متغیرهای اپ'), ), const SizedBox(height: 16), SizedBox.square( dimension: 210, child: Stack( alignment: Alignment.center, children: [ Container( decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: color, width: 2), ), ), const SizedBox( height: 1, width: 190, child: ColoredBox(color: Colors.grey), ), const SizedBox( height: 190, width: 1, child: ColoredBox(color: Colors.grey), ), Container( width: 38, height: 38, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all(color: Colors.grey), ), ), Transform.translate( offset: Offset(dx * scale, dy * scale), child: Container( width: 24, height: 24, decoration: BoxDecoration( shape: BoxShape.circle, color: color, ), ), ), ], ), ), const SizedBox(height: 12), Text( 'X: ${x.toStringAsFixed(1)}° Y: ${y.toStringAsFixed(1)}°', textDirection: TextDirection.ltr, ), Text( !manual && !hasData ? 'در انتظار داده؛ وضعیت تراز دستگاه مشخص نیست.' : centered ? 'مقادیر ورودی در محدوده ترازند' : 'مقادیر ورودی خارج از محدوده ترازند', ), if (manual) ...[ const Text('شیب افقی'), Directionality( textDirection: TextDirection.ltr, child: Slider( min: -maxTilt, max: maxTilt, value: x.clamp(-maxTilt, maxTilt), onChanged: (value) => _publish(_xVariable, value), ), ), const Text('شیب عمودی'), Directionality( textDirection: TextDirection.ltr, child: Slider( min: -maxTilt, max: maxTilt, value: y.clamp(-maxTilt, maxTilt), onChanged: (value) => _publish(_yVariable, value), ), ), OutlinedButton( onPressed: _reset, child: const Text('صفر کردن ورودی‌ها'), ), ], ], ), ), ); }, ), ); } }