From cca291e9a78991c4bfe18544fa788fe95c3bbb91 Mon Sep 17 00:00:00 2001 From: ssendev Date: Sat, 24 Apr 2021 00:42:15 +0200 Subject: [PATCH 1/6] recognize thumbnail begin from cura --- octoprint_prusaslicerthumbnails/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint_prusaslicerthumbnails/__init__.py b/octoprint_prusaslicerthumbnails/__init__.py index 3794df3..d9cc0cf 100644 --- a/octoprint_prusaslicerthumbnails/__init__.py +++ b/octoprint_prusaslicerthumbnails/__init__.py @@ -53,7 +53,7 @@ def get_template_configs(self): def _extract_thumbnail(self, gcode_filename, thumbnail_filename): import re import base64 - regex = r"(?:^; thumbnail begin \d+x\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" + regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" lineNum = 0 collectedString = "" with open(gcode_filename,"rb") as gcode_file: From b076b1fcf06e27edb51179c0e5c6cff9fb3abb4e Mon Sep 17 00:00:00 2001 From: jneilliii Date: Fri, 23 Apr 2021 19:14:25 -0400 Subject: [PATCH 2/6] Revert "recognize thumbnail begin from cura" --- octoprint_prusaslicerthumbnails/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/octoprint_prusaslicerthumbnails/__init__.py b/octoprint_prusaslicerthumbnails/__init__.py index d9cc0cf..3794df3 100644 --- a/octoprint_prusaslicerthumbnails/__init__.py +++ b/octoprint_prusaslicerthumbnails/__init__.py @@ -53,7 +53,7 @@ def get_template_configs(self): def _extract_thumbnail(self, gcode_filename, thumbnail_filename): import re import base64 - regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" + regex = r"(?:^; thumbnail begin \d+x\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" lineNum = 0 collectedString = "" with open(gcode_filename,"rb") as gcode_file: From 652277b08ecf1af3ce5aad738d573ab25555ea95 Mon Sep 17 00:00:00 2001 From: jneilliii Date: Tue, 18 May 2021 21:49:26 -0400 Subject: [PATCH 3/6] 1.0.0 (#76) * Fix Typo, `prusalicer` => `prusaslicer` * bump version * add file list height options and print button to popup, #49 * subscribe to state panel changes to automatically apply change * add position inline thumbnail on left of text option, #68 * tweaks to scaling and positioning of inline thumbnail * update state panel thumbnail for better text alignment when scaled * disable modal popup in TouchUI * update sponsors * subscribe to upload knockout instead of filepath to resolve issues with overwriting selected file with new upload, #43 * update regular expression to recognize Cura 4.9 post processing script format. * add MKS TFT/Lotmaxx thumbnail support * bump major version fix bug related to moving whole folders of files * update readme * update name in settings * prepare for release Co-authored-by: lightmaster --- README.md | 38 +++-- octoprint_prusaslicerthumbnails/__init__.py | 138 ++++++++++++++---- .../static/css/prusaslicerthumbnails.css | 16 ++ .../static/js/prusaslicerthumbnails.js | 94 ++++++++---- .../templates/prusaslicerthumbnails.jinja2 | 9 +- .../prusaslicerthumbnails_settings.jinja2 | 26 +++- requirements.txt | 4 + screenshot_cura.png | Bin 0 -> 24343 bytes screenshot_prusaslicer.png | Bin 0 -> 7756 bytes setup.py | 6 +- 10 files changed, 248 insertions(+), 83 deletions(-) create mode 100644 screenshot_cura.png create mode 100644 screenshot_prusaslicer.png diff --git a/README.md b/README.md index 3aac83f..5f87045 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ -# PrusaSlicer Thumbnails +# Slicer Thumbnails ![GitHub Downloads](https://badgen.net/github/assets-dl/jneilliii/OctoPrint-PrusaSlicerThumbnails/) -This plugin will extract the embedded thumbnails from PrusaSlicer gcode files where the printer's profile ini file has the thumbnail option configured. This is default behavior for the Prusa Mini printer profile. - -The thumbnail image extracted will always be the last resolution provided in the thumbnail setting. So for example the Prusa Mini setting is `thumbnails = 16x16,220x124` so the thumbnail that will be extracted will be 220x124 pixels as seen in the screenshots below. Check the Configuration section below for additional details. +This plugin will extract embedded thumbnails from gcode files created from [PrusaSlicer](#PrusaSlicer), [SuperSlicer](#SuperSlicer), [Cura](#Cura), or [Simplify3D](#Simplify3D). The preview thumbnail can be shown in OctoPrint from the files list by clicking the newly added image button. @@ -14,29 +12,37 @@ The thumbnail will open in a modal window. ![thumbnail](screenshot_thumbnail.png) -If enabled in settings the thumbnail can also be embedded as an inline thumbnail within the file list itself. If you use this option it's highly recommended to use Themeify to make the file list taller by adding the below custom style. - -| Selector | CSS_Rule | Value | -|-----------------------------------------------------|------------|------------------| -| #files > div > div.gcode_files > div.scroll-wrapper | min-height | 800px !important | +If enabled in settings the thumbnail can also be embedded as an inline thumbnail within the file list itself. If you use this option it's highly recommended to also set the option to set file list height or position inline image to the left. ![thumbnail](screenshot_inline_thumbnail.png) ## Configuration -Since PrusaSlicer only enables thumbnails by default for the Prusa Mini you may need to manually update your configuration files. Those can be found by selecting `Show Configuration Folder` from the Help menu of the application and then inside the printers sub-folder you'll find your printer profiles. +### PrusaSlicer + +Available via the UI since version 2.3, requires expert mode to be enabled in the upper right corner of the program to see the setting. + +![PrusaSlicer](screenshot_prusaslicer.png) + +**Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced. -**Update:** [SuperSlicer](https://github.com/supermerill/SuperSlicer), an advanced fork of PrusaSlicer, now has image options in the Printer Settings as of version 2.2.53, big shout out to the team there. You must enable expert mode in order to see it. This plugin will use the `big` reoslution configured. +### SuperSlicer + +Available via the UI since version 2.2.53, requires expert mode to be enabled in the upper right corner of the program to see the setting. ![SuperSlicer](screenshot_superslicer.png) -**Note:** If you don't see your printer's ini file in the printers sub-folder; you are probably using one of the bundled Prusa Printer profiles (ie MK3S). If so you may need to create a copy of this printer profile to be able to have an ini file to edit. To do this in PrusaSlicer go to the Printer Settings tab and Click the save button next to the printer list and give it a new name. Alternatively, push Prusa Research to update their bundled profiles to match the Mini by commenting in the issue posted on their repository [here](https://github.com/prusa3d/PrusaSlicer/issues/3488). +**Warning**: the higher the resolution of the thumbnail you use in this setting the larger your gcode file will be when sliced. + +### Cura -Open your desired printer profile in your favorite text editor and find the `thumbnails =` section and add the resolution that you would like to include in your sliced files, and therefore visible by this plugin. For example `thumbnails = 16x16,220x124` will be the equivalent of the Prusa Mini as described above. +A post-processing script has been bundled since version 4.9. For older versions you can manually add the post-processing script as described [here](https://gist.github.com/jneilliii/4034c84d1ec219c68c8877d0e794ec4e). -**Note:** Once you've made your changes you will need to restart PrusaSlicer in order for the changes to be used and embed the thumbnails in the exported gcode files. +![Cura](screenshot_cura.png) -**Warning**: the higher the resolution of the thumbnail you enter in this setting the larger your gcode file will be when sliced. +### Simplify3D + +Available as a post-processing script for [windows](https://github.com/boweeble/s3d-thumbnail-generator) or [linux](https://github.com/NotExpectedYet/s3d-thumbnail-generator) thanks to [@boweeble](https://github.com/boweeble/) and [@NotExpectedYet](https://github.com/NotExpectedYet/). ## Get Help @@ -57,6 +63,8 @@ Check out my other plugins [here](https://plugins.octoprint.org/by_author/#jneil - [Andrew Beeman](https://github.com/Kiendeleo) - [Calanish](https://github.com/calanish) - [Will O](https://github.com/4wrxb) +- [Stephen Berry](https://github.com/berrystephenw) + ### Support My Efforts I, jneilliii, programmed this plugin for fun and do my best effort to support those that have issues with it, please return the favor and leave me a tip or become a Patron if you find this plugin helpful and want me to continue future development. diff --git a/octoprint_prusaslicerthumbnails/__init__.py b/octoprint_prusaslicerthumbnails/__init__.py index 3794df3..33821c7 100644 --- a/octoprint_prusaslicerthumbnails/__init__.py +++ b/octoprint_prusaslicerthumbnails/__init__.py @@ -8,17 +8,23 @@ import octoprint.util import os import datetime +import io +from PIL import Image + class PrusaslicerthumbnailsPlugin(octoprint.plugin.SettingsPlugin, - octoprint.plugin.AssetPlugin, - octoprint.plugin.TemplatePlugin, - octoprint.plugin.EventHandlerPlugin, - octoprint.plugin.SimpleApiPlugin): + octoprint.plugin.AssetPlugin, + octoprint.plugin.TemplatePlugin, + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.SimpleApiPlugin): def __init__(self): self._fileRemovalTimer = None self._fileRemovalLastDeleted = None self._fileRemovalLastAdded = None + self._folderRemovalTimer = None + self._folderRemovalLastDeleted = {} + self._folderRemovalLastAdded = {} self._waitForAnalysis = False self._analysis_active = False @@ -30,9 +36,14 @@ def get_settings_defaults(self): inline_thumbnail=False, scale_inline_thumbnail=False, inline_thumbnail_scale_value="50", + inline_thumbnail_position_left=False, align_inline_thumbnail=False, inline_thumbnail_align_value="left", - state_panel_thumbnail=True + state_panel_thumbnail=True, + state_panel_thumbnail_scale_value="100", + resize_filelist=False, + filelist_height="306", + scale_inline_thumbnail_position=False ) ##~~ AssetPlugin mixin @@ -53,10 +64,12 @@ def get_template_configs(self): def _extract_thumbnail(self, gcode_filename, thumbnail_filename): import re import base64 - regex = r"(?:^; thumbnail begin \d+x\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" + regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" + regex_mks = re.compile('(?:;(?:simage|;gimage):).*?M10086 ;[\r\n]', re.DOTALL) lineNum = 0 collectedString = "" - with open(gcode_filename,"rb") as gcode_file: + use_mks = False + with open(gcode_filename, "rb") as gcode_file: for line in gcode_file: lineNum += 1 line = line.decode("utf-8", "ignore") @@ -65,36 +78,100 @@ def _extract_thumbnail(self, gcode_filename, thumbnail_filename): if gcode == "G1" and extrusionMatch: self._logger.debug("Line %d: Detected first extrusion. Read complete.", lineNum) break - if line.startswith(";") or line.startswith("\n"): + if line.startswith(";") or line.startswith("\n") or line.startswith("M10086 ;"): collectedString += line self._logger.debug(collectedString) - test_str = collectedString.replace(octoprint.util.to_native_str('\r\n'),octoprint.util.to_native_str('\n')) - test_str = test_str.replace(octoprint.util.to_native_str(';\n;\n'),octoprint.util.to_native_str(';\n\n;\n')) + test_str = collectedString.replace(octoprint.util.to_native_str('\r\n'), octoprint.util.to_native_str('\n')) + test_str = test_str.replace(octoprint.util.to_native_str(';\n;\n'), octoprint.util.to_native_str(';\n\n;\n')) matches = re.findall(regex, test_str, re.MULTILINE) + if len(matches) == 0: # MKS lottmaxx fallback + matches = regex_mks.findall(test_str) + if len(matches) > 0: + use_mks = True if len(matches) > 0: path = os.path.dirname(thumbnail_filename) if not os.path.exists(path): os.makedirs(path) - with open(thumbnail_filename,"wb") as png_file: - png_file.write(base64.b64decode(matches[-1:][0].replace("; ", "").encode())) + with open(thumbnail_filename, "wb") as png_file: + if use_mks: + png_file.write(self._extract_mks_thumbnail(matches)) + else: + png_file.write(base64.b64decode(matches[-1:][0].replace("; ", "").encode())) + + # Extracts a thumbnail from a gcode and returns png binary string + def _extract_mks_thumbnail(self, gcode_encoded_images): + + # Find the biggest thumbnail + encoded_image_dimensions, encoded_image = self.find_best_thumbnail(gcode_encoded_images) + + # Not found? + if encoded_image is None: + return None # What to return? Is None ok? + + # Remove M10086 ; and whitespaces + encoded_image = encoded_image.replace('M10086 ;', '').replace('\n', '').replace('\r', '').replace(' ', '') + + # Get bytes from hex + encoded_image = bytes(bytearray.fromhex(encoded_image)) + + # Load pixel data + image = Image.frombytes('RGB', encoded_image_dimensions, encoded_image, 'raw', 'BGR;16', 0, 1) + + # Save image as png + with io.BytesIO() as png_bytes: + image.save(png_bytes, "PNG") + png_bytes_string = png_bytes.getvalue() + + return png_bytes_string + + # Finds the biggest thumbnail + def find_best_thumbnail(self, gcode_encoded_images): + + # Check for gimage + for image in gcode_encoded_images: + if image.startswith(';;gimage:'): + # Return size and trimmed string + return (200, 200), image[9:] + + # Check for simage + for image in gcode_encoded_images: + if image.startswith(';simage:'): + # Return size and trimmed string + return (100, 100), image[8:] + + # Image not found + return None ##~~ EventHandlerPlugin mixin def on_event(self, event, payload): + if event not in ["FileAdded", "FileRemoved", "FolderRemoved", "FolderAdded"]: + return if event == "FolderRemoved" and payload["storage"] == "local": import shutil shutil.rmtree(self.get_plugin_data_folder() + "/" + payload["path"], ignore_errors=True) - if event in ["FileAdded","FileRemoved"] and payload["storage"] == "local" and "gcode" in payload["type"]: - thumbnail_filename = self.get_plugin_data_folder() + "/" + payload["path"].replace(".gcode",".png") + if event == "FolderAdded" and payload["storage"] == "local": + file_list = self._file_manager.list_files(path=payload["path"], recursive=True) + local_files = file_list["local"] + results = dict(no_thumbnail=[], no_thumbnail_src=[]) + for file_key, file in local_files.items(): + results = self._process_gcode(local_files[file_key], results) + self._logger.debug("Scan results: {}".format(results)) + if event in ["FileAdded", "FileRemoved"] and payload["storage"] == "local" and "gcode" in payload["type"]: + thumbnail_filename = self.get_plugin_data_folder() + "/" + payload["path"].replace(".gcode", ".png") if os.path.exists(thumbnail_filename): os.remove(thumbnail_filename) if event == "FileAdded": gcode_filename = self._file_manager.path_on_disk("local", payload["path"]) self._extract_thumbnail(gcode_filename, thumbnail_filename) if os.path.exists(thumbnail_filename): - thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/" + payload["path"].replace(".gcode", ".png") + "?" + "{:%Y%m%d%H%M%S}".format(datetime.datetime.now()) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", thumbnail_url.replace("//", "/"), overwrite=True) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", self._identifier, overwrite=True) + thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/" + payload["path"].replace(".gcode", + ".png") + "?" + "{:%Y%m%d%H%M%S}".format( + datetime.datetime.now()) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", + thumbnail_url.replace("//", "/"), overwrite=True) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", + self._identifier, overwrite=True) ##~~ SimpleApiPlugin mixin @@ -102,14 +179,15 @@ def _process_gcode(self, gcode_file, results=[]): self._logger.debug(gcode_file["path"]) if gcode_file.get("type") == "machinecode": self._logger.debug(gcode_file.get("thumbnail")) - if gcode_file.get("thumbnail") == None: + if gcode_file.get("thumbnail") is None or not os.path.exists(gcode_file.get("thumbnail").split("?")[0]): self._logger.debug("No Thumbnail for %s, attempting extraction" % gcode_file["path"]) results["no_thumbnail"].append(gcode_file["path"]) - self.on_event("FileAdded", dict(path=gcode_file["path"],storage="local",type=["gcode"])) + self.on_event("FileAdded", dict(path=gcode_file["path"], storage="local", type=["gcode"])) elif "prusaslicerthumbnails" in gcode_file.get("thumbnail") and not gcode_file.get("thumbnail_src"): self._logger.debug("No Thumbnail source for %s, adding" % gcode_file["path"]) results["no_thumbnail_src"].append(gcode_file["path"]) - self._file_manager.set_additional_metadata("local", gcode_file["path"], "thumbnail_src", self._identifier, overwrite=True) + self._file_manager.set_additional_metadata("local", gcode_file["path"], "thumbnail_src", + self._identifier, overwrite=True) elif gcode_file.get("type") == "folder" and not gcode_file.get("children") == None: children = gcode_file["children"] for key, file in children.items(): @@ -131,7 +209,7 @@ def on_api_command(self, command, data): FileList = self._file_manager.list_files(recursive=True) self._logger.info(FileList) LocalFiles = FileList["local"] - results = dict(no_thumbnail=[],no_thumbnail_src=[]) + results = dict(no_thumbnail=[], no_thumbnail_src=[]) for key, file in LocalFiles.items(): results = self._process_gcode(LocalFiles[key], results) return flask.jsonify(results) @@ -141,17 +219,18 @@ def route_hook(self, server_routes, *args, **kwargs): from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory from octoprint.util import is_hidden_path return [ - (r"thumbnail/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder(), - as_attachment=False, - path_validation=path_validation_factory(lambda path: not is_hidden_path(path),status_code=404))) - ] + (r"thumbnail/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder(), + as_attachment=False, + path_validation=path_validation_factory( + lambda path: not is_hidden_path(path), status_code=404))) + ] ##~~ Softwareupdate hook def get_update_information(self): return dict( prusaslicerthumbnails=dict( - displayName="PrusaSlicer Thumbnails", + displayName="Slicer Thumbnails", displayVersion=self._plugin_version, # version check: github repository @@ -175,8 +254,10 @@ def get_update_information(self): ) ) -__plugin_name__ = "PrusaSlicer Thumbnails" -__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 + +__plugin_name__ = "Slicer Thumbnails" +__plugin_pythoncompat__ = ">=2.7,<4" # python 2 and 3 + def __plugin_load__(): global __plugin_implementation__ @@ -187,4 +268,3 @@ def __plugin_load__(): "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, "octoprint.server.http.routes": __plugin_implementation__.route_hook } - diff --git a/octoprint_prusaslicerthumbnails/static/css/prusaslicerthumbnails.css b/octoprint_prusaslicerthumbnails/static/css/prusaslicerthumbnails.css index e345739..e435c64 100644 --- a/octoprint_prusaslicerthumbnails/static/css/prusaslicerthumbnails.css +++ b/octoprint_prusaslicerthumbnails/static/css/prusaslicerthumbnails.css @@ -6,3 +6,19 @@ #prusa_thumbnail_viewer h3 { overflow: hidden; } + +#files .gcode_files .entry { + overflow: hidden; +} + +#files .gcode_files .entry img, #prusaslicer_state_thumbnail img { + width: auto; +} + +.inline_prusa_thumbnail.pull-left, #prusaslicer_state_thumbnail.pull-left { + padding-right: 5px; +} + +#prusaslicer_state_thumbnail.pull-left img { + width: 100%; +} diff --git a/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js b/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js index cc26185..66152e1 100644 --- a/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js +++ b/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js @@ -15,6 +15,7 @@ $(function() { self.thumbnail_url = ko.observable('/static/img/tentacle-20x20.png'); self.thumbnail_title = ko.observable(''); self.inline_thumbnail = ko.observable(); + self.file_details = ko.observable(); self.crawling_files = ko.observable(false); self.crawl_results = ko.observableArray([]); @@ -23,15 +24,19 @@ $(function() { var thumbnail_title = data.path.replace('.gcode',''); self.thumbnail_url(data.thumbnail); self.thumbnail_title(thumbnail_title); + self.file_details(data); $('div#prusa_thumbnail_viewer').modal("show"); } } - self.DEFAULT_THUMBNAIL_SCALE = "100%" - self.filesViewModel.thumbnailScaleValue = ko.observable(self.DEFAULT_THUMBNAIL_SCALE) + self.DEFAULT_THUMBNAIL_SCALE = "100%"; + self.filesViewModel.thumbnailScaleValue = ko.observable(self.DEFAULT_THUMBNAIL_SCALE); - self.DEFAULT_THUMBNAIL_ALIGN = "left" - self.filesViewModel.thumbnailAlignValue = ko.observable(self.DEFAULT_THUMBNAIL_ALIGN) + self.DEFAULT_THUMBNAIL_ALIGN = "left"; + self.filesViewModel.thumbnailAlignValue = ko.observable(self.DEFAULT_THUMBNAIL_ALIGN); + + self.DEFAULT_THUMBNAIL_POSITION = false; + self.filesViewModel.thumbnailPositionLeft = ko.observable(self.DEFAULT_THUMBNAIL_POSITION); self.crawl_files = function(){ self.crawling_files(true); @@ -61,6 +66,24 @@ $(function() { } self.onBeforeBinding = function() { + // inject filelist thumpnail into template + let regex = /
([\s\S]*)<.div>/mi; + let template = ''; + + let inline_thumbnail_template = '
' + + '
' + + $("#files_template_machinecode").text(function () { + var return_value = inline_thumbnail_template + $(this).text(); + return_value = return_value.replace(regex, '
$1 ' + template + '>
'); + return return_value + }); + // assign initial scaling if (self.settingsViewModel.settings.plugins.prusaslicerthumbnails.scale_inline_thumbnail()==true){ self.filesViewModel.thumbnailScaleValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_scale_value() + "%"); @@ -71,6 +94,16 @@ $(function() { self.filesViewModel.thumbnailAlignValue(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_align_value()); } + // assign initial filelist height + if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.resize_filelist()) { + $('#files > div > div.gcode_files > div.scroll-wrapper').css({'height': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height() + 'px'}); + } + + // assign initial position + if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_position_left()==true) { + self.filesViewModel.thumbnailPositionLeft(true); + } + // observe scaling changes self.settingsViewModel.settings.plugins.prusaslicerthumbnails.scale_inline_thumbnail.subscribe(function(newValue){ if (newValue == false){ @@ -82,6 +115,9 @@ $(function() { self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_scale_value.subscribe(function(newValue){ self.filesViewModel.thumbnailScaleValue(newValue + "%"); }); + self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value.subscribe(function(newValue){ + $('#prusaslicer_state_thumbnail').attr({'width': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() + '%'}) + }); // observe alignment changes self.settingsViewModel.settings.plugins.prusaslicerthumbnails.align_inline_thumbnail.subscribe(function(newValue){ @@ -95,48 +131,44 @@ $(function() { self.filesViewModel.thumbnailAlignValue(newValue); }); - self.printerStateViewModel.filepath.subscribe(function(data){ + // observe position changes + self.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail_position_left.subscribe(function(newValue){ + self.filesViewModel.thumbnailPositionLeft(newValue); + }); + + // observe file list height changes + self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height.subscribe(function(newValue){ + if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.resize_filelist()) { + $('#files > div > div.gcode_files > div.scroll-wrapper').css({'height': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.filelist_height() + 'px'}); + } + }); + + self.printerStateViewModel.dateString.subscribe(function(data){ if(data){ - OctoPrint.files.get('local',data) + OctoPrint.files.get('local',self.printerStateViewModel.filepath()) .done(function(file_data){ if(file_data){ if(self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail() && file_data.thumbnail && file_data.thumbnail_src == 'prusaslicerthumbnails'){ - if($('#prusalicer_state_thumbnail').length) { - $('#prusalicer_state_thumbnail > img').attr('src', file_data.thumbnail); + if($('#prusaslicer_state_thumbnail').length) { + $('#prusaslicer_state_thumbnail').attr('src', file_data.thumbnail); } else { - $('#state > div > hr:first').after('
\n
'); + $('#state > div > hr:first').after(''); } } else { - $('#prusalicer_state_thumbnail').remove(); + $('#prusaslicer_state_thumbnail').remove(); } } }) .fail(function(file_data){ - if($('#prusalicer_state_thumbnail').length) { - $('#prusalicer_state_thumbnail').remove(); + if($('#prusaslicer_state_thumbnail').length) { + $('#prusaslicer_state_thumbnail').remove(); } }); - } + } else { + $('#prusaslicer_state_thumbnail').remove(); + } }); } - - - $(document).ready(function(){ - let regex = /
([\s\S]*)<.div>/mi; - let template = ''; - let inline_thumbnail_template = '
' + - '
' - - $("#files_template_machinecode").text(function () { - var return_value = inline_thumbnail_template + $(this).text(); - return_value = return_value.replace(regex, '
$1 ' + template + '>
'); - return return_value - }); - }); } OCTOPRINT_VIEWMODELS.push({ diff --git a/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails.jinja2 b/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails.jinja2 index cb7a9ec..87c9463 100644 --- a/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails.jinja2 +++ b/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails.jinja2 @@ -4,9 +4,12 @@

-
\ No newline at end of file + diff --git a/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2 b/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2 index 497e499..66149d3 100644 --- a/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2 +++ b/octoprint_prusaslicerthumbnails/templates/prusaslicerthumbnails_settings.jinja2 @@ -1,10 +1,16 @@ -

{{ _('PrusaSlicer Thumbnails') }}

+

{{ _('Slicer Thumbnails') }}

+
+ + + % + +
+
+ +
+
+ +
+
+ + + px + +
@@ -61,4 +83,4 @@
- \ No newline at end of file + diff --git a/requirements.txt b/requirements.txt index a1dc463..0179e5d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,7 @@ ### . + +setuptools +OctoPrint +Pillow diff --git a/screenshot_cura.png b/screenshot_cura.png new file mode 100644 index 0000000000000000000000000000000000000000..069d835a5ef022313ddcaa7071694318044e3429 GIT binary patch literal 24343 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 Fe*tcA58MC% literal 0 HcmV?d00001 diff --git a/screenshot_prusaslicer.png b/screenshot_prusaslicer.png new file mode 100644 index 0000000000000000000000000000000000000000..dce7b4d4de7f762d8c5e6524bd1636af1643b573 GIT binary patch literal 7756 zcmbW6cT|&Yx8_mW3knDbNLP>=K#KG(L8M6wy;r463B3piQUsLVJA~eA=pZ6pN+_X8 zhlJ3Zf#Ae>-?QfX&a9bpX8y>N-0NP?v!3j=@BQ1?wWBm&E0GY>5#!+CkSM{(9R6ak@P6SK?mKUpShF3_~sqb~7l88-0 z@P@0aQ;~kYdpWD=r=wCr*>fjqlFr?^n;%ZIPNjBywdbqZAdsDv#_uzsqz1& zASeHV`^X?FGLqqIj|vdD`ItZtyG1Bai`@~A3`vfR{4RgL5(os6T3N8Mu(&_K1z>Lp zV<4_H1KHU=88PGTEN|1ETLyR9$+EB%)Qv5Cf;CazhTQoMc}hCtuf|GFK6&+JrOZO3 zleE4IVKOZT1U_#r7OjOtLj93Hn>DE3==M1_wJkTxh;jp#^D#CmhGbN`_eQxd3&t6D z$viX5-g-0ou}-i$qd#-*#Lv2*G_X&=BkEq>9!(c25t-KrM(gM#_B1~ru~P*x(8@`J zEKKKd=3dJN#K?q8))1iHSwdC`v%jinCW@rLxX@KlRNaI%j`Th%ozThjEwv}x@7|r3 zGruwryk>ywNk+UGq?q5xUWK{@%4qERRXVe+AURlA=zw<}r8+x1OQ;o}#2lmz{bkZSIZ3acSd-4pRhB#FqFbS&l&_MU`Oh_Xm3Pq*qMs<|bKdk&_ zYox%QuUzz%s-(k7eJvvIAey&@PUiP(DiiwJAAUaemIYyvk h_&J$3fOD`KAb2W z{*yd{jDM%a|3@ZWeTe`*w<2YhadH6myACC=m2C51bC6De!eD1K-MTq4Vj_x?V~W~9 zoJ#F~<)<5W+S@Fs?%o2#nE*gq+-Ddw#WYImf$nT8Z9QI%0@+3oG`&-|`7 zX`{##aV7>#v3H>n0L(LTwu$2&zrt?jH@N{6t04W2?Q>*nar8`g(0bI>0un~Hwt(pK z)G6t_e1cZf_haaXS5?&QlrN~g^sYj|Bi>1A5!4C7va%R3HQ6v~-c0oxy$%^`0=teC zhvxolj0KV1s6i;%ct%9g_q6S_24)N8;X%scZ<9TKn6E@8JluU`=0WyGFRHxb^x8h#TM5& zMZ{DJ6mkT*T+|9R2Ova)EfET5yB=Zz2mCqyGftGnA%!95N;NdzrqB;Vj=8%DF1HTjw_{7xFrW7ab3Rj2|2_F}) z+DmgFGBKb3)$XXEvm{U>#6Qr{Y!Ch^u|N30wl*$~ROepQY|5vOVPVdBlNxu>h+)8y zm#B#DZJ!fiW0wrBkPSl22`$EdevL*7I^RH@?^|Y4HzSxkm>8^yb|RIEWq<#gm~U}q zmbu;+nx9$j8LnwMO)_)^9^rzt!dpwqL7G6^Dj1S-I1O9~XeBCXtRH5Lk=!}fnC2NVa zjeMIp1n&B)>=Gu)tBu0#dFH!=yUXrm zin_)b@mvIM3=KzT6+#({uLhzqSEJF66f--v+52+U)}TE zL2078wO9vE@kOqK$sbuC5(kh|kJ>l3{=U6FSMWY5YL{+RF@+atT+s5_lT^NV{rPCi zgtQ8>-Fit7nz#^`p`5|v?QHYD-H+6?*0y_L!V0w^y}PFm-pfR@n27|6AM7~qeBQXb zP&)pYOt^4E1^6;E(zn(gI_wm7r#%mZzo3_hVG6s977y+~&YaX;37o=-Mcma>8FntO z)p_e|E_u1R1)DxEG`z_)LZ|>l^RH24pz3n>5%d_R!6^d)Z)~&C!nVVQkG~;D72i1- zU^?9QMDknCXbeJJ0>+BKpy>P6tW=C~nAl8G>=>AsJrDdtD||kcL&6)$831 z!%-Y%TKj{gHiH%y#^{(Bbyb9lipo!~_VbUbuf9MVNfrua^~Djc$JeuDM_!lg2FK4{ zLxJ!wZaFJidJ&uls5?j5;=%y(IZ3l=q&0F6Sj$UuP zczT7?>D1T}eAd`IOE5kE%ycEzTP+t4YGG#|NB`Wc9)118 zQvD6m$*9FaJbsMu_2Gad#N!36R|oRbS7ALm0=`sL!v3i#ZOq79&P~dU?TOqP+{TAx zt1A!@>FAx=hZd)ZG0nZ5pHAnFvRyvhWWfM%I(OF?VT8R?Z7A{;zV6x;=m_t;U)+9< zPtC;{wovh~>`m|)`9YT8z&_RMl3-g?kEZmKciq(~!2l<;S>MeMP^~y=^YLA>$82R5!Xj3p+e2<2uQeZy8c_XW zmBXLJ)lqR?!2XA)bU}mh%ySwi&E>he*Jwl}pbC_}OI#UsarL7+UUk0y(8Y!ZFiz(V3N2tNshvp73JC+{OMP;O}SJ&car6kIZrpYw$hEP%Jx-( zH(vBCbn>GH_C-fA<5-lvB>J}w$gvVjC!5&hw?##cvB`h;Ls`V^e>x$dKa@hw)rblg z)P-SpUVg%xtuk)O;4P>N(dqq{mI)FX^{othQVZJJXiZH`(V5Qsn}AK?d-t^T^t>|7 zG1SbEPNPUmg>ljV)+BR8qqmEFU^1&-XWqA*yiFGj+pjODy0*M}wO!tY$cXEgN_^4ag8 zLxr>S>NBy3CS!f;3EhVD`bB9OTGCvFRcptNwbr0v8@-Z@#KgkiO%3n@+lKx^8n6r1 z^Njbz(^g+_8f{CM>!XGFe=2~h_acY$-TaV0boj27#X}8+E7Tre6u3Wm_jK>#(Ui0d z2>^Q@&M7(G)s_Aw8}O^xo~UhhGGA!^@De4&D{^u#SmPeGe$z%DTFyRe>Z4GJq|m1j zxg-ov2lBFjd(nwf{MD?JEyCye0tJ0#3Xfo?fyq!kM~sX$55$y=XTSZ#tnSP}$I(Iu zW71J#vV*F@7C%HpyW>Hne83#UNG?9`cd=4)4wjV5LrzAh+#mZWg%;kjdUs!5)5bmAk{0)AM#Eat%vh zZVnQg;@&;YbQT!O4amr07?kCb8y-NF=bK*0{7`vF4 zM(|mKL+!06x(74HZYCIpTob-9NRJbTI{xP63!Op7c6E=hN$$0#^Ju9qkCMsSbM-}| zn8Ffqxq8^#k$~UPHWO^sbF>kFxe%Dh{Pul+w#yl@oWHS2cO6gxmJVQGT@epx(u$Xa zKYW`=0bTiytOvHGE;se_Giq2XD=B>}3kN#3JCkIm?WJ=4xWkaH8>S1^G%Qp5=%V9W zn$B{rTVWy`BERvJ#;6m4mn~^7ykNzBk_a8okJm_NaSqY_u7FNxW0xA5VK8vrt-Cwg zSbC?2CB#BpCf)b37`Q`$(2h#%;?;#?Nm7LwaPEn*T*G;13^l`NJ9YF6ok^P1yOMpO zi&Farzo~PyWFNVf2J)p$2%)4)F$_NX<4ukC8J+!E@4EA>4F(2$4^P#kZ{3#Pn7`1i zA#|p9zEnIn<%;!N3U!CEqY%*4TtC_IP7w66z-!ppE*NPxWzZb_J4gpt#C3(C$zcEb z8*BP%ZRz|26SDIp{WP_)Ff?mVA5WCt`4E0_>Bd*e@1JZA4dJAJ(~w?QMug9o8Z8hXsx6%3 z$9R0#K=aCtqLxP1&TJ&l&FLC$9$$jqPJhhon>8$kOS|jXEc@8XM~tvRqmw#^i^l2e z0L`ec{pj&^Vy=jq*J?oli_X1tY5(T3STBk*y*shEmh#WsiDxqkVqr|i&_T%B=39%- z#f{z%oEndY8*9~wG}6S#$*Q$~pU&G($XRy0FJf*bveBlQgLo`A-S;2!J!=a!v}YHO z7aHo|HMo5wj(6%uYOw*DW{PPWJhUn#{bo76^PF!qTapGjTczOUR%fqls;o@3aqtO9}9|grdmhz4xoCr=J zaKVv-*{_DyYSLGm@!QY@)A)l!g^$9W)$x;6L_Qco76!ikt)MvKHb z#2w3hvH9@U;E~Udp@*0U+AhKYeh&KZvjY3aVlSQJ1GC3JJ=0f7?>R2}Nrd?5F;#7v zwgfM4(C5;gxZP!461R z2vKu~nz6oSaeQV_-Y{nAD-K^bw9@nB5-(hyn$9;(K~N&FgIuP z<;EceRw}A!P9Cl%wM@R!_{l3g`_RFqi3~T4iCD#sfl5CUv<#ik?Cr zCE$SwbPGlkQ&Vrk_{#Prp^8HeQ4#M~T@-+<5!kD>mJyb^ECt6A|LRPv(*ek19~oM+ z(K-GjK+7!t?4B+7b8X6kXb)jS5I;lEZbh@z} zRV~$Z@x?{HjKhP;-mHp2+*u_ZJd7_dR7Ae^0b?(8Hju=h`d`V2SHo`@t(Y3CtGTCjzsVR^cyjjp)+wyD5 zSiHeG1J|?5jr~N8^-P4rQb!PD3+M}aPD6@()Pk@saPs1BEwuM&*2OedT7S$g%_p$m zvS>o1WhCP^_I`PvLrfM6d%;%*lFB)lc>+yL^ihARc}B`-M@GgYjoi3Hn0c>dbh8+-bH1-lJfz zKtI*3_jHW3sa+Ko$>1&QCS%m`c#!2Z9z?B+U-BAQ_|n*zK0qdS+`Sk};K*;8jh)Ne z|Dp7hw$S4ZhO!L_~^GlL4v^3i{?fH{1 z7ZSQ2xLF!a3&4Vz{8wVUve@d)zr`>p)Mc;ApaL7YJe}UH<~_2Gx_wk5^!5nz+41ny zutsR%y6_(~MqWUnjsmDovPp<{UR`xzVgJmFf+q4-(f+t|!rH5| zliu#{8wh#SPEWgFxXF0|+QYYy_pd>Oj=bH(AKt4;s+!L|@jp6{pG$(vt)g3w?w=gL z)Rn}-Nk`u5nFFN577eds6|7*Ic~%3kx*vE;&7-$iKQU0;LrOVAokR%V>=@DLbZuuR zcjmG5&sH7zG1)Bo4efaq#LVoz#+Eim>t)h$O}TQ(XY@{vnboSBZ&>a6@=#RR;&gjV zD`ud}Rm#xtiw6j}p<{$w@tJaox%AudU*%@CL0>rNO-}3Fh8ljv8`#LkG{s-$LCn25 z1<=(w3N*t%>q4!PHmGGUBFmKifd?x9L~(vE@K$cNOsG#`Y`4`)u=^*jNzKAtJ(lJ< zuJrC9P7Qh6`sZ*1kJB@wqIgDsKlu< zzWZiH9rw81bUiF}JVmHETc3?eXa#E;9?A1&FDqYgO_%l(w*CQ51G1E23p`mW;C*0% zY0g+*x0+6XCL~a_F1ui;8UKK$;23L>NVttNNClicJuq$f*7!lquVuNDD?QlSV>ZToC_@>0|dh8dWdLo?p=hR$<@kjGNaY^;wV4R-Y@s~YYWt~N? z(;-XO+}k|)d3bpTO4Kve93B2eYMtt2zLjzh;PwecuVpnl(y+HQBldNpXBuSDoM;Yy zs!_Rk;qX>)n;GN`YMXIF4x%EQ#VvL!em z!OTFmnI(BXfxxH#A*nc?O&TQ4eoTFuC&3Jg+V$G{M^i~Yq~J;91s3*lV!Y_>u-ITuhY69V zi_V#)b}i z+ndQ6u=4C!rs(9~;lr!_mvYQ5+zjr2o$+8v{jWSvWgPS*a+ygeHqXdHzY5?fYDED~V^d3`) zALlgK(=*NJRb#WwLKvdidGXzZ^ko`T!D!0tW!*}Cv`zJ~#=1P_J@W|Zc6d-ayCNoZ z_3m@T-bS|E7`O4`NEjsO*AC2LmfR8U%OODn$9*L!@kWFIO%RuNmv8^9!mm>$zZ2Wb zA|vGWZOU^<*P8m9OA0@X?Jpwi(CAC8$!VL}ZJ z5@VLoY{gh_s1)LO%?d>mWKQR%iT~r($tu@+&i#aaWVD-W0(5Zm!{P5OUme`r>#eNT zKQK5sm3$~T0MO~Ac*T3a{CP&Pa4oTTFDAr+p{?|y%)CV62OGT!?Ag^f2XIk>aV!ZH zN(?o}sPd_6g~_x0ho%drFV20bUQvMh{;6Fbtd?I2N%z&OLnf8@dBU;pGF*H!I{F^fuqk<3pmd*G4 zIUALsv)|;jeRo6RIj}+lZ6o555hkG;VoKQ>JPxxZjI)t6oYDLP9x8 zyFnZ%c=%9Nm4tsX%lUr1O!O_0T24S#a|vF<$r)XyMt3kP4UGOS(i2*v?9L!bW0y^8 zBVy0r z2|K#`K$4Tqd0mJAz?rrJs@K5IJ=aD&!gA2WhRsaFPyUG~((I6%)$ZQ_m%!Ed?f~?J zpDFn1tH*&m+1{R(n#GemRm}E2NVqVj2G97-5M`+`|1>j7I_VhA$t8%N?M+92reN@# zy7+G$(5GQmP{&zC_Doq0EK{lt^=*ND=x#~-mK7DBn(@b_`%gp}Pq>WV&S!6p0FY4& z7_+g^5o0UypQ+@O#Ge__^A-O8VtV=C8&jzu6t1v_Ag4IdpWzlcTl~M7djI>t>(S5O aceu(Uj9(o+F~d%Pag-Hb1FK(Hg#HiErZ#i{ literal 0 HcmV?d00001 diff --git a/setup.py b/setup.py index 8a14888..45fed45 100644 --- a/setup.py +++ b/setup.py @@ -11,10 +11,10 @@ # The plugin's human readable name. Can be overwritten within OctoPrint's internal data via __plugin_name__ in the # plugin module -plugin_name = "PrusaSlicer Thumbnails" +plugin_name = "Slicer Thumbnails" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "0.1.4" +plugin_version = "1.0.0" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module @@ -33,7 +33,7 @@ plugin_license = "AGPLv3" # Any additional requirements besides OctoPrint should be listed here -plugin_requires = [] +plugin_requires = ["Pillow"] ### -------------------------------------------------------------------------------------------------------------------- ### More advanced options that you usually shouldn't have to touch follow after this point From bd0c083009d51eda5a811e6bb625ddbaf187e28a Mon Sep 17 00:00:00 2001 From: jneilliii Date: Sat, 22 May 2021 00:39:56 -0400 Subject: [PATCH 4/6] update gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index ecfcd6f..2343769 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ dist *.egg* .DS_Store *.zip +/git-flow-plus.config From cde1ad07af5e7a961c1c89d9d1482e0a54f4c64d Mon Sep 17 00:00:00 2001 From: jneilliii Date: Sat, 22 May 2021 01:32:18 -0400 Subject: [PATCH 5/6] change filename replacement to allow .gco (#77) and resolve issues with filenames that end in gcode.gcode. add tft file upload support for new MKS format. --- octoprint_prusaslicerthumbnails/__init__.py | 44 ++++++++++++--------- setup.py | 2 +- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/octoprint_prusaslicerthumbnails/__init__.py b/octoprint_prusaslicerthumbnails/__init__.py index 33821c7..5e3861f 100644 --- a/octoprint_prusaslicerthumbnails/__init__.py +++ b/octoprint_prusaslicerthumbnails/__init__.py @@ -10,6 +10,8 @@ import datetime import io from PIL import Image +import re +import base64 class PrusaslicerthumbnailsPlugin(octoprint.plugin.SettingsPlugin, @@ -27,8 +29,9 @@ def __init__(self): self._folderRemovalLastAdded = {} self._waitForAnalysis = False self._analysis_active = False + self.regex_extension = re.compile("\.(?:gco(?:de)?|tft)$") - ##~~ SettingsPlugin mixin + # ~~ SettingsPlugin mixin def get_settings_defaults(self): return dict( @@ -46,7 +49,7 @@ def get_settings_defaults(self): scale_inline_thumbnail_position=False ) - ##~~ AssetPlugin mixin + # ~~ AssetPlugin mixin def get_assets(self): return dict( @@ -54,7 +57,7 @@ def get_assets(self): css=["css/prusaslicerthumbnails.css"] ) - ##~~ TemplatePlugin mixin + # ~~ TemplatePlugin mixin def get_template_configs(self): return [ @@ -62,8 +65,6 @@ def get_template_configs(self): ] def _extract_thumbnail(self, gcode_filename, thumbnail_filename): - import re - import base64 regex = r"(?:^; thumbnail begin \d+[x ]\d+ \d+)(?:\n|\r\n?)((?:.+(?:\n|\r\n?))+?)(?:^; thumbnail end)" regex_mks = re.compile('(?:;(?:simage|;gimage):).*?M10086 ;[\r\n]', re.DOTALL) lineNum = 0 @@ -142,7 +143,7 @@ def find_best_thumbnail(self, gcode_encoded_images): # Image not found return None - ##~~ EventHandlerPlugin mixin + # ~~ EventHandlerPlugin mixin def on_event(self, event, payload): if event not in ["FileAdded", "FileRemoved", "FolderRemoved", "FolderAdded"]: @@ -158,22 +159,19 @@ def on_event(self, event, payload): results = self._process_gcode(local_files[file_key], results) self._logger.debug("Scan results: {}".format(results)) if event in ["FileAdded", "FileRemoved"] and payload["storage"] == "local" and "gcode" in payload["type"]: - thumbnail_filename = self.get_plugin_data_folder() + "/" + payload["path"].replace(".gcode", ".png") + thumbnail_path = self.regex_extension.sub(".png", payload["path"]) + thumbnail_filename = "{}/{}".format(self.get_plugin_data_folder(), thumbnail_path) if os.path.exists(thumbnail_filename): os.remove(thumbnail_filename) if event == "FileAdded": gcode_filename = self._file_manager.path_on_disk("local", payload["path"]) self._extract_thumbnail(gcode_filename, thumbnail_filename) if os.path.exists(thumbnail_filename): - thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/" + payload["path"].replace(".gcode", - ".png") + "?" + "{:%Y%m%d%H%M%S}".format( - datetime.datetime.now()) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", - thumbnail_url.replace("//", "/"), overwrite=True) - self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", - self._identifier, overwrite=True) + thumbnail_url = "plugin/prusaslicerthumbnails/thumbnail/{}?{:%Y%m%d%H%M%S}".format(thumbnail_path, datetime.datetime.now()) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail", thumbnail_url.replace("//", "/"), overwrite=True) + self._file_manager.set_additional_metadata("local", payload["path"], "thumbnail_src", self._identifier, overwrite=True) - ##~~ SimpleApiPlugin mixin + # ~~ SimpleApiPlugin mixin def _process_gcode(self, gcode_file, results=[]): self._logger.debug(gcode_file["path"]) @@ -199,7 +197,6 @@ def get_api_commands(self): def on_api_command(self, command, data): import flask - import json from octoprint.server import user_permission if not user_permission.can(): return flask.make_response("Insufficient rights", 403) @@ -214,9 +211,17 @@ def on_api_command(self, command, data): results = self._process_gcode(LocalFiles[key], results) return flask.jsonify(results) - ##~~ Routes hook + # ~~ extension_tree hook + def get_extension_tree(self, *args, **kwargs): + return dict( + machinecode=dict( + gcode=["tft"] + ) + ) + + # ~~ Routes hook def route_hook(self, server_routes, *args, **kwargs): - from octoprint.server.util.tornado import LargeResponseHandler, UrlProxyHandler, path_validation_factory + from octoprint.server.util.tornado import LargeResponseHandler, path_validation_factory from octoprint.util import is_hidden_path return [ (r"thumbnail/(.*)", LargeResponseHandler, dict(path=self.get_plugin_data_folder(), @@ -225,7 +230,7 @@ def route_hook(self, server_routes, *args, **kwargs): lambda path: not is_hidden_path(path), status_code=404))) ] - ##~~ Softwareupdate hook + # ~~ Softwareupdate hook def get_update_information(self): return dict( @@ -266,5 +271,6 @@ def __plugin_load__(): global __plugin_hooks__ __plugin_hooks__ = { "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information, + "octoprint.filemanager.extension_tree": __plugin_implementation__.get_extension_tree, "octoprint.server.http.routes": __plugin_implementation__.route_hook } diff --git a/setup.py b/setup.py index 45fed45..ae84b9f 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "Slicer Thumbnails" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "1.0.0" +plugin_version = "1.0.1dev1" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module From 33530aca88bfebcb8bebfed249a3ddf345702c85 Mon Sep 17 00:00:00 2001 From: jneilliii Date: Sat, 22 May 2021 02:31:45 -0400 Subject: [PATCH 6/6] fix modal popup with .gco extension --- .../static/js/prusaslicerthumbnails.js | 22 +++++++++---------- setup.py | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js b/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js index 66152e1..05cd03c 100644 --- a/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js +++ b/octoprint_prusaslicerthumbnails/static/js/prusaslicerthumbnails.js @@ -20,14 +20,14 @@ $(function() { self.crawl_results = ko.observableArray([]); self.filesViewModel.prusaslicerthumbnails_open_thumbnail = function(data) { - if(data.name.indexOf('.gcode') > 0){ - var thumbnail_title = data.path.replace('.gcode',''); + if(data.thumbnail_src === "prusaslicerthumbnails"){ + var thumbnail_title = data.name.replace(/\.(?:gco(?:de)?|tft)$/,''); self.thumbnail_url(data.thumbnail); self.thumbnail_title(thumbnail_title); self.file_details(data); $('div#prusa_thumbnail_viewer').modal("show"); } - } + }; self.DEFAULT_THUMBNAIL_SCALE = "100%"; self.filesViewModel.thumbnailScaleValue = ko.observable(self.DEFAULT_THUMBNAIL_SCALE); @@ -55,18 +55,18 @@ $(function() { self.crawl_results.push({name: ko.observable(key), files: ko.observableArray(data[key])}); } } - if(self.crawl_results().length == 0){ + if(self.crawl_results().length === 0){ self.crawl_results.push({name: ko.observable('No convertible files found'), files: ko.observableArray([])}); } self.filesViewModel.requestData({force: true}); self.crawling_files(false); }).fail(function(data){ self.crawling_files(false); - }) - } + }); + }; self.onBeforeBinding = function() { - // inject filelist thumpnail into template + // inject filelist thumbnail into template let regex = /
([\s\S]*)<.div>/mi; let template = ''; @@ -76,12 +76,12 @@ $(function() { 'visible: ($data.thumbnail_src == \'prusaslicerthumbnails\' && $root.settingsViewModel.settings.plugins.prusaslicerthumbnails.inline_thumbnail() == true), ' + 'click: function() { if ($root.loginState.isUser() && !($(\'html\').attr(\'id\') === \'touch\')) { $root.prusaslicerthumbnails_open_thumbnail($data) } else { return; } },' + 'style: {\'width\': (!$root.thumbnailPositionLeft()) ? $root.thumbnailScaleValue() : \'100%\' }" ' + - 'style="display: none;"/>
' + 'style="display: none;"/>'; $("#files_template_machinecode").text(function () { var return_value = inline_thumbnail_template + $(this).text(); return_value = return_value.replace(regex, '
$1 ' + template + '>
'); - return return_value + return return_value; }); // assign initial scaling @@ -116,7 +116,7 @@ $(function() { self.filesViewModel.thumbnailScaleValue(newValue + "%"); }); self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value.subscribe(function(newValue){ - $('#prusaslicer_state_thumbnail').attr({'width': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() + '%'}) + $('#prusaslicer_state_thumbnail').attr({'width': self.settingsViewModel.settings.plugins.prusaslicerthumbnails.state_panel_thumbnail_scale_value() + '%'}); }); // observe alignment changes @@ -168,7 +168,7 @@ $(function() { $('#prusaslicer_state_thumbnail').remove(); } }); - } + }; } OCTOPRINT_VIEWMODELS.push({ diff --git a/setup.py b/setup.py index ae84b9f..89c48fd 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,7 @@ plugin_name = "Slicer Thumbnails" # The plugin's version. Can be overwritten within OctoPrint's internal data via __plugin_version__ in the plugin module -plugin_version = "1.0.1dev1" +plugin_version = "1.0.1rc1" # The plugin's description. Can be overwritten within OctoPrint's internal data via __plugin_description__ in the plugin # module