From 99afe496db51be67a19ebf6ea60993f53566e083 Mon Sep 17 00:00:00 2001 From: Patrick Schmidt Date: Thu, 31 Mar 2022 11:59:10 +0200 Subject: [PATCH] Custom MJPEG implementation and added swipe for next printer --- assets/translations/en.json | 17 +- .../connection/connection_state_view.dart | 19 +- .../dialog/editForm/range_edit_form_view.dart | 6 +- lib/ui/components/drawer/nav_drawer_view.dart | 8 +- lib/ui/components/mjpeg.dart | 351 ++++++++++++++++++ lib/ui/views/console/console_view.dart | 6 +- lib/ui/views/files/files_view.dart | 7 +- lib/ui/views/fullcam/full_cam_view.dart | 54 ++- lib/ui/views/fullcam/full_cam_viewmodel.dart | 50 ++- lib/ui/views/overview/overview_view.dart | 9 +- lib/ui/views/overview/overview_viewmodel.dart | 16 + lib/ui/views/overview/tabs/general_tab.dart | 73 ++-- .../printers/edit/printers_edit_view.dart | 16 +- pubspec.yaml | 1 - 14 files changed, 521 insertions(+), 112 deletions(-) create mode 100644 lib/ui/components/mjpeg.dart diff --git a/assets/translations/en.json b/assets/translations/en.json index 78345d22..88e10919 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -222,11 +222,18 @@ } } }, - "nav_drawer": { - "printer_settings": "Printer Settings", - "manage_printers": "Manager Printers", - "fetching_printers": "@:general.fetching printers...", - "footer": "Made with ❤️ by Patrick Schmidt\nCheckout the project's" + "components": { + "nav_drawer": { + "printer_settings": "Printer Settings", + "manage_printers": "Manager Printers", + "fetching_printers": "@:general.fetching printers...", + "footer": "Made with ❤️ by Patrick Schmidt\nCheckout the project's" + }, + "connection_watcher": { + "reconnect": "Reconnect", + "trying_connect": "Trying to connect ...", + "server_starting": "Server is starting..." + } }, "klipper_state": { "ready": "Ready", diff --git a/lib/ui/components/connection/connection_state_view.dart b/lib/ui/components/connection/connection_state_view.dart index 6a205ecc..f5f96c09 100644 --- a/lib/ui/components/connection/connection_state_view.dart +++ b/lib/ui/components/connection/connection_state_view.dart @@ -78,27 +78,26 @@ class ConnectionStateView SizedBox( height: 30, ), - Text("Disconnected!"), + Text('@:klipper_state.disconnected !').tr(), TextButton.icon( onPressed: model.onRetryPressed, - icon: Icon(Icons.stream), - label: Text("Reconnect")) + icon: Icon(Icons.restart_alt_outlined), + label: Text('components.connection_watcher.reconnect').tr()) ], ), ); case WebSocketState.connecting: return Center( - key: UniqueKey(), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - SpinKitPouringHourGlassRefined( - color: Theme.of(context).colorScheme.secondary, + SpinKitPulse( + color: Theme.of(context).colorScheme.primary, ), SizedBox( height: 30, ), - FadingText("Trying to connect ..."), + FadingText(tr('components.connection_watcher.trying_connect')), ], ), ); @@ -120,8 +119,8 @@ class ConnectionStateView ), TextButton.icon( onPressed: model.onRetryPressed, - icon: Icon(Icons.stream), - label: Text("Reconnect!!")) + icon: Icon(Icons.restart_alt_outlined), + label: Text('components.connection_watcher.reconnect').tr()) ], ), ); @@ -193,7 +192,7 @@ class ConnectionStateView ), title: Text(model.klippyState), ), - Text('Server is starting...') + Text('components.connection_watcher.server_starting').tr() ], ), )), diff --git a/lib/ui/components/dialog/editForm/range_edit_form_view.dart b/lib/ui/components/dialog/editForm/range_edit_form_view.dart index 5b607c60..d3ee5dfa 100644 --- a/lib/ui/components/dialog/editForm/range_edit_form_view.dart +++ b/lib/ui/components/dialog/editForm/range_edit_form_view.dart @@ -7,12 +7,12 @@ import 'package:stacked_services/stacked_services.dart'; class NumberEditDialogArguments { final num min; - final num max; + final num? max; final num current; final int fraction; NumberEditDialogArguments( - {this.min = 0, this.max = 100, required this.current, this.fraction = 0}); + {this.min = 0, this.max, required this.current, this.fraction = 0}); } class RangeEditFormDialogView extends StatelessWidget { @@ -46,7 +46,7 @@ class RangeEditFormDialogView extends StatelessWidget { initialValue: data.current.toDouble().toPrecision(data.fraction), min: data.min.toDouble(), - max: data.max.toDouble(), + max: (data.max?? 100).toDouble(), // divisions: (data.max + data.min.abs()).toInt(), autofocus: true, numberFormat: NumberFormat("####"), diff --git a/lib/ui/components/drawer/nav_drawer_view.dart b/lib/ui/components/drawer/nav_drawer_view.dart index 947c3a37..a82564fd 100644 --- a/lib/ui/components/drawer/nav_drawer_view.dart +++ b/lib/ui/components/drawer/nav_drawer_view.dart @@ -41,7 +41,7 @@ class NavigationDrawerWidget children: [ ExpansionTile( title: const Text( - 'nav_drawer.manage_printers', + 'components.nav_drawer.manage_printers', style: TextStyle(color: Colors.white), ).tr(), children: [ @@ -94,7 +94,7 @@ class NavigationDrawerWidget alignment: Alignment.center, padding: EdgeInsets.only(bottom: 20, top: 10), child: RichText( - text: TextSpan(text: 'nav_drawer.footer'.tr(), children: [ + text: TextSpan(text: 'components.nav_drawer.footer'.tr(), children: [ new TextSpan( text: ' GitHub ', style: new TextStyle(color: Colors.blue), @@ -153,7 +153,7 @@ class NavigationDrawerWidget } else { widgetsToReturn = [ ListTile( - title: FadingText('nav_drawer.fetching_printers'.tr()), + title: FadingText('components.nav_drawer.fetching_printers'.tr()), contentPadding: const EdgeInsets.only(left: 32, right: 16), ), ]; @@ -208,7 +208,7 @@ class NavigationDrawerWidget ), IconButton( onPressed: onClicked, - tooltip: 'nav_drawer.printer_settings'.tr(), + tooltip: 'components.nav_drawer.printer_settings'.tr(), icon: Icon( FlutterIcons.settings_fea, color: Colors.white, diff --git a/lib/ui/components/mjpeg.dart b/lib/ui/components/mjpeg.dart new file mode 100644 index 00000000..dd3a8b9d --- /dev/null +++ b/lib/ui/components/mjpeg.dart @@ -0,0 +1,351 @@ +import 'dart:async'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_spinkit/flutter_spinkit.dart'; +import 'package:http/http.dart'; +import 'package:mobileraker/app/app_setup.logger.dart'; +import 'package:mobileraker/ui/components/ease_in.dart'; +import 'package:progress_indicators/progress_indicators.dart'; +import 'package:stacked/stacked.dart'; + +typedef StreamConnectedBuilder = Widget Function( + BuildContext context, Transform imageTransformed); + +class Mjpeg extends ViewModelBuilderWidget { + final String feedUri; + final List stackChildren; + final Matrix4? transform; + final BoxFit? fit; + final double? width; + final double? height; + final Duration timeout; + final Map headers; + final bool showFps; + final StreamConnectedBuilder? imageBuilder; + + const Mjpeg({ + Key? key, + required this.feedUri, + this.stackChildren = const [], + this.transform, + this.fit, + this.width, + this.height, + this.timeout = const Duration(seconds: 5), + this.headers = const {}, + this.showFps = false, + this.imageBuilder, + }) : super(key: key); + + @override + MjpegViewModel viewModelBuilder(BuildContext context) => + MjpegViewModel(feedUri, timeout, headers); + + @override + Widget builder(BuildContext context, MjpegViewModel model, Widget? child) { + if (!model.isBusy) { + if (model.hasError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.error_outline), + SizedBox( + height: 30, + ), + Text(model.modelError.toString(), + textAlign: TextAlign.center, + style: TextStyle(color: Theme.of(context).errorColor)), + TextButton.icon( + onPressed: model.onRetryPressed, + icon: Icon(Icons.restart_alt_outlined), + label: Text('components.connection_watcher.reconnect').tr()) + ], + ); + } else if (model.dataReady) { + Widget img = Image( + image: model.data!, + width: width, + height: height, + gaplessPlayback: true, + fit: fit, + ); + if (transform == null) { + return img; + } else { + Transform transformWidget = Transform( + alignment: Alignment.center, + transform: transform!, + child: img, + ); + + return EaseIn( + child: Stack( + children: [ + (imageBuilder == null) + ? transformWidget + : imageBuilder!(context, transformWidget), + if (showFps) + Positioned.fill( + child: Align( + alignment: Alignment.topRight, + child: Container( + padding: EdgeInsets.all(4), + margin: EdgeInsets.all(5), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.secondary, + borderRadius: BorderRadius.all(Radius.circular(5))), + child: Text( + 'FPS: ${model.fps.toStringAsFixed(1)}', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondary), + )), + ), + ), + ...stackChildren + ], + ), + ); + } + } + } + + return Column( + children: [ + SpinKitDancingSquare( + color: Theme.of(context).colorScheme.primary, + ), + SizedBox( + height: 15, + ), + FadingText(tr('components.connection_watcher.trying_connect')) + ], + ); + // if (errorState.value != null) { + // return SizedBox( + // width: width, + // height: height, + // child: error == null + // ? Center( + // child: Padding( + // padding: const EdgeInsets.all(8.0), + // child: Text( + // '${errorState.value}', + // textAlign: TextAlign.center, + // style: TextStyle(color: Colors.red), + // ), + // ), + // ) + // : error!(context, errorState.value!.first, errorState.value!.last), + // ); + // } + + // if (image.value == null) { + // return SizedBox( + // width: width, + // height: height, + // child: loading == null + // ? Center(child: CircularProgressIndicator()) + // : loading!(context)); + // } + } +} + +class MjpegViewModel extends StreamViewModel + with WidgetsBindingObserver { + final _logger = getLogger('MjpegViewModel'); + final String feedUri; + final Duration timeout; + final Map headers; + + late _StreamManager _manager = _StreamManager(feedUri, headers, timeout); + int _frameCnt = 0; + double _lastFps = 0; + + double get fps => _lastFps; + DateTime? _start; + + MjpegViewModel(this.feedUri, this.timeout, this.headers); + + @override + Stream get stream => _manager.mjpegStream; + + onRetryPressed() { + setBusy(true); + _manager.start(); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + switch (state) { + case AppLifecycleState.resumed: + _manager.start(); + break; + + case AppLifecycleState.paused: + _manager.stop(); + break; + default: + // Do Nothing + } + } + + @override + void initialise() { + super.initialise(); + if (!initialised) { + WidgetsBinding.instance?.addObserver(this); + setBusy(true); + _manager.start(); + } + } + + @override + void onData(MemoryImage? data) { + setBusy(false); + if (data != null) { + _frameCnt++; + DateTime now = DateTime.now(); + + if (_start == null) { + _start = now; + return; + } + var passed = now.difference(_start!).inSeconds; + if (passed >= 1) { + _lastFps = _frameCnt / passed; + _frameCnt = 0; + _start = now; + } + } + } + + @override + void onError(error) { + _logger.e('Error: $error'); + setBusy(false); + } + + @override + void dispose() { + super.dispose(); + _manager.dispose(); + WidgetsBinding.instance?.removeObserver(this); + } +} + +class _StreamManager { + final _logger = getLogger('_StreamManager'); + + // Jpeg Magic Nubmers: https://www.file-recovery.com/jpg-signature-format.htm + static const _TRIGGER = 0xFF; + static const _SOI = 0xD8; + static const _EOI = 0xD9; + + final String feedUri; + final Duration _timeout; + final Map headers; + final Client _httpClient = Client(); + + final StreamController _mjpegStreamController = + StreamController.broadcast(); + + Stream get mjpegStream => _mjpegStreamController.stream; + + StreamSubscription? _subscription; + + _StreamManager(this.feedUri, this.headers, this._timeout); + + void stop() { + _logger.i('STOPPING STREAM!'); + _subscription?.cancel(); + } + + void start() async { + _subscription?.cancel(); // Ensure its clear to start a new stream! + try { + final request = Request("GET", Uri.parse(feedUri)); + request.headers.addAll(headers); + final StreamedResponse response = await _httpClient.send(request).timeout( + _timeout); //timeout is to prevent process to hang forever in some case + + if (response.statusCode >= 200 && response.statusCode < 300) { + _subscription = response.stream + .listen(_onData, onError: _onError, cancelOnError: true); + } else { + _mjpegStreamController.addError( + HttpException('Stream returned ${response.statusCode} status'), + StackTrace.current); + } + } catch (error, stack) { + // we ignore those errors in case play/pause is triggers + if (!error + .toString() + .contains('Connection closed before full header was received')) { + if (!_mjpegStreamController.isClosed) + _mjpegStreamController.addError(error, stack); + } + } + } + + BytesBuilder _byteBuffer = BytesBuilder(); + int _lastByte = 0x00; + + void _sendImage(Uint8List bytes) async { + if (bytes.isNotEmpty) { + _mjpegStreamController.add(MemoryImage(bytes)); + } + } + + _onData(List byteChunk) { + if (_byteBuffer.isNotEmpty && _lastByte == _TRIGGER) { + if (byteChunk.first == _EOI) { + _byteBuffer.addByte(byteChunk.first); + + _sendImage(_byteBuffer.takeBytes()); + } + } + + for (var i = 0; i < byteChunk.length; i++) { + final int cur = byteChunk[i]; + final int next = (i != byteChunk.length - 1) ? byteChunk[i + 1] : 0x00; + + if (cur == _TRIGGER && next == _SOI) { + // Detect start of JPEG + _byteBuffer.addByte(_TRIGGER); + } else if (_byteBuffer.isNotEmpty && cur == _TRIGGER && next == _EOI) { + // Detect end of JPEG + _byteBuffer.addByte(cur); + _byteBuffer.addByte(next); + _sendImage(_byteBuffer.takeBytes()); + i++; + } else if (_byteBuffer.isNotEmpty) { + // Prevent it from adding other than jpeg bytes + _byteBuffer.addByte(cur); + } + } + } + + _onError(error, stack) { + try { + _mjpegStreamController.addError(error, stack); + } catch (ex) {} + dispose(); + } + + Future dispose() async { + await _subscription?.cancel(); + _subscription = null; + _mjpegStreamController.close(); + + _httpClient.close(); + _logger.i('DISPOSED'); + } +} diff --git a/lib/ui/views/console/console_view.dart b/lib/ui/views/console/console_view.dart index b448bff3..fcf0deeb 100644 --- a/lib/ui/views/console/console_view.dart +++ b/lib/ui/views/console/console_view.dart @@ -8,6 +8,7 @@ import 'package:mobileraker/ui/components/connection/connection_state_view.dart' import 'package:mobileraker/ui/components/ease_in.dart'; import 'package:mobileraker/ui/components/drawer/nav_drawer_view.dart'; import 'package:mobileraker/ui/views/console/console_viewmodel.dart'; +import 'package:progress_indicators/progress_indicators.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:stacked/stacked.dart'; @@ -128,13 +129,14 @@ class ConsoleView extends ViewModelBuilderWidget { key: UniqueKey(), mainAxisAlignment: MainAxisAlignment.center, children: [ - SpinKitSpinningLines( + SpinKitDoubleBounce( color: themeData.colorScheme.primary, + size: 100, ), SizedBox( height: 30, ), - Text('pages.console.fetching_console').tr() + FadingText(tr('pages.console.fetching_console')) ], ), ); diff --git a/lib/ui/views/files/files_view.dart b/lib/ui/views/files/files_view.dart index 1c15a507..b997675f 100644 --- a/lib/ui/views/files/files_view.dart +++ b/lib/ui/views/files/files_view.dart @@ -11,6 +11,7 @@ import 'package:mobileraker/ui/components/connection/connection_state_view.dart' import 'package:mobileraker/ui/components/ease_in.dart'; import 'package:mobileraker/ui/components/drawer/nav_drawer_view.dart'; import 'package:mobileraker/ui/views/files/files_viewmodel.dart'; +import 'package:progress_indicators/progress_indicators.dart'; import 'package:pull_to_refresh/pull_to_refresh.dart'; import 'package:shimmer/shimmer.dart'; import 'package:stacked/stacked.dart'; @@ -116,13 +117,13 @@ class FilesView extends ViewModelBuilderWidget { key: UniqueKey(), mainAxisAlignment: MainAxisAlignment.center, children: [ - SpinKitSpinningLines( - color: Theme.of(context).colorScheme.primary, + SpinKitRipple( + color: Theme.of(context).colorScheme.primary,size: 100, ), SizedBox( height: 30, ), - Text('pages.files.fetching_files').tr(), + FadingText(tr('pages.files.fetching_files')), // Text('Fetching printer ...') ], ), diff --git a/lib/ui/views/fullcam/full_cam_view.dart b/lib/ui/views/fullcam/full_cam_view.dart index 9c04c60a..ba2fb0f7 100644 --- a/lib/ui/views/fullcam/full_cam_view.dart +++ b/lib/ui/views/fullcam/full_cam_view.dart @@ -1,8 +1,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_mjpeg/flutter_mjpeg.dart'; import 'package:mobileraker/domain/webcam_setting.dart'; import 'package:mobileraker/ui/components/interactive_viewer_center.dart'; +import 'package:mobileraker/ui/components/mjpeg.dart'; import 'package:mobileraker/ui/views/fullcam/full_cam_viewmodel.dart'; import 'package:stacked/stacked.dart'; @@ -19,22 +19,46 @@ class FullCamView extends ViewModelBuilderWidget { Widget builder(BuildContext context, FullCamViewModel model, Widget? child) { return Scaffold( body: Container( - child: Stack( - alignment: Alignment.center, - children: [ + child: Stack(alignment: Alignment.center, children: [ CenterInteractiveViewer( - constrained: true, - minScale: 1, - maxScale: 10, - child: Transform( - alignment: Alignment.center, + constrained: true, + minScale: 1, + maxScale: 10, + child: Mjpeg( + key: ValueKey(model.selectedCam.url), + feedUri: model.selectedCam.url, + showFps: true, transform: model.transformMatrix, - child: Mjpeg( - isLive: true, - stream: model.selectedCam!.url, - ) - ), - ), + stackChildren: [ + if (model.dataReady) + Positioned.fill( + child: Align( + alignment: Alignment.topLeft, + child: Container( + margin: EdgeInsets.only(top: 5, left: 2), + child: Text( + '${model.nozzleString} \n' + '${model.bedString}', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith( + color: Theme.of(context) + .colorScheme + .onSecondary), + )), + ), + ), + if (model.showProgress) + Positioned.fill( + child: Align( + alignment: Alignment.bottomCenter, + child: LinearProgressIndicator( + value: model.printProgress, + )), + ) + ], + )), if (model.webcams.length > 1) Align( alignment: Alignment.bottomCenter, diff --git a/lib/ui/views/fullcam/full_cam_viewmodel.dart b/lib/ui/views/fullcam/full_cam_viewmodel.dart index 466e1d8c..c8ebdc3b 100644 --- a/lib/ui/views/fullcam/full_cam_viewmodel.dart +++ b/lib/ui/views/fullcam/full_cam_viewmodel.dart @@ -1,32 +1,42 @@ import 'dart:math'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/domain/printer_setting.dart'; import 'package:mobileraker/domain/webcam_setting.dart'; +import 'package:mobileraker/dto/machine/print_stats.dart'; +import 'package:mobileraker/dto/machine/printer.dart'; import 'package:mobileraker/service/machine_service.dart'; +import 'package:mobileraker/service/printer_service.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; -class FullCamViewModel extends BaseViewModel { + +class FullCamViewModel extends StreamViewModel { final _navigationService = locator(); final _machineService = locator(); - WebcamSetting? selectedCam; + WebcamSetting selectedCam; FullCamViewModel(this.selectedCam); PrinterSetting? get _printerSetting => _machineService.selectedMachine.valueOrNull; + PrinterService? get _printerService => _printerSetting?.printerService; + + @override + Stream get stream => _printerService!.printerStream; + double get yTransformation { - if (selectedCam?.flipHorizontal ?? false) + if (selectedCam.flipHorizontal) return pi; else return 0; } double get xTransformation { - if (selectedCam?.flipVertical ?? false) + if (selectedCam.flipVertical) return pi; else return 0; @@ -36,15 +46,33 @@ class FullCamViewModel extends BaseViewModel { ..rotateX(xTransformation) ..rotateY(yTransformation); - // double get nozzleCurrent => this.data!.extruder.temperature; - // - // double get nozzleTarget => this.data!.extruder.target; - // - // double get bedCurrent => this.data!.heaterBed.temperature; - // - // double get bedTarget => this.data!.heaterBed.target; + double get _nozzleCurrent => this.data?.extruder.temperature ?? 0; + + double get _nozzleTarget => this.data?.extruder.target ?? 0; + + double get _bedCurrent => this.data?.heaterBed.temperature ?? 0; + + double get _bedTarget => this.data?.heaterBed.target ?? 0; + + double get printProgress => data?.virtualSdCard.progress ?? 0; + + bool get showProgress => + dataReady && data?.print.state == PrintState.printing; + + String get nozzleString { + String cur = _nozzleCurrent.toStringAsFixed(1); + if (_nozzleTarget > 0) cur += '/${_nozzleTarget.toStringAsFixed(0)}'; + return tr('pages.overview.general.temp_preset_card.h_temp', args: [cur]); + } + + String get bedString { + String cur = _bedCurrent.toStringAsFixed(1); + if (_bedTarget > 0) cur += '/${_bedTarget.toStringAsFixed(0)}'; + return tr('pages.overview.general.temp_preset_card.b_temp', args: [cur]); + } onWebcamSettingSelected(WebcamSetting? webcamSetting) { + if (webcamSetting == null) return; selectedCam = webcamSetting; notifyListeners(); } diff --git a/lib/ui/views/overview/overview_view.dart b/lib/ui/views/overview/overview_view.dart index c2aa464e..ee64a647 100644 --- a/lib/ui/views/overview/overview_view.dart +++ b/lib/ui/views/overview/overview_view.dart @@ -24,9 +24,12 @@ class OverView extends ViewModelBuilderWidget { Widget builder(BuildContext context, OverViewModel model, Widget? child) => Scaffold( appBar: AppBar( - title: Text( - model.title, - overflow: TextOverflow.fade, + title: GestureDetector( + child: Text( + model.title, + overflow: TextOverflow.fade, + ), + onHorizontalDragEnd: model.onHorizontalDragEnd, ), actions: [ IconButton( diff --git a/lib/ui/views/overview/overview_viewmodel.dart b/lib/ui/views/overview/overview_viewmodel.dart index 527bf7ae..dd5577fa 100644 --- a/lib/ui/views/overview/overview_viewmodel.dart +++ b/lib/ui/views/overview/overview_viewmodel.dart @@ -1,4 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/widgets.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/domain/printer_setting.dart'; import 'package:mobileraker/dto/machine/printer.dart'; @@ -127,6 +128,21 @@ class OverViewModel extends MultipleStreamViewModel { bool get canUseEms => isServerAvailable && server.klippyState == KlipperState.ready; + onHorizontalDragEnd(DragEndDetails endDetails) { + double primaryVelocity = endDetails.primaryVelocity ?? 0; + if (primaryVelocity < 0) { + // Page forwards + _machineService.selectPreviousMachine(); + } else if (primaryVelocity > 0) { + // Page backwards + _machineService.selectNextMachine(); + } + } + + onPanUpdate(DragUpdateDetails updateDetails) { + print(updateDetails); + } + // onTitleSwipeDetection(SwipeDirection dir) { // switch (dir) { // diff --git a/lib/ui/views/overview/tabs/general_tab.dart b/lib/ui/views/overview/tabs/general_tab.dart index 8e33ed89..11c0f0aa 100644 --- a/lib/ui/views/overview/tabs/general_tab.dart +++ b/lib/ui/views/overview/tabs/general_tab.dart @@ -5,7 +5,6 @@ import 'package:enum_to_string/enum_to_string.dart'; import 'package:flip_card/flip_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_icons/flutter_icons.dart'; -import 'package:flutter_mjpeg/flutter_mjpeg.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/domain/temperature_preset.dart'; import 'package:mobileraker/dto/machine/print_stats.dart'; @@ -13,6 +12,7 @@ import 'package:mobileraker/dto/machine/toolhead.dart'; import 'package:mobileraker/dto/server/klipper.dart'; import 'package:mobileraker/ui/components/HorizontalScrollIndicator.dart'; import 'package:mobileraker/ui/components/card_with_button.dart'; +import 'package:mobileraker/ui/components/mjpeg.dart'; import 'package:mobileraker/ui/components/range_selector.dart'; import 'package:mobileraker/ui/components/refresh_printer.dart'; import 'package:mobileraker/ui/views/overview/tabs/general_tab_viewmodel.dart'; @@ -268,52 +268,43 @@ class CamCard extends ViewModelWidget { }).toList()) : null, ), - Padding( - padding: const EdgeInsets.fromLTRB(8, 8, 8, 10), - child: Container( - constraints: BoxConstraints(minHeight: minWebCamHeight), - child: Stack(children: [ - Center( - child: Transform( - alignment: Alignment.center, - transform: model.transformMatrix, - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(5)), - child: Mjpeg( - isLive: true, - stream: model.webCamUrl, - error: (context, error, stack) { - return Center( - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - 'error', - textAlign: TextAlign.center, - style: TextStyle(color: Theme.of(context).errorColor), - ), - ), - ); - }, - ), - )), - ), - Positioned.fill( - child: Align( - alignment: Alignment.bottomRight, - child: IconButton( - icon: Icon(Icons.aspect_ratio_outlined), - tooltip: - 'pages.overview.general.cam_card.fullscreen'.tr(), - onPressed: model.onFullScreenTap, + Container( + padding: const EdgeInsets.fromLTRB(8, 8, 8, 10), + constraints: BoxConstraints(minHeight: minWebCamHeight), + child: Center( + child: Mjpeg( + key: ValueKey(model.webCamUrl), + imageBuilder: _imageBuilder, + feedUri: model.webCamUrl, + transform: model.transformMatrix, + showFps: true, + stackChildren: [ + Positioned.fill( + child: Align( + alignment: Alignment.bottomRight, + child: IconButton( + color: Colors.white, + icon: Icon(Icons.aspect_ratio), + tooltip: + 'pages.overview.general.cam_card.fullscreen' + .tr(), + onPressed: model.onFullScreenTap, + ), ), ), - ), - ]), - )), + ], + )), + ), ], ), ); } + + Widget _imageBuilder(BuildContext context, Transform imageTransformed) { + return ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(5)), + child: imageTransformed); + } } class TemperatureCard extends ViewModelWidget { diff --git a/lib/ui/views/printers/edit/printers_edit_view.dart b/lib/ui/views/printers/edit/printers_edit_view.dart index ddbdfd23..c9b552c2 100644 --- a/lib/ui/views/printers/edit/printers_edit_view.dart +++ b/lib/ui/views/printers/edit/printers_edit_view.dart @@ -401,6 +401,7 @@ class _WebCamItemState extends State<_WebCamItem> { FormBuilderTextField( decoration: InputDecoration( labelText: 'pages.printer_edit.general.displayname'.tr(), + suffix: IconButton(icon: Icon(Icons.delete), onPressed: () => widget.model.onWebCamRemove(widget.cam),) ), name: '${widget.cam.uuid}-camName', initialValue: widget.cam.name, @@ -436,13 +437,6 @@ class _WebCamItemState extends State<_WebCamItem> { initialValue: widget.cam.flipHorizontal, name: '${widget.cam.uuid}-camFH', activeColor: Theme.of(context).colorScheme.primary, - ), - Align( - alignment: Alignment.centerLeft, - child: ElevatedButton( - onPressed: () => widget.model.onWebCamRemove(widget.cam), - child: const Text('general.remove').tr(), - ), ) ])); } @@ -577,6 +571,7 @@ class _TempPresetItemState extends State<_TempPresetItem> { FormBuilderTextField( decoration: InputDecoration( labelText: 'pages.printer_edit.general.displayname'.tr(), + suffix: IconButton(icon: Icon(Icons.delete), onPressed: () => model.onTempPresetRemove(temperaturePreset),) ), name: '${temperaturePreset.uuid}-presetName', initialValue: temperaturePreset.name, @@ -617,13 +612,6 @@ class _TempPresetItemState extends State<_TempPresetItem> { ], ), keyboardType: TextInputType.number, - ), - Align( - alignment: Alignment.centerLeft, - child: ElevatedButton( - onPressed: () => model.onTempPresetRemove(temperaturePreset), - child: Text('general.remove').tr(), - ), ) ])); } diff --git a/pubspec.yaml b/pubspec.yaml index 0f5c9d57..5ce0158d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -36,7 +36,6 @@ dependencies: percent_indicator: ^4.0.0 - flutter_mjpeg: ^2.0.1 logger: # flutter_icons: ^1.1.0 flutter_icons: