diff --git a/lib/app/app_setup.dart b/lib/app/app_setup.dart index 8f1909ac..63de75b2 100644 --- a/lib/app/app_setup.dart +++ b/lib/app/app_setup.dart @@ -7,6 +7,7 @@ import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/machine/temperature_preset.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/service/machine_service.dart'; +import 'package:mobileraker/ui/views/files/details/file_details_view.dart'; import 'package:mobileraker/ui/views/files/files_view.dart'; import 'package:mobileraker/ui/views/fullcam/full_cam_view.dart'; import 'package:mobileraker/ui/views/overview/overview_view.dart'; @@ -26,6 +27,7 @@ import 'package:stacked_services/stacked_services.dart'; MaterialRoute(page: PrintersAdd), MaterialRoute(page: PrintersEdit), MaterialRoute(page: FilesView), + MaterialRoute(page: FileDetailView), ], dependencies: [ LazySingleton(classType: NavigationService), LazySingleton(classType: SnackbarService), diff --git a/lib/WebSocket.dart b/lib/datasource/websocket_wrapper.dart similarity index 92% rename from lib/WebSocket.dart rename to lib/datasource/websocket_wrapper.dart index b913cb67..4fc732c6 100644 --- a/lib/WebSocket.dart +++ b/lib/datasource/websocket_wrapper.dart @@ -33,10 +33,10 @@ class WebSocketWrapper { BehaviorSubject stateStream = BehaviorSubject.seeded(WebSocketState.disconnected); - WebSocketState get state => stateStream.value; + WebSocketState get _state => stateStream.value; - set state(WebSocketState newState) { - _logger.i("$state ➝ $newState"); + set _state(WebSocketState newState) { + _logger.i("$_state ➝ $newState"); stateStream.add(newState); } @@ -66,7 +66,7 @@ class WebSocketWrapper { _tryConnect() { _logger.i("Trying to connect to $url with APIkey: `${apiKey??'NO-APIKEY'}`"); - state = WebSocketState.connecting; + _state = WebSocketState.connecting; reset(); WebSocket.connect(url.toString(), headers: _headers) @@ -86,8 +86,8 @@ class WebSocketWrapper { ); // Send a req msg to be sure we are connected! - if (state != WebSocketState.connected) { - state = WebSocketState.connected; + if (_state != WebSocketState.connected) { + _state = WebSocketState.connected; } }, onError: _onWSError); } @@ -113,7 +113,7 @@ class WebSocketWrapper { /// Ensures that the ws is still connected. /// ---------------------------------------------------------- ensureConnection() { - if (state != WebSocketState.connected && state != WebSocketState.connecting) + if (_state != WebSocketState.connected && _state != WebSocketState.connecting) initCommunication(); } @@ -176,7 +176,7 @@ class WebSocketWrapper { _onWSError(error) { _logger.e("WS-Stream error: $error"); errorReason = error; - state = WebSocketState.error; + _state = WebSocketState.error; } bool get requiresAPIKey { @@ -192,11 +192,11 @@ class WebSocketWrapper { } _onWSClosesNormal() { - var t = state; + var t = _state; if (t != WebSocketState.error) { t = WebSocketState.disconnected; } - if (!stateStream.isClosed) state = t; + if (!stateStream.isClosed) _state = t; initCommunication(); _logger.i("WS-Stream close normal!"); } diff --git a/lib/dto/files/gcode_file.dart b/lib/dto/files/gcode_file.dart index 5e75c428..57e59e84 100644 --- a/lib/dto/files/gcode_file.dart +++ b/lib/dto/files/gcode_file.dart @@ -60,8 +60,11 @@ class GCodeFile { /// Path to the location/directory where the file is located String parentPath; - - GCodeFile({required this.name, required this.size, required this.modified, required this.parentPath}); + GCodeFile( + {required this.name, + required this.size, + required this.modified, + required this.parentPath}); GCodeFile.fromJson(Map json, this.parentPath) { this.name = json['filename']; @@ -101,8 +104,29 @@ class GCodeFile { String? get smallImagePath { //ToDo: Filter for small <.< - if (thumbnails.isNotEmpty) - return thumbnails.first.relativePath; + if (thumbnails.isNotEmpty) return thumbnails.first.relativePath; + } + + String? get bigImagePath { + //ToDo: Filter for big <.< + if (thumbnails.isNotEmpty) return thumbnails.last.relativePath; + } + + DateTime? get modifiedDate { + return DateTime.fromMillisecondsSinceEpoch(modified.toInt() * 1000); + } + + DateTime? get lastPrintDate { + return DateTime.fromMillisecondsSinceEpoch( + (printStartTime?.toInt() ?? 0) * 1000); + } + + /// combines parentpath and name to the correct path to request a print! + String get pathForPrint { + List split = parentPath.split('/'); + split.removeAt(0); // remove 'gcodes' + split.add(name); + return split.join('/'); } @override diff --git a/lib/dto/machine/printer.dart b/lib/dto/machine/printer.dart index a885bb15..99f39f7a 100644 --- a/lib/dto/machine/printer.dart +++ b/lib/dto/machine/printer.dart @@ -1,28 +1,8 @@ -import 'dart:ui'; - -import 'package:flutter/material.dart'; import 'package:mobileraker/dto/config/config_file.dart'; enum PrinterAxis { X, Y, Z, E } -enum PrinterState { ready, error, shutdown, startup, disconnected } enum PrintState { standby, printing, paused, complete, error } -String printerStateName(PrinterState printerState) { - switch (printerState) { - case PrinterState.ready: - return "Ready"; - case PrinterState.shutdown: - return "Shutdown"; - case PrinterState.startup: - return "Starting"; - case PrinterState.disconnected: - return "Disconnected"; - case PrinterState.error: - default: - return "Error"; - } -} - String printStateName(PrintState printState) { switch (printState) { case PrintState.standby: @@ -40,8 +20,6 @@ String printStateName(PrintState printState) { } class Printer { - PrinterState state = PrinterState.error; //Matches ServerState - Toolhead toolhead = Toolhead(); Extruder extruder = Extruder(); HeaterBed heaterBed = HeaterBed(); @@ -61,8 +39,6 @@ class Printer { List queryableObjects = []; List gcodeMacros = []; - String get stateName => printerStateName(state); - double get zOffset => gCodeMove.homingOrigin[2]; DateTime? get eta { @@ -74,23 +50,9 @@ class Printer { return null; } - static Color stateToColor(PrinterState state) { - switch (state) { - case PrinterState.ready: - return Colors.green; - case PrinterState.error: - return Colors.red; - case PrinterState.shutdown: - case PrinterState.startup: - case PrinterState.disconnected: - default: - return Colors.orange; - } - } - @override String toString() { - return 'Printer{state: $state, toolhead: $toolhead, extruder: $extruder, heaterBed: $heaterBed, printFan: $printFan, gCodeMove: $gCodeMove, print: $print, configFile: $configFile, fans: $fans, temperatureSensors: $temperatureSensors, outputPins: $outputPins, virtualSdCard: $virtualSdCard, queryableObjects: $queryableObjects, gcodeMacros: $gcodeMacros}'; + return 'Printer{toolhead: $toolhead, extruder: $extruder, heaterBed: $heaterBed, printFan: $printFan, gCodeMove: $gCodeMove, print: $print, configFile: $configFile, fans: $fans, temperatureSensors: $temperatureSensors, outputPins: $outputPins, virtualSdCard: $virtualSdCard, queryableObjects: $queryableObjects, gcodeMacros: $gcodeMacros}'; } @override @@ -98,7 +60,6 @@ class Printer { identical(this, other) || other is Printer && runtimeType == other.runtimeType && - state == other.state && toolhead == other.toolhead && extruder == other.extruder && heaterBed == other.heaterBed && @@ -115,7 +76,6 @@ class Printer { @override int get hashCode => - state.hashCode ^ toolhead.hashCode ^ extruder.hashCode ^ heaterBed.hashCode ^ @@ -458,6 +418,7 @@ class TemperatureSensor { class OutputPin { String name; + // This value is between 0-1 double value = 0.0; diff --git a/lib/dto/machine/printer_setting.dart b/lib/dto/machine/printer_setting.dart index 491e1843..589c33ea 100644 --- a/lib/dto/machine/printer_setting.dart +++ b/lib/dto/machine/printer_setting.dart @@ -1,5 +1,5 @@ import 'package:hive/hive.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/dto/machine/temperature_preset.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/service/file_service.dart'; diff --git a/lib/dto/server/klipper.dart b/lib/dto/server/klipper.dart index f6f5fdc5..e01b818d 100644 --- a/lib/dto/server/klipper.dart +++ b/lib/dto/server/klipper.dart @@ -1,15 +1,67 @@ -import 'package:mobileraker/dto/machine/printer.dart'; +import 'package:flutter/material.dart'; + +enum KlipperState { ready, error, shutdown, startup, disconnected } class KlipperInstance { bool klippyConnected; - PrinterState klippyState; //Matches Printer state + KlipperState klippyState; //Matches Printer state String get klippyStateName => printerStateName(klippyState); List plugins; KlipperInstance( {this.klippyConnected = false, - this.klippyState = PrinterState.error, + this.klippyState = KlipperState.error, this.plugins = const []}); + + String get stateName => printerStateName(klippyState); + + static Color stateToColor(KlipperState state) { + switch (state) { + case KlipperState.ready: + return Colors.green; + case KlipperState.error: + return Colors.red; + case KlipperState.shutdown: + case KlipperState.startup: + case KlipperState.disconnected: + default: + return Colors.orange; + } + } + + static String printerStateName(KlipperState printerState) { + switch (printerState) { + case KlipperState.ready: + return "Ready"; + case KlipperState.shutdown: + return "Shutdown"; + case KlipperState.startup: + return "Starting"; + case KlipperState.disconnected: + return "Disconnected"; + case KlipperState.error: + default: + return "Error"; + } + } + + @override + String toString() { + return 'KlipperInstance{klippyConnected: $klippyConnected, klippyState: $klippyState, plugins: $plugins}'; + } + + @override + bool operator ==(Object other) => + identical(this, other) || + other is KlipperInstance && + runtimeType == other.runtimeType && + klippyConnected == other.klippyConnected && + klippyState == other.klippyState && + plugins == other.plugins; + + @override + int get hashCode => + klippyConnected.hashCode ^ klippyState.hashCode ^ plugins.hashCode; } diff --git a/lib/service/file_service.dart b/lib/service/file_service.dart index 94fdb344..9b78aff0 100644 --- a/lib/service/file_service.dart +++ b/lib/service/file_service.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:enum_to_string/enum_to_string.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/app/app_setup.logger.dart'; import 'package:mobileraker/dto/files/folder.dart'; import 'package:mobileraker/dto/files/gcode_file.dart'; @@ -27,22 +27,20 @@ class FileService { final WebSocketWrapper _webSocket; final _logger = getLogger('FileService'); - // final BehaviorSubject> fileStream = - // BehaviorSubject>(); FileService(this._webSocket) { _webSocket.addMethodListener(_onFileListChanged, "notify_filelist_changed"); - _webSocket.stateStream.listen((value) { - switch (value) { - case WebSocketState.connected: - // _fetchAvailableFiles(FileRoot.gcodes); - - // _fetchDirectoryInfo('gcodes', true); - break; - default: - } - }); + // _webSocket.stateStream.listen((value) { + // switch (value) { + // case WebSocketState.connected: + // // _fetchAvailableFiles(FileRoot.gcodes); + // + // // _fetchDirectoryInfo('gcodes', true); + // break; + // default: + // } + // }); } _onFileListChanged(Map rawMessage) { @@ -54,7 +52,7 @@ class FileService { Future fetchDirectoryInfo(String path, [bool extended = false]) async { Completer reqCompleter = Completer(); - _logger.i('Fetching for $path [extended:$extended]'); + _logger.i('Fetching for `$path` [extended:$extended]'); _webSocket.sendObject("server.files.get_directory", @@ -69,6 +67,18 @@ class FileService { params: {'root': EnumToString.convertToString(root)}); } + + Future getGCodeMetadata(String filename) async { + Completer reqCompleter = Completer(); + _logger.i('Getting meta for file: `$filename`'); + + + _webSocket.sendObject("server.files.metadata", + (response) => _parseFileMeta(response, filename, reqCompleter), + params: {'filename': filename}); + return reqCompleter.future; + } + _parseResult(response, FileRoot root) { // List fileList = response; // Just add an type // @@ -119,6 +129,15 @@ class FileService { completer.complete(FolderReqWrapper(forPath,listOfFolder, listOfFiles)); } + + _parseFileMeta(response, String forFile, Completer completer) { + var split = forFile.split('/'); + split.removeLast(); + split.insert(0, 'gcodes');// we need to add the gcodes here since the getMetaInfo omits gcodes path. + + ; + completer.complete(GCodeFile.fromJson(response, split.join('/'))); + } } class FolderReqWrapper { diff --git a/lib/service/klippy_service.dart b/lib/service/klippy_service.dart index 5b61d408..bb94a132 100644 --- a/lib/service/klippy_service.dart +++ b/lib/service/klippy_service.dart @@ -1,9 +1,8 @@ import 'dart:convert'; import 'package:enum_to_string/enum_to_string.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/app/app_setup.logger.dart'; -import 'package:mobileraker/dto/machine/printer.dart'; import 'package:mobileraker/dto/server/klipper.dart'; import 'package:rxdart/rxdart.dart'; @@ -16,24 +15,24 @@ class KlippyService { final BehaviorSubject klipperStream = BehaviorSubject.seeded( KlipperInstance( - klippyConnected: false, klippyState: PrinterState.startup)); + klippyConnected: false, klippyState: KlipperState.startup)); KlippyService(this._webSocket) { _webSocket.addMethodListener((m) { KlipperInstance l = _getLatestKlippy(); - l.klippyState = PrinterState.ready; + l.klippyState = KlipperState.ready; klipperStream.add(l); }, "notify_klippy_ready"); _webSocket.addMethodListener((m) { KlipperInstance l = _getLatestKlippy(); - l.klippyState = PrinterState.shutdown; + l.klippyState = KlipperState.shutdown; klipperStream.add(l); }, "notify_klippy_shutdown"); _webSocket.addMethodListener((m) { KlipperInstance l = _getLatestKlippy(); - l.klippyState = PrinterState.disconnected; + l.klippyState = KlipperState.disconnected; klipperStream.add(l); }, "notify_klippy_disconnected"); @@ -45,7 +44,7 @@ class KlippyService { case WebSocketState.disconnected: case WebSocketState.error: KlipperInstance l = _getLatestKlippy(); - l.klippyState = PrinterState.error; + l.klippyState = KlipperState.error; klipperStream.add(l); break; default: @@ -61,8 +60,8 @@ class KlippyService { _parseServerInfo(response) { _logger.v('ServerInfo: ${JsonEncoder.withIndent(' ').convert(response)}'); - PrinterState state = - EnumToString.fromString(PrinterState.values, response['klippy_state'])!; + KlipperState state = + EnumToString.fromString(KlipperState.values, response['klippy_state'])!; bool con = response['klippy_connected']; List plugins = response['plugins'].cast(); KlipperInstance klipperInstance = _getLatestKlippy(); @@ -100,4 +99,6 @@ class KlippyService { emergencyStop() { _webSocket.sendObject("printer.emergency_stop", null); } + + bool get isKlippyConnected => klipperStream.valueOrNull?.klippyConnected ?? false; } diff --git a/lib/service/printer_service.dart b/lib/service/printer_service.dart index e041ac4d..6b3b1538 100644 --- a/lib/service/printer_service.dart +++ b/lib/service/printer_service.dart @@ -2,10 +2,11 @@ import 'dart:convert'; import 'dart:math'; import 'package:enum_to_string/enum_to_string.dart'; -import 'package:mobileraker/WebSocket.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/app/app_setup.logger.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/dto/config/config_file.dart'; +import 'package:mobileraker/dto/files/gcode_file.dart'; import 'package:mobileraker/dto/machine/printer.dart'; import 'package:rxdart/rxdart.dart'; import 'package:stacked_services/stacked_services.dart'; @@ -53,7 +54,6 @@ class PrinterService { _fetchPrinter() { // printerStream.value = Printer(); - // _webSocket.sendObject("printer.info", _printerInfo); // ToDo remove this, isn't needed anymore! _logger.i(">>>Querying printers object list"); _webSocket.sendObject("printer.objects.list", _printerObjectsList); } @@ -86,15 +86,6 @@ class PrinterService { printerStream.add(latestPrinter); } - _printerInfo(response) { - Printer printer = _latestPrinter; - _logger.v('PrinterInfo: ${JsonEncoder.withIndent(' ').convert(response)}'); - var fromString = - EnumToString.fromString(PrinterState.values, response['state']); - printer.state = fromString ?? PrinterState.error; - printerStream.add(printer); - } - _printerObjectsList(response) { Printer printer = _latestPrinter; _logger.i("<< _EaseInState(); +} + +class _EaseInState extends State { + late double opacity; + + @override + void initState() { + super.initState(); + opacity = 0; + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + Future.delayed(widget.duration).then((_) { + setState(() { + opacity = 1; + }); + }); + } + + @override + Widget build(BuildContext context) { + return AnimatedOpacity( + opacity: opacity, + duration: widget.duration, + child: widget.child, + ); + } +} \ No newline at end of file diff --git a/lib/ui/components/connection/connection_state_view.dart b/lib/ui/components/connection/connection_state_view.dart index 9333f75b..e0b95f8f 100644 --- a/lib/ui/components/connection/connection_state_view.dart +++ b/lib/ui/components/connection/connection_state_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:progress_indicators/progress_indicators.dart'; import 'package:stacked/stacked.dart'; diff --git a/lib/ui/components/connection/connection_state_viewmodel.dart b/lib/ui/components/connection/connection_state_viewmodel.dart index 8e327475..915a6162 100644 --- a/lib/ui/components/connection/connection_state_viewmodel.dart +++ b/lib/ui/components/connection/connection_state_viewmodel.dart @@ -1,5 +1,5 @@ import 'package:flutter_fgbg/flutter_fgbg.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/app/app_setup.logger.dart'; import 'package:mobileraker/app/app_setup.router.dart'; diff --git a/lib/ui/drawer/nav_drawer_view.dart b/lib/ui/drawer/nav_drawer_view.dart index a6485ac7..6d71aea3 100644 --- a/lib/ui/drawer/nav_drawer_view.dart +++ b/lib/ui/drawer/nav_drawer_view.dart @@ -1,75 +1,109 @@ import 'package:flutter/cupertino.dart'; +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_icons/flutter_icons.dart'; import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/ui/drawer/nav_drawer_viewmodel.dart'; import 'package:stacked/stacked.dart'; +import 'package:url_launcher/url_launcher.dart'; -class NavigationDrawerWidget extends StatelessWidget { +class NavigationDrawerWidget + extends ViewModelBuilderWidget { final String curPath; NavigationDrawerWidget({required this.curPath}); @override - Widget build(BuildContext context) { + NavDrawerViewModel viewModelBuilder(BuildContext context) => + NavDrawerViewModel(curPath); + + @override + Widget builder( + BuildContext context, NavDrawerViewModel model, Widget? child) { Color bgCol = Color.fromRGBO(50, 75, 205, 1); var themeData = Theme.of(context); if (themeData.brightness == Brightness.dark) bgCol = themeData.primaryColor; - return ViewModelBuilder.reactive( - builder: (context, model, child) => Drawer( - child: Material( - color: bgCol, - child: ListView( - children: [ - buildHeader( - name: model.printerDisplayName, - email: model.printerUrl, - onClicked: () => model.onEditTap(null), - ), - Container( - padding: const EdgeInsets.symmetric(horizontal: 00), - child: Column( - children: [ - ExpansionTile( - title: const Text( - 'Manager Printers', - style: TextStyle(color: Colors.white), - ), - children: buildPrinterSelection(context, model), - ), - buildMenuItem( - model, - text: 'Overview', - icon: Icons.home, - path: Routes.overView, + return Drawer( + child: Material( + color: bgCol, + child: Column( + children: [ + buildHeader( + name: model.printerDisplayName, + email: model.printerUrl, + onClicked: () => model.onEditTap(null), + ), + Expanded( + child: Column( + children: [ + ExpansionTile( + title: const Text( + 'Manager Printers', + style: TextStyle(color: Colors.white), ), + children: buildPrinterSelection(context, model), + ), + buildMenuItem( + model, + text: 'Overview', + icon: Icons.home, + path: Routes.overView, + ), - buildMenuItem( - model, - text: 'Files', - icon: Icons.file_present, - path: Routes.filesView, - ), - // Divider(color: Colors.white70), - // const SizedBox(height: 16), - // buildMenuItem( - // text: 'Notifications', - // icon: Icons.notifications_outlined, - // onClicked: () => selectedItem(context, 5), - // ), - ], - ), + buildMenuItem( + model, + text: 'Files', + icon: Icons.file_present, + path: Routes.filesView, + ), + // Divider(color: Colors.white70), + // const SizedBox(height: 16), + // buildMenuItem( + // text: 'Notifications', + // icon: Icons.notifications_outlined, + // onClicked: () => selectedItem(context, 5), + // ), + ], ), - ], - ), + ), + Container( + alignment: Alignment.center, + padding: EdgeInsets.only(bottom: 20, top: 10), + child: RichText( + text: TextSpan( + text: + 'Made with ❤️ by Patrick Schmidt\nCheckout the project', + children: [ + new TextSpan( + text: ' GitHub ', + style: new TextStyle(color: Colors.blue), + children: [ + WidgetSpan( + child: + Icon(FlutterIcons.github_alt_faw, size: 18), + ), + ], + recognizer: TapGestureRecognizer() + ..onTap = () async { + const String url = + 'https://github.com/Clon1998/mobileraker'; + if (await canLaunch(url)) {//TODO Fix this... neds Android Package Visibility + await launch(url); + } else { + throw 'Could not launch $url'; + } + }, + ), + ]), + textAlign: TextAlign.center, + )), + ], ), ), - viewModelBuilder: () => NavDrawerViewModel(curPath), ); - } + } // Note always the first is the currently selected! - // Note always the first is the currently selected! List buildPrinterSelection( BuildContext context, NavDrawerViewModel model) { var theme = Theme.of(context); @@ -113,7 +147,7 @@ class NavigationDrawerWidget extends StatelessWidget { required VoidCallback onClicked, }) => Container( - margin: const EdgeInsets.symmetric(vertical: 40, horizontal: 20), + margin: const EdgeInsets.fromLTRB(20, 90, 20, 20), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ diff --git a/lib/ui/views/files/details/file_details_view.dart b/lib/ui/views/files/details/file_details_view.dart new file mode 100644 index 00000000..3a95ebdd --- /dev/null +++ b/lib/ui/views/files/details/file_details_view.dart @@ -0,0 +1,131 @@ +import 'package:cached_network_image/cached_network_image.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_icons/flutter_icons.dart'; +import 'package:mobileraker/dto/files/gcode_file.dart'; +import 'package:mobileraker/ui/views/files/details/file_details_viewmodel.dart'; +import 'package:mobileraker/util/time_util.dart'; +import 'package:stacked/stacked.dart'; + +class FileDetailView extends ViewModelBuilderWidget { + const FileDetailView({Key? key, required this.file}) : super(key: key); + final GCodeFile file; + + @override + Widget builder( + BuildContext context, FileDetailsViewModel model, Widget? child) { + return Scaffold( + appBar: AppBar( + title: Text( + file.name, + overflow: TextOverflow.fade, + ), + ), + body: Column(children: [ + CachedNetworkImage( + imageUrl: + '${model.curPathToPrinterUrl}/${file.parentPath}/${file.bigImagePath}', + placeholder: (context, url) => Icon(Icons.insert_drive_file), + errorWidget: (context, url, error) => Icon(Icons.error), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4.0), + child: Table( + border: TableBorder( + horizontalInside: BorderSide( + width: 1, + color: Theme.of(context).dividerColor, + style: BorderStyle.solid)), + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: { + 0: FlexColumnWidth(2), + 1: FlexColumnWidth(5), + }, + children: [ + TableRow(children: [ + Text('File Name'), + Text(file.name), + ]), + TableRow(children: [ + Text('Path'), + Text('${file.parentPath}/${file.name}'), + ]), + TableRow(children: [ + Text('Last modified'), + Text(model.formattedLastModified), + ]), + TableRow(children: [ + Text('Last printed'), + Text((file.printStartTime != null)? model.formattedLastPrinted: 'No Data'), + ]), + TableRow(children: [ + Text('Slicer'), + Text('${file.slicer} (v${file.slicerVersion})'), + ]), + TableRow(children: [ + Text('Layer Height'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Normal"), + Text('${file.layerHeight?.toStringAsFixed(2)} mm'), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("First"), + Text('${file.firstLayerHeight?.toStringAsFixed(2)} mm'), + ], + ), + ], + ), + ]), + TableRow(children: [ + Text('First Layer Temps'), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Extruder"), + Text( + '${file.firstLayerTempExtruder?.toStringAsFixed(0)}°C'), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text("Bed"), + Text('${file.firstLayerTempBed?.toStringAsFixed(0)}°C'), + ], + ), + ], + ), + ]), + TableRow(children: [ + Text('Est. print time'), + Text('${secondsToDurationText(file.estimatedTime ?? 0)} (ETA: ${model.potentialEta})'), + ]), + ], + ), + ), + ]), + floatingActionButton: FloatingActionButton.extended( + backgroundColor: (model.canStartPrint)? null:Theme.of(context).disabledColor, + onPressed: (model.canStartPrint) ? model.onStartPrintTap : null, + icon: Icon(FlutterIcons.printer_3d_nozzle_mco), + label: Text("Print"), + ), + floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat, + ); + } + + @override + FileDetailsViewModel viewModelBuilder(BuildContext context) => + FileDetailsViewModel(file); +} diff --git a/lib/ui/views/files/details/file_details_viewmodel.dart b/lib/ui/views/files/details/file_details_viewmodel.dart new file mode 100644 index 00000000..84e00e1f --- /dev/null +++ b/lib/ui/views/files/details/file_details_viewmodel.dart @@ -0,0 +1,88 @@ +import 'package:intl/intl.dart'; +import 'package:mobileraker/app/app_setup.locator.dart'; +import 'package:mobileraker/app/app_setup.logger.dart'; +import 'package:mobileraker/app/app_setup.router.dart'; +import 'package:mobileraker/dto/files/gcode_file.dart'; +import 'package:mobileraker/dto/machine/printer.dart'; +import 'package:mobileraker/dto/machine/printer_setting.dart'; +import 'package:mobileraker/dto/server/klipper.dart'; +import 'package:mobileraker/service/klippy_service.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'; + +const String _ServerStreamKey = 'server'; +const String _PrinterStreamKey = 'printer'; + +class FileDetailsViewModel extends MultipleStreamViewModel { + final _logger = getLogger('FileDetailsViewModel'); + + final _navigationService = locator(); + final _machineService = locator(); + + PrinterSetting? get _printerSetting => + _machineService.selectedPrinter.valueOrNull; + + PrinterService? get _printerService => _printerSetting?.printerService; + + KlippyService? get _klippyService => _printerSetting?.klippyService; + + final GCodeFile _file; + + FileDetailsViewModel(this._file); + + @override + Map get streamsMap => { + if (_printerService != null) ...{ + _PrinterStreamKey: StreamData(_printerService!.printerStream) + }, + if (_klippyService != null) ...{ + _ServerStreamKey: + StreamData(_klippyService!.klipperStream) + } + }; + + bool get hasServer => dataReady(_ServerStreamKey); + + KlipperInstance get server => dataMap![_ServerStreamKey]; + + bool get hasPrinter => dataReady(_PrinterStreamKey); + + Printer get printer => dataMap![_PrinterStreamKey]; + + onStartPrintTap() { + _printerService?.startPrintFile(_file); + _navigationService.clearStackAndShow(Routes.overView); + } + + bool get canStartPrint { + if (!hasServer || !hasPrinter || server.klippyState != KlipperState.ready) + return false; + else { + return (printer.print.state == PrintState.complete || + printer.print.state == PrintState.standby); + } + } + + String? get curPathToPrinterUrl { + if (_printerSetting != null) { + return '${_printerSetting!.httpUrl}/server/files'; + } + } + + String get formattedLastPrinted { + return DateFormat.yMMMd().add_Hm().format(_file.lastPrintDate!); + } + + String get formattedLastModified { + return DateFormat.yMMMd().add_Hm().format(_file.modifiedDate!); + } + + String get potentialEta { + var eta = DateTime.now() + .add(Duration(seconds: _file.estimatedTime!.toInt())) + .toLocal(); + return DateFormat.MMMEd().add_Hm().format(eta); + } +} diff --git a/lib/ui/views/files/files_view.dart b/lib/ui/views/files/files_view.dart index da4464c3..123b0095 100644 --- a/lib/ui/views/files/files_view.dart +++ b/lib/ui/views/files/files_view.dart @@ -7,6 +7,7 @@ import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/files/folder.dart'; import 'package:mobileraker/dto/files/gcode_file.dart'; import 'package:mobileraker/service/file_service.dart'; +import 'package:mobileraker/ui/components/EaseIn.dart'; import 'package:mobileraker/ui/components/connection/connection_state_view.dart'; import 'package:mobileraker/ui/drawer/nav_drawer_view.dart'; import 'package:mobileraker/ui/views/files/files_viewmodel.dart'; @@ -21,26 +22,9 @@ class FilesView extends ViewModelBuilderWidget { @override Widget builder(BuildContext context, FilesViewModel model, Widget? child) { return WillPopScope( - onWillPop: model.onPopFolder, + onWillPop: model.onWillPop, child: Scaffold( - appBar: AppBar( - title: Text( - 'File Browser', - overflow: TextOverflow.fade, - ), - actions: [ - IconButton( - icon: Icon(Icons.search), - onPressed: () => null, - ), - IconButton( - icon: Icon( - Icons.sort, - ), - onPressed: () => null, - ), - ], - ), + appBar: buildAppBar(context, model), drawer: NavigationDrawerWidget(curPath: Routes.filesView), body: ConnectionStateView( pChild: buildBody(context, model), @@ -49,6 +33,54 @@ class FilesView extends ViewModelBuilderWidget { ); } + AppBar buildAppBar(BuildContext context, FilesViewModel model) { + if (model.isSearching) { + return AppBar( + leading: IconButton( + icon: Icon(Icons.arrow_back), + onPressed: model.stopSearching, + ), + title: EaseIn( + child: TextField( + controller: model.searchEditingController, + autofocus: true, + style: Theme.of(context).textTheme.headline6, + decoration: InputDecoration( + hintText: 'Search files...', + border: InputBorder.none, + suffixIcon: model.searchEditingController.text.isEmpty + ? null + : IconButton( + tooltip: 'Clear search', + icon: Icon(Icons.close), + color: Theme.of(context).colorScheme.onPrimary, + onPressed: model.resetSearchQuery, + ), + ), + ), + ), + ); + } else + return AppBar( + title: Text( + 'File Browser', + overflow: TextOverflow.fade, + ), + actions: [ + IconButton( + icon: Icon( + Icons.sort, + ), + onPressed: () => null, + ), + IconButton( + icon: Icon(Icons.search), + onPressed: model.startSearching, + ), + ], + ); + } + Widget buildBody(BuildContext context, FilesViewModel model) { if (model.isBusy) return buildBusyListView(context, model); @@ -214,6 +246,7 @@ class FolderItem extends ViewModelWidget { @override Widget build(BuildContext context, FilesViewModel model) { return ListTile( + key: ValueKey(folder), leading: SizedBox(width: 64, height: 64, child: Icon(Icons.folder)), title: Text(folder.name), onTap: () => model.onFolderPressed(folder), @@ -229,11 +262,13 @@ class FileItem extends ViewModelWidget { @override Widget build(BuildContext context, FilesViewModel model) { return ListTile( + key: ValueKey(gCode), leading: SizedBox( width: 64, height: 64, child: buildLeading(gCode, model.curPathToPrinterUrl)), title: Text(gCode.name), + onTap: () => model.onFileTapped(gCode), ); // return ListTile( diff --git a/lib/ui/views/files/files_viewmodel.dart b/lib/ui/views/files/files_viewmodel.dart index 79ec5484..75e2c342 100644 --- a/lib/ui/views/files/files_viewmodel.dart +++ b/lib/ui/views/files/files_viewmodel.dart @@ -3,7 +3,9 @@ import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/app/app_setup.logger.dart'; +import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/files/folder.dart'; +import 'package:mobileraker/dto/files/gcode_file.dart'; import 'package:mobileraker/dto/machine/printer.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/server/klipper.dart'; @@ -27,6 +29,8 @@ class FilesViewModel extends MultipleStreamViewModel { final _bottomSheetService = locator(); final _machineService = locator(); + bool isSearching = false; + PrinterSetting? _printerSetting; FileService? get _fileService => _printerSetting?.fileService; @@ -38,6 +42,8 @@ class FilesViewModel extends MultipleStreamViewModel { RefreshController refreshController = RefreshController(initialRefresh: false); + TextEditingController searchEditingController = TextEditingController(); + StreamController _foldersStream = StreamController.broadcast(); @@ -78,11 +84,14 @@ class FilesViewModel extends MultipleStreamViewModel { } onRefresh() { - runBusyFuture(_fetchDirectoryData(newPath: folderContent.reqPath.split('/'))).then((value) => refreshController.refreshCompleted()); + runBusyFuture( + _fetchDirectoryData(newPath: folderContent.reqPath.split('/'))) + .then((value) => refreshController.refreshCompleted()); } - onEmergencyPressed() { - _klippyService?.emergencyStop(); + onFileTapped(GCodeFile file) { + _navigationService.navigateTo(Routes.fileDetailView, + arguments: FileDetailViewArguments(file: file)); } onFolderPressed(Folder folder) { @@ -91,7 +100,21 @@ class FilesViewModel extends MultipleStreamViewModel { runBusyFuture(_fetchDirectoryData(newPath: newPath)); } - Future onPopFolder() async { + Future onWillPop() async { + List newPath = folderContent.reqPath.split('/'); + + if (isSearching) { + stopSearching(); + return false; + } else if (newPath.length > 1 && !isBusy) { + newPath.removeLast(); + runBusyFuture(_fetchDirectoryData(newPath: newPath)); + return false; + } + return true; + } + + onPopFolder() async { List newPath = folderContent.reqPath.split('/'); if (newPath.length > 1 && !isBusy) { newPath.removeLast(); @@ -101,15 +124,49 @@ class FilesViewModel extends MultipleStreamViewModel { return true; } + startSearching() { + isSearching = true; + } + + stopSearching() { + isSearching = false; + } + + resetSearchQuery() { + searchEditingController.text = ''; + } + Future _fetchDirectoryData({List newPath = const ['gcodes']}) { requestedPath = newPath; return _foldersStream.addStream( _fileService!.fetchDirectoryInfo(newPath.join('/'), true).asStream()); } + FolderReqWrapper get folderContent { + FolderReqWrapper fullContent = _folderContent; + List folders = _folderContent.folders.toList(growable: false); + List files = _folderContent.gCodes.toList(growable: false); + + String queryTerm = searchEditingController.text.toLowerCase(); + if (queryTerm.isNotEmpty && isSearching) { + folders = folders + .where((element) => element.name.toLowerCase().contains(queryTerm)) + .toList(growable: false); + + files = files + .where((element) => element.name.toLowerCase().contains(queryTerm)) + .toList(growable: false); + } + + folders.sort((fileA, fileB) => fileB.modified.compareTo(fileA.modified)); + files.sort((fileA, fileB) => fileB.modified.compareTo(fileA.modified)); + + return FolderReqWrapper(fullContent.reqPath, folders, files); + } + bool get hasFolderContent => dataReady(_FolderContentStreamKey); - FolderReqWrapper get folderContent => dataMap![_FolderContentStreamKey]; + FolderReqWrapper get _folderContent => dataMap![_FolderContentStreamKey]; bool get hasServer => dataReady(_ServerStreamKey); @@ -131,10 +188,10 @@ class FilesViewModel extends MultipleStreamViewModel { } } - @override void dispose() { - super.dispose(); - refreshController.dispose(); + super.dispose(); + refreshController.dispose(); + searchEditingController.dispose(); } } diff --git a/lib/ui/views/fullcam/full_cam_view.dart b/lib/ui/views/fullcam/full_cam_view.dart index 49f2ca45..47ec30e1 100644 --- a/lib/ui/views/fullcam/full_cam_view.dart +++ b/lib/ui/views/fullcam/full_cam_view.dart @@ -1,13 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_mjpeg/flutter_mjpeg.dart'; +import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/ui/views/fullcam/full_cam_viewmodel.dart'; import 'package:stacked/stacked.dart'; class FullCamView extends ViewModelBuilderWidget { + final WebcamSetting webcamSetting; + + FullCamView(this.webcamSetting); + @override FullCamViewModel viewModelBuilder(BuildContext context) => - FullCamViewModel(); + FullCamViewModel(this.webcamSetting); @override Widget builder(BuildContext context, FullCamViewModel model, Widget? child) { @@ -24,10 +29,23 @@ class FullCamView extends ViewModelBuilderWidget { transform: model.transformMatrix, child: Mjpeg( isLive: true, - stream: model.selectedCam.url, + stream: model.selectedCam!.url, )), ), ), + if (model.webcams.length > 1) + Align( + alignment: Alignment.bottomCenter, + child: DropdownButton( + value: model.selectedCam, + onChanged: model.onWebcamSettingSelected, + items: model.webcams.map((e) { + return DropdownMenuItem( + child: Text(e.name), + value: e, + ); + }).toList()), + ), Align( alignment: Alignment.bottomRight, child: IconButton( diff --git a/lib/ui/views/fullcam/full_cam_viewmodel.dart b/lib/ui/views/fullcam/full_cam_viewmodel.dart index 44c8f7e4..6bcbd874 100644 --- a/lib/ui/views/fullcam/full_cam_viewmodel.dart +++ b/lib/ui/views/fullcam/full_cam_viewmodel.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:flutter/widgets.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; +import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/service/machine_service.dart'; import 'package:stacked/stacked.dart'; @@ -10,28 +11,22 @@ import 'package:stacked_services/stacked_services.dart'; class FullCamViewModel extends BaseViewModel { final _navigationService = locator(); final _machineService = locator(); + WebcamSetting? selectedCam; - WebcamSetting? _camHack() { - var printSetting = _machineService.selectedPrinter.valueOrNull; - if (printSetting != null && printSetting.cams.isNotEmpty) { - return printSetting.cams.first; - } - return null; - } + FullCamViewModel(this.selectedCam); - bool get hasCam => _camHack() != null; - - WebcamSetting get selectedCam => _camHack()!; + PrinterSetting? get _printerSetting => + _machineService.selectedPrinter.valueOrNull; double get yTransformation { - if (selectedCam.flipHorizontal) + if (selectedCam?.flipHorizontal ?? false) return pi; else return 0; } double get xTransformation { - if (selectedCam.flipVertical) + if (selectedCam?.flipVertical ?? false) return pi; else return 0; @@ -41,6 +36,18 @@ class FullCamViewModel extends BaseViewModel { ..rotateX(xTransformation) ..rotateY(yTransformation); + onWebcamSettingSelected(WebcamSetting? webcamSetting) { + selectedCam = webcamSetting; + notifyListeners(); + } + + List get webcams { + if (_printerSetting != null && _printerSetting!.cams.isNotEmpty) { + return _printerSetting!.cams; + } + return List.empty(); + } + onCloseTapped() { _navigationService.back(); } diff --git a/lib/ui/views/overview/overview_view.dart b/lib/ui/views/overview/overview_view.dart index 49f174f2..de3ec04c 100644 --- a/lib/ui/views/overview/overview_view.dart +++ b/lib/ui/views/overview/overview_view.dart @@ -3,15 +3,16 @@ import 'package:animations/animations.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_icons/flutter_icons.dart'; +import 'package:flutter_speed_dial/flutter_speed_dial.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/machine/printer.dart'; +import 'package:mobileraker/dto/server/klipper.dart'; import 'package:mobileraker/ui/components/connection/connection_state_view.dart'; import 'package:mobileraker/ui/drawer/nav_drawer_view.dart'; import 'package:mobileraker/ui/views/overview/tabs/control_tab.dart'; import 'package:mobileraker/ui/views/overview/tabs/general_tab.dart'; import 'package:progress_indicators/progress_indicators.dart'; -import 'package:simple_speed_dial/simple_speed_dial.dart'; import 'package:stacked/stacked.dart'; import 'overview_viewmodel.dart'; @@ -32,9 +33,9 @@ class OverView extends StatelessWidget { IconButton( icon: Icon(Icons.radio_button_on, size: 10, - color: Printer.stateToColor(model.hasServer + color: KlipperInstance.stateToColor(model.hasServer ? model.server.klippyState - : PrinterState.error)), + : KlipperState.error)), tooltip: model.hasServer ? 'Server State is ${model.server.klippyStateName} and Moonraker is ${model.server.klippyConnected ? 'connected' : 'disconnected'} to Klipper' : 'Server is not connected', @@ -52,61 +53,61 @@ class OverView extends StatelessWidget { ], ), body: ConnectionStateView( - pChild: - (model.hasPrinter) - ? PageTransitionSwitcher( - duration: const Duration(milliseconds: 300), - reverse: model.reverse, - transitionBuilder: ( - Widget child, - Animation animation, - Animation secondaryAnimation, - ) { - return SharedAxisTransition( - child: child, - animation: animation, - secondaryAnimation: secondaryAnimation, - transitionType: SharedAxisTransitionType.horizontal, - ); - }, - child: getViewForIndex(model.currentIndex), - ) - : Center( - child: Column( - key: UniqueKey(), - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SpinKitFadingCube( - color: Colors.orange, - ), - SizedBox( - height: 30, - ), - FadingText("Fetching printer..."), - // Text("Fetching printer ...") - ], + pChild: (model.hasPrinter) + ? PageTransitionSwitcher( + duration: const Duration(milliseconds: 300), + reverse: model.reverse, + transitionBuilder: ( + Widget child, + Animation animation, + Animation secondaryAnimation, + ) { + return SharedAxisTransition( + child: child, + animation: animation, + secondaryAnimation: secondaryAnimation, + transitionType: SharedAxisTransitionType.horizontal, + ); + }, + child: getViewForIndex(model.currentIndex), + ) + : Center( + child: Column( + key: UniqueKey(), + mainAxisAlignment: MainAxisAlignment.center, + children: [ + SpinKitFadingCube( + color: Colors.orange, ), - ), + SizedBox( + height: 30, + ), + FadingText("Fetching printer..."), + // Text("Fetching printer ...") + ], + ), + ), ), floatingActionButton: printingStateToFab(model), floatingActionButtonLocation: FloatingActionButtonLocation.endDocked, - bottomNavigationBar: (model.isPrinterSelected) - ? AnimatedBottomNavigationBar( - // ToDo swap with Text - icons: [ - FlutterIcons.tachometer_faw, - // FlutterIcons.camera_control_mco, - FlutterIcons.settings_oct, - ], + bottomNavigationBar: + (model.isPrinterSelected && model.isKlippyConnected) + ? AnimatedBottomNavigationBar( + // ToDo swap with Text + icons: [ + FlutterIcons.tachometer_faw, + // FlutterIcons.camera_control_mco, + FlutterIcons.settings_oct, + ], - activeColor: getActiveTextColor(context), - gapLocation: GapLocation.end, - backgroundColor: Theme.of(context).primaryColor, - notchSmoothness: NotchSmoothness.softEdge, - activeIndex: model.currentIndex, - onTap: model.setIndex, - ) - : null, + activeColor: getActiveTextColor(context), + gapLocation: GapLocation.end, + backgroundColor: Theme.of(context).primaryColor, + notchSmoothness: NotchSmoothness.softEdge, + activeIndex: model.currentIndex, + onTap: model.setIndex, + ) + : null, // ConvexAppBar( // // style: TabStyle.textIn, @@ -126,7 +127,8 @@ class OverView extends StatelessWidget { Color? getActiveTextColor(context) { var themeData = Theme.of(context); - if (themeData.brightness == Brightness.dark) return themeData.colorScheme.secondary; + if (themeData.brightness == Brightness.dark) + return themeData.colorScheme.secondary; return Colors.white; } @@ -144,7 +146,7 @@ class OverView extends StatelessWidget { Widget? printingStateToFab(OverViewModel model) { if (!model.hasPrinter || !model.hasServer) return null; - if (model.server.klippyState == PrinterState.error) return null; + if (model.server.klippyState == KlipperState.error) return null; switch (model.printer.print.state) { case PrintState.printing: @@ -153,15 +155,15 @@ class OverView extends StatelessWidget { child: Icon(Icons.pause), ); case PrintState.paused: - return SpeedDialPaused(); + return PausedFAB(); default: - return MenuNonPrinting(); + return IdleFAB(); } } } -class MenuNonPrinting extends ViewModelWidget { - const MenuNonPrinting({ +class IdleFAB extends ViewModelWidget { + const IdleFAB({ Key? key, }) : super(key: key, reactive: false); @@ -172,69 +174,32 @@ class MenuNonPrinting extends ViewModelWidget { } } -class SpeedDialPaused extends ViewModelWidget { - const SpeedDialPaused({ +class PausedFAB extends ViewModelWidget { + const PausedFAB({ Key? key, }) : super(key: key, reactive: false); @override Widget build(BuildContext context, OverViewModel model) { return SpeedDial( - child: Icon(FlutterIcons.options_vertical_sli), - speedDialChildren: [ + icon: FlutterIcons.options_vertical_sli, + activeIcon: Icons.close, + children: [ SpeedDialChild( child: Icon(Icons.cleaning_services), backgroundColor: Colors.red, label: 'Cancel', - onPressed: model.onCancelPrintPressed, + onTap: model.onCancelPrintPressed, ), SpeedDialChild( child: Icon(Icons.play_arrow), backgroundColor: Colors.blue, label: 'Resume', - onPressed: model.onResumePrintPressed, + onTap: model.onResumePrintPressed, ), ], + spacing: 5, + overlayOpacity: 0, ); - - // SpeedDial( - // - // child: FlutterIcons.options_vertical_sli, - // activeIcon: Icons.close, - // visible: true, - // - // /// If true user is forced to close dial manually - // /// by tapping main button and overlay is not rendered. - // closeManually: false, - // - // /// If true overlay will render no matter what. - // renderOverlay: false, - // // curve: Curves.bounceIn, - // overlayColor: Colors.black, - // overlayOpacity: 0.3, - // backgroundColor: Colors.white, - // foregroundColor: Colors.black, - // elevation: 8.0, - // shape: CircleBorder(), - // // orientation: SpeedDialOrientation.Up, - // // childMarginBottom: 2, - // // childMarginTop: 2, - // children: [ - // SpeedDialChild( - // child: Icon(Icons.cleaning_services), - // backgroundColor: Colors.red, - // label: 'Cancel', - // labelStyle: TextStyle(fontSize: 18.0), - // onTap: model.onCancelPrintPressed, - // ), - // SpeedDialChild( - // child: Icon(Icons.play_arrow), - // backgroundColor: Colors.blue, - // label: 'Resume', - // labelStyle: TextStyle(fontSize: 18.0), - // onTap: model.onResumePrintPressed, - // ), - // ], - // ); } } diff --git a/lib/ui/views/overview/overview_viewmodel.dart b/lib/ui/views/overview/overview_viewmodel.dart index 8ab290cd..721aa537 100644 --- a/lib/ui/views/overview/overview_viewmodel.dart +++ b/lib/ui/views/overview/overview_viewmodel.dart @@ -5,8 +5,8 @@ import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/server/klipper.dart'; import 'package:mobileraker/enums/bottom_sheet_type.dart'; import 'package:mobileraker/service/klippy_service.dart'; -import 'package:mobileraker/service/printer_service.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'; @@ -20,7 +20,9 @@ class OverViewModel extends MultipleStreamViewModel { final _machineService = locator(); PrinterSetting? _printerSetting; + PrinterService? get _printerService => _printerSetting?.printerService; + KlippyService? get _klippyService => _printerSetting?.klippyService; @override @@ -75,6 +77,9 @@ class OverViewModel extends MultipleStreamViewModel { bool get hasPrinter => dataReady(_PrinterStreamKey); + bool get isKlippyConnected => + _klippyService?.isKlippyConnected ?? false; + @override onData(String key, data) { super.onData(key, data); @@ -85,6 +90,7 @@ class OverViewModel extends MultipleStreamViewModel { _printerSetting = nPrinterSetting; notifySourceChanged(clearOldData: true); break; + default: // Do nothing break; diff --git a/lib/ui/views/overview/tabs/general_tab.dart b/lib/ui/views/overview/tabs/general_tab.dart index 7c5dad21..df9ab665 100644 --- a/lib/ui/views/overview/tabs/general_tab.dart +++ b/lib/ui/views/overview/tabs/general_tab.dart @@ -39,7 +39,7 @@ class GeneralTab extends ViewModelBuilderWidget { model.isPrinterSelected) ...[ PrintCard(), TemperatureCard(), - if (model.webCamUrl != null) CamCard(), + if (model.webcams.isNotEmpty) CamCard(), if (model.printer.print.state != PrintState.printing) ControlXYZCard(), if (model.printer.print.state == PrintState.printing) @@ -62,65 +62,54 @@ class PrintCard extends ViewModelWidget { @override Widget build(BuildContext context, GeneralTabViewModel model) { + return Card( + child: Column( + children: [ + ListTile( + contentPadding: const EdgeInsets.only(top: 3, left: 16, right: 16), + leading: Icon(FlutterIcons.monitor_dashboard_mco), + title: Text('${model.printer.print.stateName}'), + subtitle: _subTitle(model), + trailing: _trailing(model), + ), + _buildTableView(context, model), + ], + ), + ); + } + + Widget? _trailing(GeneralTabViewModel model) { switch (model.printer.print.state) { case PrintState.printing: - return Card( - child: Column( - children: [ - ListTile( - contentPadding: - const EdgeInsets.only(top: 3, left: 16, right: 16), - leading: Icon(FlutterIcons.monitor_dashboard_mco), - title: Text('${model.printer.print.stateName}'), - subtitle: Text( - "Printing: ${model.printer.print.filename}\nFor: ${secondsToDurationText(model.printer.print.totalDuration)}"), - trailing: CircularPercentIndicator( - radius: 50, - lineWidth: 4, - percent: model.printer.virtualSdCard.progress, - center: Text( - "${(model.printer.virtualSdCard.progress * 100).round()}%"), - progressColor: - (model.printer.print.state == PrintState.complete) - ? Colors.green - : Colors.deepOrange, - ), - ), - _buildTableView(context, model), - ], - ), + return CircularPercentIndicator( + radius: 50, + lineWidth: 4, + percent: model.printer.virtualSdCard.progress, + center: + Text("${(model.printer.virtualSdCard.progress * 100).round()}%"), + progressColor: (model.printer.print.state == PrintState.complete) + ? Colors.green + : Colors.deepOrange, ); + case PrintState.complete: + return TextButton.icon( + onPressed: model.onResetPrintTap, + icon: Icon(Icons.restart_alt_outlined), + label: Text('Reset')); + default: + return null; + } + } + Widget? _subTitle(GeneralTabViewModel model) { + switch (model.printer.print.state) { + case PrintState.printing: + return Text( + "Printing: ${model.printer.print.filename}\nFor: ${secondsToDurationText(model.printer.print.totalDuration)}"); case PrintState.error: - return Card( - child: Column( - children: [ - ListTile( - contentPadding: - const EdgeInsets.only(top: 3, left: 16, right: 16), - leading: Icon(FlutterIcons.monitor_dashboard_mco), - title: Text('${model.printer.print.stateName}'), - subtitle: Text('${model.printer.print.message}'), - ), - _buildTableView(context, model), - ], - ), - ); - + return Text('${model.printer.print.message}'); default: - return Card( - child: Column( - children: [ - ListTile( - contentPadding: - const EdgeInsets.only(top: 3, left: 16, right: 16), - leading: Icon(FlutterIcons.monitor_dashboard_mco), - title: Text('${model.printer.print.stateName}'), - ), - _buildTableView(context, model), - ], - ), - ); + return null; } } @@ -198,7 +187,7 @@ class PrintCard extends ViewModelWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ Text("Layer"), - Text('Todo'), + Text('${model.layer}/${model.maxLayers}'), ], ), ), @@ -228,11 +217,7 @@ class CamCard extends ViewModelWidget { @override Widget build(BuildContext context, GeneralTabViewModel model) { - Matrix4 matrix4 = Matrix4.identity() - ..rotateX(model.webCamXSwap) - ..rotateY(model.webCamYSwap); - - const double webCamHeight = 280; + const double minWebCamHeight = 280; return Card( child: Column( children: [ @@ -241,27 +226,40 @@ class CamCard extends ViewModelWidget { FlutterIcons.webcam_mco, ), title: Text('Webcam'), + trailing: (model.webcams.length > 1) + ? DropdownButton( + value: model.selectedCam, + onChanged: model.onWebcamSettingSelected, + items: model.webcams.map((e) { + return DropdownMenuItem( + child: Text(e.name), + value: e, + ); + }).toList()) + : null, ), Padding( padding: const EdgeInsets.fromLTRB(8, 8, 8, 10), child: Container( - width: double.infinity, - height: webCamHeight, + constraints: BoxConstraints(minHeight: minWebCamHeight), child: Stack(children: [ - Transform( - alignment: Alignment.center, - transform: matrix4, - child: Mjpeg( - height: webCamHeight, - isLive: true, - stream: model.webCamUrl!, - )), - Align( - alignment: Alignment.bottomRight, - child: IconButton( - icon: Icon(Icons.aspect_ratio_outlined), - tooltip: 'Fullscreen', - onPressed: model.onFullScreenTap, + Center( + child: Transform( + alignment: Alignment.center, + transform: model.transformMatrix, + child: Mjpeg( + isLive: true, + stream: model.webCamUrl, + )), + ), + Positioned.fill( + child: Align( + alignment: Alignment.bottomRight, + child: IconButton( + icon: Icon(Icons.aspect_ratio_outlined), + tooltip: 'Fullscreen', + onPressed: model.onFullScreenTap, + ), ), ), ]), diff --git a/lib/ui/views/overview/tabs/general_tab_viewmodel.dart b/lib/ui/views/overview/tabs/general_tab_viewmodel.dart index 9a52fce5..03efd1b9 100644 --- a/lib/ui/views/overview/tabs/general_tab_viewmodel.dart +++ b/lib/ui/views/overview/tabs/general_tab_viewmodel.dart @@ -4,13 +4,14 @@ import 'package:flip_card/flip_card.dart'; import 'package:flutter/widgets.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/app/app_setup.router.dart'; +import 'package:mobileraker/dto/files/gcode_file.dart'; import 'package:mobileraker/dto/machine/printer.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/machine/temperature_preset.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/dto/server/klipper.dart'; -import 'package:mobileraker/enums/bottom_sheet_type.dart'; import 'package:mobileraker/enums/dialog_type.dart'; +import 'package:mobileraker/service/file_service.dart'; import 'package:mobileraker/service/klippy_service.dart'; import 'package:mobileraker/service/machine_service.dart'; import 'package:mobileraker/service/printer_service.dart'; @@ -28,8 +29,12 @@ class GeneralTabViewModel extends MultipleStreamViewModel { final _navigationService = locator(); PrinterSetting? _printerSetting; - PrinterService? _printerService; - KlippyService? _klippyService; + + PrinterService? get _printerService => _printerSetting?.printerService; + + KlippyService? get _klippyService => _printerSetting?.klippyService; + + FileService? get _fileService => _printerSetting?.fileService; GlobalKey tmpCardKey = GlobalKey(); @@ -39,6 +44,10 @@ class GeneralTabViewModel extends MultipleStreamViewModel { List babySteppingSizes = [0.005, 0.01, 0.05, 0.1]; int selectedIndexBabySteppingSize = 0; + GCodeFile? currentFile; + + WebcamSetting? selectedCam; + @override Map get streamsMap => { _SelectedPrinterStreamKey: @@ -60,15 +69,19 @@ class GeneralTabViewModel extends MultipleStreamViewModel { PrinterSetting? nPrinterSetting = data; if (nPrinterSetting == _printerSetting) break; _printerSetting = nPrinterSetting; + selectedCam = _printerSetting?.cams.first; + notifySourceChanged(clearOldData: true); + break; - if (nPrinterSetting?.printerService != null) { - _printerService = nPrinterSetting?.printerService; - } + case _PrinterStreamKey: + Printer nPrinter = data; + + String filename = nPrinter.print.filename; + if (filename.isNotEmpty && currentFile?.pathForPrint != filename) + _fileService! + .getGCodeMetadata(filename) + .then((value) => currentFile = value); - if (nPrinterSetting?.klippyService != null) { - _klippyService = nPrinterSetting?.klippyService; - } - notifySourceChanged(clearOldData: true); break; default: // Do nothing @@ -90,19 +103,19 @@ class GeneralTabViewModel extends MultipleStreamViewModel { return _printerSetting?.temperaturePresets.toList() ?? List.empty(); } - WebcamSetting? _camHack() { + List get webcams { if (_printerSetting != null && _printerSetting!.cams.isNotEmpty) { - return _printerSetting?.cams.first; + return _printerSetting!.cams; } - return null; + return List.empty(); } - String? get webCamUrl { - return _camHack()?.url; + String get webCamUrl { + return selectedCam!.url; } - double get webCamYSwap { - var vertical = _camHack()?.flipVertical ?? false; + double get yTransformation { + var vertical = selectedCam?.flipVertical ?? false; if (vertical) return pi; @@ -110,8 +123,8 @@ class GeneralTabViewModel extends MultipleStreamViewModel { return 0; } - double get webCamXSwap { - var horizontal = _camHack()?.flipVertical ?? false; + double get xTransformation { + var horizontal = selectedCam?.flipVertical ?? false; if (horizontal) return pi; @@ -119,6 +132,10 @@ class GeneralTabViewModel extends MultipleStreamViewModel { return 0; } + Matrix4 get transformMatrix => Matrix4.identity() + ..rotateX(xTransformation) + ..rotateY(yTransformation); + setTemperaturePreset(int extruderTemp, int bedTemp) { _printerService?.setTemperature('extruder', extruderTemp); _printerService?.setTemperature('heater_bed', bedTemp); @@ -216,8 +233,50 @@ class GeneralTabViewModel extends MultipleStreamViewModel { } onFullScreenTap() { - _navigationService.navigateTo(Routes.fullCamView); + _navigationService.navigateTo(Routes.fullCamView, + arguments: FullCamViewArguments(webcamSetting: selectedCam!)); } + onResetPrintTap() { + _printerService?.resetPrintStat(); + } + + onWebcamSettingSelected(WebcamSetting? webcamSetting) { + selectedCam = webcamSetting; + } + + int get maxLayers { + if (!_canCalcMaxLayer) return 0; + GCodeFile crntFile = currentFile!; + int max = ((crntFile.objectHeight! - crntFile.firstLayerHeight!) / + crntFile.layerHeight! + + 1) + .ceil(); + return max > 0 ? max : 0; + } + + bool get _canCalcMaxLayer => + hasPrinter && + currentFile != null && + currentFile!.firstLayerHeight != null && + currentFile!.layerHeight != null && + currentFile!.objectHeight != null; + + int get layer { + if (!_canCalcLayer) return 0; + GCodeFile crntFile = currentFile!; + int currentLayer = + ((printer.toolhead.position[3] - crntFile.firstLayerHeight!) / + crntFile.layerHeight! + + 1) + .ceil(); + currentLayer = (currentLayer <= maxLayers) ? currentLayer : maxLayers; + return currentLayer > 0 ? currentLayer : 0; + } + bool get _canCalcLayer => + hasPrinter && + currentFile != null && + currentFile!.firstLayerHeight != null && + currentFile!.layerHeight != null; } diff --git a/lib/ui/views/printers/add/printers_add_view.dart b/lib/ui/views/printers/add/printers_add_view.dart index 2cbb8618..3386e79c 100644 --- a/lib/ui/views/printers/add/printers_add_view.dart +++ b/lib/ui/views/printers/add/printers_add_view.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:stacked/stacked.dart'; import 'printers_add_viewmodel.dart'; diff --git a/lib/ui/views/printers/add/printers_add_viewmodel.dart b/lib/ui/views/printers/add/printers_add_viewmodel.dart index 5c78f763..6c3f3c79 100644 --- a/lib/ui/views/printers/add/printers_add_viewmodel.dart +++ b/lib/ui/views/printers/add/printers_add_viewmodel.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:hive_flutter/hive_flutter.dart'; -import 'package:mobileraker/WebSocket.dart'; +import 'package:mobileraker/datasource/websocket_wrapper.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; diff --git a/lib/ui/views/printers/components/printers_slidable_viewmodel.dart b/lib/ui/views/printers/components/printers_slidable_viewmodel.dart index 57725eae..fba75d72 100644 --- a/lib/ui/views/printers/components/printers_slidable_viewmodel.dart +++ b/lib/ui/views/printers/components/printers_slidable_viewmodel.dart @@ -49,7 +49,7 @@ class PrintersSlidableViewModel extends MultipleStreamViewModel { Color get stateColor { if (hasServer) { - return Printer.stateToColor(server.klippyState); + return KlipperInstance.stateToColor(server.klippyState); } return Colors.red; } diff --git a/lib/ui/views/printers/edit/printers_edit_view.dart b/lib/ui/views/printers/edit/printers_edit_view.dart index c2217d85..ae80b7cf 100644 --- a/lib/ui/views/printers/edit/printers_edit_view.dart +++ b/lib/ui/views/printers/edit/printers_edit_view.dart @@ -5,6 +5,7 @@ import 'package:flutter_icons/flutter_icons.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/machine/temperature_preset.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; +import 'package:reorderables/reorderables.dart'; import 'package:stacked/stacked.dart'; import 'printers_edit_viewmodel.dart'; @@ -27,7 +28,7 @@ class PrintersEdit extends ViewModelBuilderWidget { actions: [ IconButton( onPressed: model.onFormConfirm, - tooltip: 'Add printer', + tooltip: 'Save', icon: Icon(Icons.save_outlined)) ], ), @@ -91,7 +92,7 @@ class PrintersEdit extends ViewModelBuilderWidget { label: Text('Add'), icon: Icon(FlutterIcons.webcam_mco), )), - ..._buildWebCams(model), + _buildWebCams(model), _SectionHeaderWithAction( title: 'TEMPERATURE PRESETS', action: TextButton.icon( @@ -99,7 +100,7 @@ class PrintersEdit extends ViewModelBuilderWidget { label: Text('Add'), icon: Icon(FlutterIcons.thermometer_lines_mco), )), - ..._buildTempPresets(model), + _buildTempPresets(model), Divider(), Align( alignment: Alignment.bottomCenter, @@ -120,46 +121,47 @@ class PrintersEdit extends ViewModelBuilderWidget { PrintersEditViewModel viewModelBuilder(BuildContext context) => PrintersEditViewModel(printerSetting); - List _buildWebCams(PrintersEditViewModel model) { + Widget _buildWebCams(PrintersEditViewModel model) { if (model.webcams.isEmpty) { - return [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text('No webcams added'), - ) - ]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text('No webcams added'), + ); } - return List.generate(model.webcams.length, (index) { - WebcamSetting cam = model.webcams[index]; - return _WebCamItem( - key: ValueKey(cam.uuid), - model: model, - cam: cam, - idx: index, - ); - }); + return ReorderableColumn( + children: List.generate(model.webcams.length, (index) { + WebcamSetting cam = model.webcams[index]; + return _WebCamItem( + key: ValueKey(cam.uuid), + model: model, + cam: cam, + idx: index, + ); + }), + onReorder: model.onWebCamReorder); } - List _buildTempPresets(PrintersEditViewModel model) { + Widget _buildTempPresets(PrintersEditViewModel model) { if (model.tempPresets.isEmpty) { - return [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text('No presets added'), - ) - ]; + return Padding( + padding: const EdgeInsets.all(8.0), + child: Text('No presets added'), + ); } + return ReorderableColumn( + children: List.generate(model.tempPresets.length, (index) { + TemperaturePreset preset = model.tempPresets[index]; + return _TempPresetItem( + key: ValueKey(preset.uuid), + model: model, + temperaturePreset: preset, + idx: index, + ); + }), + onReorder: model.onPresetReorder, - return List.generate(model.tempPresets.length, (index) { - TemperaturePreset preset = model.tempPresets[index]; - return _TempPresetItem( - key: ValueKey(preset.uuid), - model: model, - temperaturePreset: preset, - idx: index, - ); - }); + ); } } diff --git a/lib/ui/views/printers/edit/printers_edit_viewmodel.dart b/lib/ui/views/printers/edit/printers_edit_viewmodel.dart index c0ccfdef..30389d3d 100644 --- a/lib/ui/views/printers/edit/printers_edit_viewmodel.dart +++ b/lib/ui/views/printers/edit/printers_edit_viewmodel.dart @@ -2,16 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_form_builder/flutter_form_builder.dart'; import 'package:mobileraker/app/app_setup.locator.dart'; +import 'package:mobileraker/app/app_setup.logger.dart'; import 'package:mobileraker/app/app_setup.router.dart'; import 'package:mobileraker/dto/machine/printer_setting.dart'; import 'package:mobileraker/dto/machine/temperature_preset.dart'; import 'package:mobileraker/dto/machine/webcam_setting.dart'; import 'package:mobileraker/service/machine_service.dart'; -import 'package:mobileraker/util/misc.dart'; import 'package:stacked/stacked.dart'; import 'package:stacked_services/stacked_services.dart'; class PrintersEditViewModel extends BaseViewModel { + final _logger = getLogger('PrintersEditViewModel'); + final _navigationService = locator(); final _dialogService = locator(); final _machineService = locator(); @@ -152,4 +154,16 @@ class PrintersEditViewModel extends BaseViewModel { (value) => _navigationService.clearStackAndShow(Routes.overView)); }); } + + onPresetReorder(int oldIndex, int newIndex) { + TemperaturePreset _row = tempPresets.removeAt(oldIndex); + tempPresets.insert(newIndex, _row); + notifyListeners(); + } + + onWebCamReorder(int oldIndex, int newIndex) { + WebcamSetting _row = webcams.removeAt(oldIndex); + webcams.insert(newIndex, _row); + notifyListeners(); + } } diff --git a/lib/util/time_util.dart b/lib/util/time_util.dart index 6d4165a6..a796f2d9 100644 --- a/lib/util/time_util.dart +++ b/lib/util/time_util.dart @@ -1,3 +1,4 @@ + String secondsToDurationText(double sec) { var d = Duration(seconds: sec.round()); var seconds = d.inSeconds; diff --git a/pubspec.yaml b/pubspec.yaml index bf66bf06..fb9f5794 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -52,7 +52,6 @@ dependencies: progress_indicators: ^1.0.0 flutter_form_builder: ^6.0.0 badges: ^2.0.1 - simple_speed_dial: ^0.1.3 awesome_notifications: any flutter_fgbg: ^0.1.0 pull_to_refresh: ^2.0.0 @@ -71,6 +70,9 @@ dependencies: flutter_breadcrumb: ^1.0.1 cached_network_image: ^3.1.0 shimmer: ^2.0.0 + url_launcher: ^6.0.11 + reorderables: ^0.4.1 + flutter_speed_dial: ^4.4.0+1 dev_dependencies: flutter_test: