diff --git a/.editorconfig b/.editorconfig index d02b3d9..74fb905 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,9 @@ charset = utf-8 insert_final_newline = true trim_trailing_whitespace = true indent_style = space -max_line_length = 90 +max_line_length = 180 +indent_size = 2 + [**.py] indent_size = 4 diff --git a/.gitignore b/.gitignore index 9bf3eaf..e4b2ccc 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,7 @@ dist .vscode .vscode/** -thunder-tests \ No newline at end of file +thunder-tests +vscode.env +.venv +OctoKlipper.egg-info \ No newline at end of file diff --git a/README.md b/README.md index cb7f60b..c0dc053 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ This plugin assists in managing and monitoring the [Klipper](https://github.com/ - PID Tuning Dialog. - Dialog to set a coordinate offset for future GCODE move commands. - Message log displaying messages from Klipper prepended with "//" and "!!". -- Basic Klipper configuration editor +- Klipper configuration editor - Performance graph displaying key parameters extracted from the Klipper logs. ## Installation @@ -46,7 +46,7 @@ Also for the moment this plugin does what I wanted it to do, it is far from fini * The [devel](https://github.com/thelastWallE/OctoprintKlipperPlugin/tree/devel) branch is the branch to merge new features and bugfixes to. * The [rc](https://github.com/thelastWallE/OctoprintKlipperPlugin/tree/rc) branch is for Release Candidates and bugfixing them. -* The [master](https://github.com/thelastWallE/OctoprintKlipperPlugin/tree/master) branch is for Stable Releases. +* The [master](https://github.com/thelastWallE/OctoprintKlipperPlugin/tree/master) branch is for Stable Releases. ## Screenshots diff --git a/octoprint_klipper/__init__.py b/octoprint_klipper/__init__.py index 5b435d6..4315455 100644 --- a/octoprint_klipper/__init__.py +++ b/octoprint_klipper/__init__.py @@ -14,35 +14,38 @@ # along with this program. If not, see . from __future__ import absolute_import, division, print_function, unicode_literals -import datetime import logging import octoprint.plugin import octoprint.plugin.core import glob import os +import time import sys -import io + +from octoprint.server import NO_CONTENT +from octoprint.util import is_hidden_path +from octoprint.util import get_formatted_size +from . import util, cfgUtils, logger from octoprint.util.comm import parse_firmware_line -from octoprint.access.permissions import Permissions, ADMIN_GROUP, USER_GROUP +from octoprint.access.permissions import Permissions, ADMIN_GROUP from .modules import KlipperLogAnalyzer +from octoprint.server.util.flask import restricted_access import flask from flask_babel import gettext -try: - import configparser -except ImportError: - import ConfigParser as configparser - if sys.version_info[0] < 3: import StringIO +MAX_UPLOAD_SIZE = 5 * 1024 * 1024 # 5Mb + class KlipperPlugin( octoprint.plugin.StartupPlugin, octoprint.plugin.TemplatePlugin, octoprint.plugin.SettingsPlugin, octoprint.plugin.AssetPlugin, octoprint.plugin.SimpleApiPlugin, - octoprint.plugin.EventHandlerPlugin): + octoprint.plugin.EventHandlerPlugin, + octoprint.plugin.BlueprintPlugin): _parsing_response = False _parsing_check_response = True @@ -75,8 +78,10 @@ class KlipperPlugin( self._settings.global_set( ["serial", "additionalPorts"], additional_ports) self._settings.save() - self.log_info( - "Added klipper serial port {} to list of additional ports.".format(klipper_port)) + logger.log_info( + self, + "Added klipper serial port {} to list of additional ports.".format(klipper_port) + ) # -- Settings Plugin @@ -101,6 +106,7 @@ class KlipperPlugin( ] def get_settings_defaults(self): + # TODO #69 put some settings on the localStorage return dict( connection=dict( port="/tmp/printer", @@ -125,103 +131,20 @@ class KlipperPlugin( ), configuration=dict( debug_logging=False, - configpath="~/printer.cfg", - old_config="", + config_path="~/", + baseconfig="printer.cfg", logpath="/tmp/klippy.log", reload_command="RESTART", + restart_onsave=True, + confirm_reload=True, shortStatus_navbar=True, shortStatus_sidebar=True, parse_check=False, - fontsize=9 + fontsize=12 ) ) - def on_settings_load(self): - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - try: - with io.open(configpath, "r", encoding="utf8") as f: - data["config"] = f.read() - f.close() - except IOError: - self.log_error( - "Error: Klipper config file not found at: {}".format( - configpath) - ) - except UnicodeDecodeError as e: - self.log_debug( - "Loading config with utf-8 failed. Trying to load config file with ISO-8859-1 now." - ) - try: - with io.open(configpath, "r", encoding="ISO-8859-1") as f: - data["config"] = f.read() - f.close() - except UnicodeDecodeError as e: - self.log_error( - "Error: Klipper config file cannot be decoded: {}".format(e) - ) - else: - self.log_debug( - "Loading config with ISO-8859-1 finished." - ) - self.send_message("reload", "config", "", data["config"]) - # send the configdata to frontend to update ace editor - else: - self.send_message("reload", "config", "", data["config"]) - # send the configdata to frontend to update ace editor - return data - def on_settings_save(self, data): - - self.log_debug( - "Save klipper configs" - ) - - if "config" in data: - if self.key_exist(data, "configuration", "parse_check"): - check_parse = data["configuration"]["parse_check"] - else: - check_parse = self._settings.get(["configuration", "parse_check"]) - - - # check for configpath if it was changed during changing of the configfile - if self.key_exist(data, "configuration", "configpath"): - configpath = os.path.expanduser( - data["configuration"]["configpath"] - ) - else: - # if the configpath was not changed during changing the printer.cfg. Then the configpath would not be in data[] - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) - if self.file_exist(configpath) and (self._parsing_check_response or not check_parse): - try: - with io.open(configpath, "w", encoding="utf-8") as f: - f.write(data["config"]) - f.close() - self.log_debug("Writing Klipper config to {}".format(configpath)) - except IOError: - self.log_error("Error: Couldn't write Klipper config file: {}".format(configpath)) - else: - #load the reload command from changed data if it is not existing load the saved setting - if self.key_exist(data, "configuration", "reload_command"): - reload_command = os.path.expanduser( - data["configuration"]["reload_command"] - ) - else: - reload_command = self._settings.get(["configuration", "reload_command"]) - - if reload_command != "manually": - # Restart klippy to reload config - self._printer.commands(reload_command) - self.log_info("Restarting Klipper.") - # we don't want to write the klipper conf to the octoprint settings - data.pop("config", None) - - # save the rest of changed settings into config.yaml of octoprint old_debug_logging = self._settings.get_boolean(["configuration", "debug_logging"]) octoprint.plugin.SettingsPlugin.on_settings_save(self, data) @@ -237,7 +160,7 @@ class KlipperPlugin( return dict( admin=[ ["connection", "port"], - ["configuration", "configpath"], + ["configuration", "config_path"], ["configuration", "replace_connection_panel"] ], user=[ @@ -247,66 +170,61 @@ class KlipperPlugin( ) def get_settings_version(self): - return 3 + # Settings_Versionhistory: + # 3 = add shortstatus on navbar. migrate the navbar setting for this + # 4 = -change of configpath to config_path with only path without filename + # -parse configpath into config_path and baseconfig + # -switch setting for 'restart on editor save' to true if it was not set to manually + # -remove old_config + # -remove config on root settingsdirectory + return 4 + #migrate Settings def on_settings_migrate(self, target, current): + settings = self._settings if current is None: - settings = self._settings + util.migrate_old_settings(settings) - if settings.has(["serialport"]): - settings.set(["connection", "port"], - settings.get(["serialport"])) - settings.remove(["serialport"]) + if current is not None and current < 3: + self.migrate_settings_3(settings) - if settings.has(["replace_connection_panel"]): - settings.set( - ["connection", "replace_connection_panel"], - settings.get(["replace_connection_panel"]) - ) - settings.remove(["replace_connection_panel"]) + if current is not None and current < 4: + self.migrate_settings_4(settings) - if settings.has(["probeHeight"]): - settings.set(["probe", "height"], - settings.get(["probeHeight"])) - settings.remove(["probeHeight"]) + def migrate_settings_3(self, settings): + util.migrate_settings_configuration( + settings, + "shortStatus_navbar", + "navbar", + ) - if settings.has(["probeLift"]): - settings.set(["probe", "lift"], settings.get(["probeLift"])) - settings.remove(["probeLift"]) + def migrate_settings_4(self, settings): + if settings.has(["configuration", "configpath"]): + cfg_path = settings.get(["configuration", "configpath"]) + new_cfg_path, baseconfig = os.path.split(cfg_path) + logger.log_info(self, "migrate setting for 'configuration/config_path': " + cfg_path + " -> " + new_cfg_path) + logger.log_info(self, "migrate setting for 'configuration/baseconfig': printer.cfg -> " + baseconfig) + settings.set(["configuration", "config_path"], new_cfg_path) + settings.set(["configuration", "baseconfig"], baseconfig) + settings.remove(["configuration", "configpath"]) + if ( + settings.has(["configuration", "reload_command"]) + and settings.get(["configuration", "reload_command"]) == "manually" + ): + logger.log_info(self, "migrate setting for 'configuration/restart_onsave': True -> False") + settings.set(["configuration", "restart_onsave"], False) + settings.remove(["configuration", "reload_command"]) - if settings.has(["probeSpeedXy"]): - settings.set(["probe", "speed_xy"], - settings.get(["probeSpeedXy"])) - settings.remove(["probeSpeedXy"]) + if settings.has(["config"]): + logger.log_info(self, "remove old setting for 'config'") + settings.remove(["config"]) - if settings.has(["probeSpeedZ"]): - settings.set(["probe", "speed_z"], - settings.get(["probeSpeedZ"])) - settings.remove(["probeSpeedZ"]) + if settings.has(["configuration", "old_config"]): + logger.log_info(self, "remove old setting for 'configuration/old_config'") + settings.remove(["configuration", "old_config"]) - if settings.has(["probePoints"]): - points = settings.get(["probePoints"]) - points_new = [] - for p in points: - points_new.append( - dict(name="", x=int(p["x"]), y=int(p["y"]), z=0)) - settings.set(["probe", "points"], points_new) - settings.remove(["probePoints"]) - - if settings.has(["configPath"]): - self.log_info("migrate setting for: configPath") - settings.set(["config_path"], settings.get(["configPath"])) - settings.remove(["configPath"]) - - if target is 3 and current is 2: - settings = self._settings - if settings.has(["configuration", "navbar"]): - self.log_info("migrate setting for: configuration/navbar") - settings.set(["configuration", "shortStatus_navbar"], settings.get(["configuration", "navbar"])) - settings.remove(["configuration", "navbar"]) # -- Template Plugin - def get_template_configs(self): return [ dict(type="navbar", custom_bindings=True), @@ -337,17 +255,29 @@ class KlipperPlugin( custom_bindings=True ), dict(type="sidebar", - custom_bindings=True, - icon="rocket", - replaces="connection" if self._settings.get_boolean( - ["connection", "replace_connection_panel"]) else "" - ), + custom_bindings=True, + icon="rocket", + replaces="connection" if self._settings.get_boolean( + ["connection", "replace_connection_panel"]) else "" + ), dict( type="generic", name="Performance Graph", template="klipper_graph_dialog.jinja2", custom_bindings=True ), + dict( + type="generic", + name="Config Backups", + template="klipper_backups_dialog.jinja2", + custom_bindings=True + ), + dict( + type="generic", + name="Config Editor", + template="klipper_editor.jinja2", + custom_bindings=True + ), dict( type="generic", name="Macro Dialog", @@ -356,6 +286,12 @@ class KlipperPlugin( ) ] + def get_template_vars(self): + return { + "max_upload_size": MAX_UPLOAD_SIZE, + "max_upload_size_str": get_formatted_size(MAX_UPLOAD_SIZE), + } + # -- Asset Plugin def get_assets(self): @@ -366,51 +302,66 @@ class KlipperPlugin( "js/klipper_pid_tuning.js", "js/klipper_offset.js", "js/klipper_param_macro.js", - "js/klipper_graph.js" - ], + "js/klipper_graph.js", + "js/klipper_backup.js", + "js/klipper_editor.js" + ], + clientjs=["clientjs/klipper.js"], css=["css/klipper.css"] ) # -- Event Handler Plugin def on_event(self, event, payload): - if "UserLoggedIn" == event: - self.update_status("info", "Klipper: Standby") - if "Connecting" == event: - self.update_status("info", "Klipper: Connecting ...") - elif "Connected" == event: - self.update_status("info", "Klipper: Connected to host") - self.log_info( + if event == "UserLoggedIn": + logger.log_info(self, "Klipper: Standby") + if event == "Connecting": + logger.log_info(self, "Klipper: Connecting ...") + elif event == "Connected": + logger.log_info(self, "Klipper: Connected to host") + logger.log_info( + self, "Connected to host via {} @{}bps".format(payload["port"], payload["baudrate"])) - elif "Disconnected" == event: - self.update_status("info", "Klipper: Disconnected from host") - elif "Error" == event: - self.update_status("error", "Klipper: Error") - self.log_error(payload["error"]) + elif event == "Disconnected": + logger.log_info(self, "Klipper: Disconnected from host") + + elif event == "Error": + logger.log_error(self, payload["error"]) + + def processAtCommand(self, comm_instance, phase, command, parameters, tags=None, *args, **kwargs): + if command != "SWITCHCONFIG": + return + + config = parameters + logger.log_info(self, "SWITCHCONFIG detected config:{}".format(config)) + return None # -- GCODE Hook + def process_sent_GCODE(self, comm_instance, phase, cmd, cmd_type, gcode, *args, **kwargs): + if cmd == "SAVE_CONFIG": + logger.log_info(self, "SAVE_CONFIG detected") + util.send_message(self, type = "reload", subtype = "config") def on_parse_gcode(self, comm, line, *args, **kwargs): if "FIRMWARE_VERSION" in line: printerInfo = parse_firmware_line(line) if "FIRMWARE_VERSION" in printerInfo: - self.log_info("Firmware version: {}".format( + logger.log_info(self, "Firmware version: {}".format( printerInfo["FIRMWARE_VERSION"])) elif "// probe" in line or "// Failed to verify BLTouch" in line: msg = line.strip('/') - self.log_info(msg) + logger.log_info(self, msg) self.write_parsing_response_buffer() elif "//" in line: # add lines with // to a buffer self._message = self._message + line.strip('/') if not self._parsing_response: - self.update_status("info", self._message) + util.update_status(self, "info", self._message) self._parsing_response = True elif "!!" in line: msg = line.strip('!') - self.update_status("error", msg) - self.log_error(msg) + logger.log_error(self, msg) self.write_parsing_response_buffer() else: self.write_parsing_response_buffer() @@ -420,15 +371,13 @@ class KlipperPlugin( # write buffer with // lines after a gcode response without // if self._parsing_response: self._parsing_response = False - self.log_info(self._message) + logger.log_info(self, self._message) self._message = "" def get_api_commands(self): return dict( listLogFiles=[], - getStats=["logFile"], - reloadConfig=[], - checkConfig=["config"] + getStats=["logFile"] ) def on_api_command(self, command, data): @@ -437,70 +386,182 @@ class KlipperPlugin( logpath = os.path.expanduser( self._settings.get(["configuration", "logpath"]) ) - if self.file_exist(logpath): + if util.file_exist(self, logpath): for f in glob.glob(self._settings.get(["configuration", "logpath"]) + "*"): filesize = os.path.getsize(f) + filemdate = time.strftime("%d.%m.%Y %H:%M",time.localtime(os.path.getctime(f))) files.append(dict( - name=os.path.basename( - f) + " ({:.1f} KB)".format(filesize / 1000.0), + name=os.path.basename(f) + " (" + filemdate + ")", file=f, size=filesize )) - return flask.jsonify(data=files) - else: - return flask.jsonify(data=files) + return flask.jsonify(data=files) elif command == "getStats": if "logFile" in data: log_analyzer = KlipperLogAnalyzer.KlipperLogAnalyzer( data["logFile"]) return flask.jsonify(log_analyzer.analyze()) - elif command == "reloadConfig": - data = octoprint.plugin.SettingsPlugin.on_settings_load(self) - configpath = os.path.expanduser( - self._settings.get(["configuration", "configpath"]) - ) + def is_blueprint_protected(self): + return False + + def route_hook(self, server_routes, *args, **kwargs): + from octoprint.server.util.tornado import LargeResponseHandler, path_validation_factory + from octoprint.util import is_hidden_path + configpath = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + bak_path = os.path.join(self.get_plugin_data_folder(), "configs", "") + + return [ + (r"/download/configs/(.*)", LargeResponseHandler, dict(path=configpath, + as_attachment=True, + path_validation=path_validation_factory(lambda path: not is_hidden_path(path), + status_code=404))), + (r"/download/backup/(.*)", LargeResponseHandler, dict(path=bak_path, + as_attachment=True, + path_validation=path_validation_factory(lambda path: not is_hidden_path(path), + status_code=404))) + ] + +# API for Backups + # Get Content of a Backupconfig + @octoprint.plugin.BlueprintPlugin.route("/backup/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def get_backup(self, filename): + data_folder = self.get_plugin_data_folder() + full_path = os.path.realpath(os.path.join(data_folder, "configs", filename)) + response = cfgUtils.get_cfg(self, full_path) + return flask.jsonify(response = response) + + # Delete a Backupconfig + @octoprint.plugin.BlueprintPlugin.route("/backup/", methods=["DELETE"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def delete_backup(self, filename): + data_folder = self.get_plugin_data_folder() + full_path = os.path.realpath(os.path.join(data_folder, "configs", filename)) + if ( + full_path.startswith(data_folder) + and os.path.exists(full_path) + and not is_hidden_path(full_path) + ): try: - with io.open(configpath, "r", encoding="utf-8") as f: - data["config"] = f.read() - f.close() - except IOError: - self.log_error( - "Error: Klipper config file not found at: {}".format( - configpath) - ) - except UnicodeDecodeError as e: - self.log_debug( - "Loading config with utf-8 failed. Trying to load config file with ISO-8859-1 now." - ) - try: - with io.open(configpath, "r", encoding="ISO-8859-1") as f: - data["config"] = f.read() - f.close() - except UnicodeDecodeError as e: - self.log_error( - "Error: Klipper config file cannot be decoded: {}".format(e) - ) - else: - self.log_debug( - "Loading config with ISO-8859-1 finished." - ) - self._settings.set(["config"], data["config"]) - return flask.jsonify(data=data["config"]) - else: + os.remove(full_path) + except Exception: + self._octoklipper_logger.exception("Could not delete {}".format(filename)) + raise + return NO_CONTENT + + # Get a list of all backed up configfiles + @octoprint.plugin.BlueprintPlugin.route("/backup/list", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def list_backups(self): + files = cfgUtils.list_cfg_files(self, "backup") + return flask.jsonify(files = files) + + # restore a backed up configfile + @octoprint.plugin.BlueprintPlugin.route("/backup/restore/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def restore_backup(self, filename): + configpath = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + data_folder = self.get_plugin_data_folder() + backupfile = os.path.realpath(os.path.join(data_folder, "configs", filename)) + return flask.jsonify(restored = cfgUtils.copy_cfg(self, backupfile, configpath)) + +# API for Configs + # Get Content of a Configfile + @octoprint.plugin.BlueprintPlugin.route("/config/", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def get_config(self, filename): + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + full_path = os.path.realpath(os.path.join(cfg_path, filename)) + response = cfgUtils.get_cfg(self, full_path) + return flask.jsonify(response = response) + + # Delete a Configfile + @octoprint.plugin.BlueprintPlugin.route("/config/", methods=["DELETE"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def delete_config(self, filename): + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + full_path = os.path.realpath(os.path.join(cfg_path, filename)) + if ( + full_path.startswith(cfg_path) + and os.path.exists(full_path) + and not is_hidden_path(full_path) + ): + try: + os.remove(full_path) + except Exception: + self._octoklipper_logger.exception("Could not delete {}".format(filename)) + raise + return NO_CONTENT + + # Get a list of all configfiles + @octoprint.plugin.BlueprintPlugin.route("/config/list", methods=["GET"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def list_configs(self): + files = cfgUtils.list_cfg_files(self, "") + path = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + return flask.jsonify(files = files, path = path, max_upload_size = MAX_UPLOAD_SIZE) + + # check syntax of a given data + @octoprint.plugin.BlueprintPlugin.route("/config/check", methods=["POST"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def check_config(self): + data = flask.request.json + data_to_check = data.get("DataToCheck", []) + response = cfgUtils.check_cfg_ok(self, data_to_check) + return flask.jsonify(is_syntax_ok = response) + + # save a configfile + @octoprint.plugin.BlueprintPlugin.route("/config/save", methods=["POST"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def save_config(self): + data = flask.request.json + filename = data.get("filename", []) + if filename == []: + flask.abort( + 400, + description="Invalid request, the filename is not set", + ) + Filecontent = data.get("DataToSave", []) + saved = cfgUtils.save_cfg(self, Filecontent, filename) + if saved == True: + util.send_message(self, type = "reload", subtype = "configlist") + return flask.jsonify(saved = saved) + + # restart klipper + @octoprint.plugin.BlueprintPlugin.route("/restart", methods=["POST"]) + @restricted_access + @Permissions.PLUGIN_KLIPPER_CONFIG.require(403) + def restart_klipper(self): + reload_command = self._settings.get(["configuration", "reload_command"]) + + if reload_command != "manually": + + # Restart klippy to reload config + self._printer.commands(reload_command) + logger.log_info(self, "Restarting Klipper.") + return flask.jsonify(command = reload_command) +# APIs end - self._settings.set(["config"], data["config"]) - return flask.jsonify(data=data["config"]) - elif command == "checkConfig": - if "config" in data: - if not self.validate_configfile(data["config"]): - self.log_debug("validateConfig not ok") - self._settings.set(["configuration", "old_config"], data["config"]) - return flask.jsonify(checkConfig="not OK") - else: - self.log_debug("validateConfig ok") - self._settings.set(["configuration", "old_config"], "") - return flask.jsonify(checkConfig="OK") def get_update_information(self): return dict( @@ -526,129 +587,28 @@ class KlipperPlugin( ) ) - #-- Helpers - def send_message(self, type, subtype, title, payload): - self._plugin_manager.send_plugin_message( - self._identifier, - dict( - time=datetime.datetime.now().strftime("%H:%M:%S"), - type=type, - subtype=subtype, - title=title, - payload=payload - ) - ) - - def poll_status(self): - self._printer.commands("STATUS") - - def update_status(self, type, status): - self.send_message("status", type, status, status) - - def log_info(self, message): - self._octoklipper_logger.info(message) - self.send_message("log", "info", message, message) - - def log_debug(self, message): - self._octoklipper_logger.debug(message) - self._logger.info(message) - # sends a message to frontend(in klipper.js -> self.onDataUpdaterPluginMessage) and write it to the console. - # _mtype, subtype=debug/info, title of message, message) - self.send_message("console", "debug", message, message) - - def log_error(self, error): - self._octoklipper_logger.error(error) - self._logger.info(error) - self.send_message("log", "error", error, error) - - def file_exist(self, filepath): - if not os.path.isfile(filepath): - self.send_message("PopUp", "warning", "OctoKlipper Settings", - "Klipper " + filepath + " does not exist!") - return False - else: - return True - - def key_exist(self, dict, key1, key2): - try: - dict[key1][key2] - except KeyError: - return False - else: - return True - - def validate_configfile(self, dataToBeValidated): - """ - --->SyntaxCheck for a given data<---- - """ - - try: - dataToValidated = configparser.RawConfigParser(strict=False) - # - if sys.version_info[0] < 3: - buf = StringIO.StringIO(dataToBeValidated) - dataToValidated.readfp(buf) - else: - dataToValidated.read_string(dataToBeValidated) - - sections_search_list = ["bltouch", - "probe"] - value_search_list = [ "x_offset", - "y_offset", - "z_offset"] - try: - # cycle through sections and then values - for y in sections_search_list: - for x in value_search_list: - if dataToValidated.has_option(y, x): - a_float = dataToValidated.getfloat(y, x) - except ValueError as error: - self.log_error( - "Error: Invalid Value for "+x+" in Section: "+y+"\n" + - "{}".format(str(error)) - ) - self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config\n", - "Config got not saved!\n" + - "You can reload your last changes\n" + - "on the 'Klipper Configuration' tab.\n\n" + - "Invalid Value for "+x+" in Section: "+y+"\n" + "{}".format(str(error))) - self._parsing_check_response = False - return False - except configparser.Error as error: - if sys.version_info[0] < 3: - error.message = error.message.replace("\\n","") - error.message = error.message.replace("file: u","Klipper Configuration", 1) - error.message = error.message.replace("'","", 2) - error.message = error.message.replace("u'","'", 1) - - else: - error.message = error.message.replace("\\n","") - error.message = error.message.replace("file:","Klipper Configuration", 1) - error.message = error.message.replace("'","", 2) - self.log_error( - "Error: Invalid Klipper config file:\n" + - "{}".format(str(error)) - ) - self.send_message("PopUp", "warning", "OctoKlipper: Invalid Config data\n", - "Config got not saved!\n" + - "You can reload your last changes\n" + - "on the 'Klipper Configuration' tab.\n\n" + str(error)) - self._parsing_check_response = False - return False - else: - self._parsing_check_response = True - return True - __plugin_name__ = "OctoKlipper" __plugin_pythoncompat__ = ">=2.7,<4" - +__plugin_settings_overlay__ = { + 'system': { + 'actions': [{ + 'action': 'octoklipper_restart', + 'command': 'sudo service klipper restart', + 'name': gettext('Restart Klipper'), + 'confirm': '

' + gettext("You are about to restart Klipper!") + '
' + gettext("This will stop ongoing prints!") + '


Command = "sudo service klipper restart"' + }] + } +} def __plugin_load__(): global __plugin_implementation__ global __plugin_hooks__ __plugin_implementation__ = KlipperPlugin() __plugin_hooks__ = { + "octoprint.server.http.routes": __plugin_implementation__.route_hook, "octoprint.access.permissions": __plugin_implementation__.get_additional_permissions, + "octoprint.comm.protocol.atcommand.sending": __plugin_implementation__.processAtCommand, + "octoprint.comm.protocol.gcode.sent": __plugin_implementation__.process_sent_GCODE, "octoprint.comm.protocol.gcode.received": __plugin_implementation__.on_parse_gcode, "octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information } diff --git a/octoprint_klipper/cfgUtils.py b/octoprint_klipper/cfgUtils.py new file mode 100644 index 0000000..477057d --- /dev/null +++ b/octoprint_klipper/cfgUtils.py @@ -0,0 +1,301 @@ +from __future__ import absolute_import, division, print_function, unicode_literals +import glob +import os, time, sys +import io +import flask + +from . import util, logger +from flask_babel import gettext +from shutil import copy, copyfile + +try: + import configparser +except ImportError: + import ConfigParser as configparser + +if sys.version_info[0] < 3: + import StringIO + + +def list_cfg_files(self, path): + """Generate list of config files. + + Args: + path (str): Path to the config files. + + Returns: + list: for every file a dict with keys for name, file, size, mdate, url. + """ + + files = [] + if path=="backup": + cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "*") + else: + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "config_path"]) + ) + cfg_path = os.path.join(cfg_path, "*.cfg") + cfg_files = glob.glob(cfg_path) + logger.log_debug(self, "list_cfg_files Path: " + cfg_path) + + for f in cfg_files: + filesize = os.path.getsize(f) + filemdate = time.localtime(os.path.getmtime(f)) + if path != "backup": + url = flask.url_for("index") + "plugin/klipper/download/configs/" + os.path.basename(f) + else: + url = flask.url_for("index") + "plugin/klipper/download/backup/" + os.path.basename(f) + files.append(dict( + name= os.path.basename(f), + file= f, + size= " ({:.1f} KB)".format(filesize / 1000.0), + mdate= time.strftime("%d.%m.%Y %H:%M", filemdate), + url= url, + )) + logger.log_debug(self, "list_cfg_files " + str(len(files)) + ": " + f) + return files + + +def get_cfg(self, file): + """Get the content of a configuration file. + + Args: + file (str): The name of the file to read + + Returns: + dict: + config (str): The configuration of the file + text (str): The text of the error + """ + + response = {"config":"", + "text": ""} + if not file: + cfg_path = os.path.expanduser( + self._settings.get(["configuration", "configpath"]) + ) + file = os.path.join(cfg_path, self._settings.get(["configuration", "baseconfig"])) + if util.file_exist(self, file): + logger.log_debug(self, "get_cfg_files Path: " + file) + try: + with io.open(file, "r", encoding='utf-8') as f: + response['config'] = f.read() + except IOError as Err: + logger.log_error( + self, + gettext("Error: Klipper config file not found at:") + + " {}".format(file) + + "\n" + + gettext("IOError:") + " {}".format(Err) + ) + response['text'] = Err + return response + except UnicodeDecodeError as Err: + logger.log_error( + self, + gettext("Decode Error:") + +"\n" + + "{}".format(Err) + + "\n\n" + + gettext("Please convert your config files to utf-8!") + + "\n" + + gettext("Or you can also paste your config \ninto the Editor and save it.") + ) + response['text'] = Err + return response + else: + return response + else: + response['text'] = gettext("File not found!") + return response + + +def save_cfg(self, content, filename): + """Save the configuration file to given file. + + Args: + content (str): The content of the configuration. + filename (str): The filename of the configuration file. Default is "printer.cfg" + + Returns: + bool: True if the configuration file was saved successfully. Otherwise False + """ + + logger.log_debug( + self, + "Save klipper config" + ) + + + configpath = os.path.expanduser(self._settings.get(["configuration", "config_path"])) + if filename == "": + filename = self._settings.get(["configuration", "baseconfig"]) + if filename[-4:] != ".cfg": + filename += ".cfg" + + filepath = os.path.join(configpath, filename) + + logger.log_debug(self, "Writing Klipper config to {}".format(filepath)) + try: + with io.open(filepath, "w", encoding='utf-8') as f: + f.write(content) + except IOError: + logger.log_error(self, "Error: Couldn't open Klipper config file: {}".format(filepath)) + return False + else: + logger.log_debug(self, "Written Klipper config to {}".format(filepath)) + return True + finally: + copy_cfg_to_backup(self, filepath) + + +def check_cfg_ok(self, data): + """Checks the given data on parsing errors. + + Args: + data (str): Content to be validated. + + Returns: + bool: True if the data is valid. False if it is not. + """ + try: + dataToValidated = configparser.RawConfigParser(strict=False) + if sys.version_info[0] < 3: + import StringIO + buf = StringIO.StringIO(data) + dataToValidated.readfp(buf) + else: + dataToValidated.read_string(data) + except configparser.Error as error: + show_error_message(self, error) + logger.log_debug(self, 'check_cfg: NOK!') + return False + else: + if not is_float_ok(self, dataToValidated): + logger.log_debug(self, "check_cfg: NOK!") + return False + logger.log_debug(self, "check_cfg: OK") + return True + + +def show_error_message(self, error): + error.message = error.message.replace('\\n', '') + if sys.version_info[0] < 3: + error.message = error.message.replace('file: u', 'Klipper Configuration', 1) + error.message = error.message.replace("'", '', 2) + error.message = error.message.replace("u'", "'", 1) + else: + error.message = error.message.replace('file:', 'Klipper Configuration', 1) + error.message = error.message.replace("'", '', 2) + logger.log_error( + self, + ('Error: Invalid Klipper config file:\n' + '{}'.format(str(error))), + ) + + +def is_float_ok(self, dataToValidated): + + sections_search_list = [ + "bltouch", + "probe" + ] + value_search_list = [ + "x_offset", + "y_offset", + "z_offset" + ] + try: + # cycle through sections and then values + for y in sections_search_list: + for x in value_search_list: + if dataToValidated.has_option(y, x): + a_float = dataToValidated.getfloat(y, x) + except ValueError as error: + logger.log_error( + self, + "Error: Invalid Value for " + x + " in Section: " + y + "\n" + + "{}".format(str(error)) + ) + util.send_message( + self, + type = "PopUp", + subtype = "warning", + title = "Invalid Config data\n", + payload = "\n" + + "Invalid Value for " + x + " in Section: " + y + "\n" + + "{}".format(str(error)) + ) + return False + else: + return True + + +def copy_cfg(self, file, dst): + """Copy the config file to the destination. + + Args: + file (str): Filepath of the config file to copy. + dst (str): Path to copy the config file to. + + Returns: + bool: True if the copy succeeded, False otherwise. + """ + + if os.path.isfile(file): + try: + copy(file, dst) + except IOError: + logger.log_error( + self, + "Error: Klipper config file not found at: {}".format(file) + ) + return False + else: + logger.log_debug( + self, + "File copied: " + + file + ) + return True + return False + + +def copy_cfg_to_backup(self, src): + """Copy the config file to backup directory of OctoKlipper. + + Args: + src (str): Path to the config file to copy. + + Returns: + bool: True if the config file was copied successfully. False otherwise. + """ + + if not os.path.isfile(src): + return False + + cfg_path = os.path.join(self.get_plugin_data_folder(), "configs", "") + filename = os.path.basename(src) + if not os.path.exists(cfg_path): + try: + os.mkdir(cfg_path) + except OSError: + logger.log_error(self, "Error: Creation of the backup directory {} failed".format(cfg_path)) + return False + else: + logger.log_debug(self, "Directory {} created".format(cfg_path)) + + dst = os.path.join(cfg_path, filename) + logger.log_debug(self, "copy_cfg_to_backup:" + src + " to " + dst) + if src == dst: + return False + try: + copyfile(src, dst) + except IOError: + logger.log_error( + self, + "Error: Couldn't copy Klipper config file to {}".format(dst) + ) + return False + else: + logger.log_debug(self, "CfgBackup " + dst + " written") + return True diff --git a/octoprint_klipper/logger.py b/octoprint_klipper/logger.py new file mode 100644 index 0000000..87b723f --- /dev/null +++ b/octoprint_klipper/logger.py @@ -0,0 +1,33 @@ +from . import util + +def log_info(self, message): + self._octoklipper_logger.info(message) + util.send_message( + self, + type = "log", + subtype = "info", + title = message, + payload = message + ) + +def log_debug(self, message): + self._octoklipper_logger.debug(message) + self._logger.info(message) + util.send_message( + self, + type = "console", + subtype = "debug", + title = message, + payload = message + ) + +def log_error(self, error): + self._octoklipper_logger.error(error) + self._logger.error(error) + util.send_message( + self, + type = "log", + subtype = "error", + title = error, + payload = error + ) diff --git a/octoprint_klipper/modules/KlipperLogAnalyzer.py b/octoprint_klipper/modules/KlipperLogAnalyzer.py index 3f1881d..5bce9ff 100644 --- a/octoprint_klipper/modules/KlipperLogAnalyzer.py +++ b/octoprint_klipper/modules/KlipperLogAnalyzer.py @@ -15,6 +15,7 @@ import flask import optparse, datetime +from .. import logger class KlipperLogAnalyzer(): MAXBANDWIDTH=25000. @@ -81,6 +82,7 @@ class KlipperLogAnalyzer(): out.append(keyparts) f.close() except IOError: + logger.log_error(self, "Couldn't open log file: {}".format(logname)) print("Couldn't open log file") return out diff --git a/octoprint_klipper/static/clientjs/klipper.js b/octoprint_klipper/static/clientjs/klipper.js new file mode 100644 index 0000000..26c01b6 --- /dev/null +++ b/octoprint_klipper/static/clientjs/klipper.js @@ -0,0 +1,76 @@ +(function (global, factory) { + if (typeof define === "function" && define.amd) { + define(["OctoPrintClient"], factory); + } else { + factory(global.OctoPrintClient); + } +})(this, function (OctoPrintClient) { + var OctoKlipperClient = function (base) { + this.base = base; + this.url = this.base.getBlueprintUrl("klipper"); + }; + + OctoKlipperClient.prototype.restartKlipper = function (opts) { + return this.base.post(this.url + "restart", opts); + }; + + OctoKlipperClient.prototype.getCfg = function (config, opts) { + return this.base.get(this.url + "config/" + config, opts); + }; + + OctoKlipperClient.prototype.getCfgBak = function (backup, opts) { + return this.base.get(this.url + "backup/" + backup, opts); + }; + + OctoKlipperClient.prototype.listCfg = function (opts) { + return this.base.get(this.url + "config/list", opts); + }; + + OctoKlipperClient.prototype.listCfgBak = function (opts) { + return this.base.get(this.url + "backup/list", opts); + }; + + OctoKlipperClient.prototype.checkCfg = function (content, opts) { + content = content || []; + + var data = { + DataToCheck: content, + }; + + return this.base.postJson(this.url + "config/check", data, opts); + }; + + OctoKlipperClient.prototype.saveCfg = function (content, filename, opts) { + content = content || []; + filename = filename || []; + + var data = { + DataToSave: content, + filename: filename, + }; + + return this.base.postJson(this.url + "config/save", data, opts); + }; + + OctoKlipperClient.prototype.deleteCfg = function (config, opts) { + return this.base.delete(this.url + "config/" + config, opts); + }; + + OctoKlipperClient.prototype.deleteBackup = function (backup, opts) { + return this.base.delete(this.url + "backup/" + backup, opts); + }; + + OctoKlipperClient.prototype.restoreBackup = function (backup, opts) { + return this.base.get(this.url + "backup/restore/" + backup, opts); + }; + + OctoKlipperClient.prototype.restoreBackupFromUpload = function (file, data) { + data = data || {}; + + var filename = data.filename || undefined; + return this.base.upload(this.url + "restore", file, filename, data); + }; + + OctoPrintClient.registerPluginComponent("klipper", OctoKlipperClient); + return OctoKlipperClient; +}); diff --git a/octoprint_klipper/static/css/klipper.css b/octoprint_klipper/static/css/klipper.css index 9ed79cd..5bd129c 100644 --- a/octoprint_klipper/static/css/klipper.css +++ b/octoprint_klipper/static/css/klipper.css @@ -1,238 +1,384 @@ .plugin-klipper-sidebar { - padding: 1px; - height: auto; - border: 1px solid #aaa; - width: 98%; - text-align: center; - word-break: break-all; - margin: auto; + padding: 1px; + height: auto; + border: 1px solid #aaa; + width: 98%; + text-align: center; + word-break: break-word; + margin: auto; } li#navbar_plugin_klipper { - cursor: pointer; - max-width:360px; - max-height:80px; - word-break: break-all; + cursor: pointer; + max-width: 360px; + max-height: 80px; + word-break: break-all; } .plugin-klipper-sidebar a { - padding: 2px 2px; - text-align: center; - text-decoration: none; - display: inline-block; - cursor: pointer; + padding: 2px 2px; + text-align: center; + text-decoration: none; + display: inline-block; + cursor: pointer; } -.plugin-klipper-sidebar a:hover, .plugin-klipper-sidebar a:active { - cursor: pointer; - } +.plugin-klipper-sidebar a:hover, +.plugin-klipper-sidebar a:active { + cursor: pointer; +} .plugin-klipper-log { - padding: 0px; - overflow-y: scroll; - height: 400px; - border: 1px solid #eee; - width: 100%; - word-break: break-all; + padding: 0px; + overflow-y: scroll; + height: 400px; + border: 1px solid #eee; + width: 100%; + word-break: break-all; } .plugin-klipper-log .log-item { - margin: 3px auto 0 auto; - border: 1px solid #ddd; - border-radius: 3px; - background-color: #efefef; - color: #333; + margin: 3px auto 0 auto; + border: 1px solid #ddd; + border-radius: 3px; + background-color: #efefef; + color: #333; } .plugin-klipper-log .error { - background-color: #eebabb; + background-color: #eebabb; } .plugin-klipper-log .log-item .ts { - display: inline-block; - width: 13%; - height: 100%; - vertical-align: top; - font-size: 0.8em; - padding: 0 0 0 5px; + display: inline-block; + width: 13%; + height: 100%; + vertical-align: top; + font-size: 0.8em; + padding: 0 0 0 5px; } .plugin-klipper-log .log-item .msg { - display: inline-block; - width: 84%; - height: 100%; + display: inline-block; + width: 84%; + height: 100%; } .clear-btn { - margin-top: 6px; - margin-bottom: 6px; + margin-top: 6px; + margin-bottom: 6px; } #level .controls { - padding: 1px; + padding: 1px; } ul#klipper-settings { - margin: 0; + margin: 0; } -#klipper-settings a{ - margin: 5px; +#klipper-settings a { + margin: 5px; } #tab_plugin_klipper_main .row-fluid { - display: flex; - flex: row wrap; - align-items: stretch; + display: flex; + flex-flow: row wrap; + align-items: stretch; } @media all and (max-width: 940px) { - #tab_plugin_klipper_main .row-fluid { - /* On small screens, we are no longer using row direction but column */ - flex-direction: column; - } - } + #tab_plugin_klipper_main .row-fluid { + /* On small screens, we are no longer using row direction but column */ + flex-direction: column; + } +} #tab_plugin_klipper_main #left-side { - flex: 3 1; - padding-right: 10px; + flex: 3 1; + padding-right: 10px; + padding-top: 5px; } #tab_plugin_klipper_main .span8 label { - float: left; + float: left; } #tab_plugin_klipper_main #right-side { - flex: 1 1; - max-width: 200px; - min-width: 100px; + flex: 1 1; + max-width: 200px; + min-width: 100px; +} + +.klipper-row-fluid { + display: flex; + flex-flow: row wrap; + align-items: stretch; +} + +.klipper-column-fluid { + display: flex; + flex-flow: column nowrap; + align-items: stretch; +} + +.klipper-fluid-item-1 { + flex: 1 auto; +} + +.klipper-fluid-item-2 { + flex: 2 auto; +} + +.klipper-fluid-item-3 { + flex: 3 auto; +} + +.gap { + justify-content: space-evenly; +} + +@media all and (max-width: 940px) { + .klipper-row-fluid { + /* On small screens, we are no longer using row direction but column */ + flex-direction: column; + } } #settings_plugin_klipper { - height: 100%; + height: 100%; + height: -webkit-fill-available; +} + +div#klipper_backups_dialog { + display: flex; + flex-flow: column; + min-height: 400px; +} + +div#klipper_backups_dialog div.modal-body { + min-height: 350px; + display: flex; + flex-flow: column; + flex-grow: 1; +} + +div#klipper_backups_dialog .editor-controls { + flex: 0 auto; + display: flex; + align-items: stretch; + flex-flow: row wrap; +} + +div#klipper_backups_dialog div.modal-body .textarea { + overflow: auto; +} + +div#klipper_backups_dialog div.modal-body textarea { + margin-bottom: 0px !important; + padding-left: 0px !important; + padding-right: 0px !important; + box-sizing: border-box; + resize: none; + width: 100%; +} + +/* UIcustomizer fix */ +body.UICResponsiveMode #settings_dialog_content { + height: calc(100% - 60px); + margin-right: -18px; + margin-top: 50px; + width: calc(100% - 15px); } div#settings_plugin_klipper form { - margin: 0px; - height: 100%; + margin: 0px; + height: 100%; } -div#settings_plugin_klipper form .tab-content { - height: calc(100% - 40px); - overflow: auto; +div#settings_plugin_klipper div.tab-content { + height: calc(100% - 76px); + overflow: auto; +} + +div#settings_plugin_klipper div.tab-content .border{ + border-bottom: 1px solid; +} + +div#settings_plugin_klipper div.tab-footer { + height: 20px; + width: 100%; + top: 10px; + position: relative; + border-top: 1px solid #eee; + padding-top: 3px; } div#settings_plugin_klipper div.tab-content div#conf.tab-pane { - height: 100%; + height: 100%; + min-height: 200px; + width: 100%; } div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group { - height: 100%; - margin: 0; - display: flex; - flex-direction: column; + height: 100%; + margin: 0; + display: flex; + flex-direction: column; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.editor-controls{ - margin-bottom: 0px; - height: 26px; +.klipper-settings-tab { + height: 100%; + width: 100%; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor { - height: 95%; - height: calc(100% - 28px); - width: 99%; - width: calc(100% - 4px); - padding-top: 2px; - flex: 1 1; - overflow: auto; +#settings_plugin_klipper .m-0 { + margin: 0; } -div#settings_plugin_klipper div.tab-content div#conf.tab-pane div.control-group div.conf-editor div#plugin-klipper-config { - font-family: monospace; - overflow: auto; - height: 100%; - height: -webkit-fill-available; +#settings_plugin_klipper .scroll-y { + overflow-y: scroll; } -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#conf.tab-pane.active button.btn.btn-small { - width: 30%; - display: inline-block; - margin: 0px 2px 2px 2px; +#settings_plugin_klipper table { + table-layout: unset !important; +} + +#settings_plugin_klipper .table-fixed thead th { + position: sticky; + top: 0; + background-color: rgba(12, 5, 5, 0.85); + color: rgb(200, 200, 200); +} + +#settings_plugin_klipper .pagination-mini { + margin: 5; +} + +@media (max-width: 979px) { + + /* div#klipper_editor.modal { + height: unset !important; + } */ + + div#klipper_editor .modal-footer .btn { + margin-bottom: 1px; + margin-left: 5px; + } + +} + + +.octoprint-container.UICMainCont.container-fluid { + margin-top:0px !important; +} + +div#klipper_editor .modal-header div button.btn + button.btn { + margin-right: 12px; +} + +div#klipper_editor .modal-body { + overflow: auto; +} + +.klipper-btn-group { + display: inline-block; +} + +div#klipper_editor .modal-footer input[type="text"] { + margin-bottom: 0px !important; +} + + +div#klipper_editor .modal-footer .editor-controls { + flex: 0 auto; + display: flex; + align-items: center; +} + + +div#klipper_editor div.conf-editor { + width: 99%; + width: calc(100% - 4px); + margin-top: 5px; + overflow: auto; +} + +div#klipper_editor div.conf-editor div#plugin-klipper-config { + font-family: monospace; + overflow: auto; + width: 100%; + margin: auto; } /*checkboxes*/ div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active input.inline-checkbox { - vertical-align: -0.2em; + vertical-align: -0.2em; } -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active label.inline { - display: inline; +.klipper-inline { + display: inline; } div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div.tab-pane.active div.controls input.controls-checkbox { - margin-top: 8px; + margin-top: 8px; } /*macros*/ -div#settings_plugin_klipper.tab-pane.active form.form-horizontal div.tab-content div#macros.tab-pane.active div div#item.control-group label.control-label { - width: 80px; +div#settings_plugin_klipper div#macros label.control-label { + width: 80px; } #macros #item.control-group { - margin-bottom: 2px; - border: 2px solid #ccc; - border-radius: 3px; - background-color: #eeeeee; - color: #333; - padding-bottom: 2px; - padding-top: 2px; + margin-bottom: 2px; + border: 2px solid #ccc; + border-radius: 3px; + background-color: #eeeeee; + color: #333; + padding-bottom: 2px; + padding-top: 2px; } -#klipper_graph_dialog { - width: 1050px; -} - -#klipper_graph_dialog .full-sized-box{ - width: 1000px; - margin: 0 auto; +#klipper_graph_dialog .full-sized-box { + width: 100%; + margin: 0 auto; } #klipper_graph_dialog form { - margin: 0; + margin: 0; } #klipper_graph_dialog select { - width: auto; + width: auto; } #klipper_graph_dialog .graph-footer { - bottom:0; + bottom: 0; } #klipper_graph_dialog input { - display: inline-block; + display: inline-block; } #klipper_graph_dialog .status-label { - display: block; - position: absolute; - margin: 5px 0 0 10px; + display: block; + position: absolute; + margin: 5px 0 0 10px; } #klipper_graph_dialog .fill-checkbox { - display: block; - position: absolute; - top: 0%; - left: 50%; + display: block; + position: absolute; + top: 0%; + left: 50%; } #klipper_graph_dialog .help-inline { - display: block; - position: absolute; - top: 0px; + display: block; + position: absolute; + top: 0px; } #klipper_graph_canvas { - margin-top: 15px; + margin-top: 15px; } diff --git a/octoprint_klipper/static/js/klipper.js b/octoprint_klipper/static/js/klipper.js index 7fdfeb5..9c41b64 100644 --- a/octoprint_klipper/static/js/klipper.js +++ b/octoprint_klipper/static/js/klipper.js @@ -14,251 +14,337 @@ // along with this program. If not, see . $(function () { - function KlipperViewModel(parameters) { - var self = this; + function KlipperViewModel(parameters) { + var self = this; - self.header = OctoPrint.getRequestHeaders({ - "content-type": "application/json", - "cache-control": "no-cache" - }); + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); - self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.Url = OctoPrint.getBlueprintUrl("klipper"); - self.settings = parameters[0]; - self.loginState = parameters[1]; - self.connectionState = parameters[2]; - self.levelingViewModel = parameters[3]; - self.paramMacroViewModel = parameters[4]; - self.access = parameters[5]; + self.settings = parameters[0]; + self.loginState = parameters[1]; + self.connectionState = parameters[2]; + self.levelingViewModel = parameters[3]; + self.paramMacroViewModel = parameters[4]; + self.access = parameters[5]; - self.shortStatus_navbar = ko.observable(); - self.shortStatus_sidebar = ko.observable(); - self.logMessages = ko.observableArray(); + self.shortStatus_navbar = ko.observable(); + self.shortStatus_navbar_hover = ko.observable(); + self.shortStatus_sidebar = ko.observable(); + self.logMessages = ko.observableArray(); - self.showPopUp = function(popupType, popupTitle, message){ - var title = popupType.toUpperCase() + ": " + popupTitle; - new PNotify({ - title: title, - text: message, - type: popupType, - hide: false - }); - }; + self.showPopUp = function (popupType, popupTitle, message) { + var title = "OctoKlipper:
" + popupTitle + "
"; + var hide = false; + if (popupType == "success") { + hide = true + } + new PNotify({ + title: title, + text: message, + type: popupType, + hide: hide, + icon: true + }); + }; - self.onSettingsShown = function () { - self.reloadConfig(); - } - - self.showLevelingDialog = function () { - var dialog = $("#klipper_leveling_dialog"); - dialog.modal({ - show: "true", - backdrop: "static", - keyboard: false - }); - self.levelingViewModel.initView(); - }; - - self.showPidTuningDialog = function () { - var dialog = $("#klipper_pid_tuning_dialog"); - dialog.modal({ - show: "true", - backdrop: "static", - keyboard: false - }); - }; - - self.showOffsetDialog = function () { - var dialog = $("#klipper_offset_dialog"); - dialog.modal({ - show: "true", - backdrop: "static" - }); - }; - - self.showGraphDialog = function () { - var dialog = $("#klipper_graph_dialog"); - dialog.modal({ - show: "true", - minHeight: "500px", - maxHeight: "600px" - }); - }; - - self.executeMacro = function (macro) { - var paramObjRegex = /{(.*?)}/g; - - if (!self.hasRight("MACRO")) return; - - if (macro.macro().match(paramObjRegex) == null) { - OctoPrint.control.sendGcode( - // Use .split to create an array of strings which is sent to - // OctoPrint.control.sendGcode instead of a single string. - macro.macro().split(/\r\n|\r|\n/) - ); - } else { - self.paramMacroViewModel.process(macro); - - var dialog = $("#klipper_macro_dialog"); - dialog.modal({ - show: "true", - backdrop: "static" - }); - } - }; - - self.navbarClicked = function () { - $("#tab_plugin_klipper_main_link").find("a").click(); - }; - - self.onGetStatus = function () { - OctoPrint.control.sendGcode("Status"); - }; - - self.onRestartFirmware = function () { - OctoPrint.control.sendGcode("FIRMWARE_RESTART"); - }; - - self.onRestartHost = function () { - OctoPrint.control.sendGcode("RESTART"); - }; - - self.onAfterBinding = function () { - self.connectionState.selectedPort( - self.settings.settings.plugins.klipper.connection.port() - ); - }; - - self.onDataUpdaterPluginMessage = function(plugin, data) { - if(plugin == "klipper") { - switch(data.type) { - case "PopUp": - self.showPopUp(data.subtype, data.title, data.payload); - break; - case "reload": - break; - case "console": - self.consoleMessage(data.subtype, data.payload); - break; - case "status": - if (data.payload.length > 36) { - var shortText = data.payload.substring(0, 31) + " [..]" - self.shortStatus_navbar(shortText); - } else { - self.shortStatus_navbar(data.payload); - } - self.shortStatus_sidebar(data.payload); - break; - default: - self.logMessage(data.time, data.subtype, data.payload); - self.consoleMessage(data.subtype, data.payload); - } - - //if ("warningPopUp" == data.type){ - // self.showPopUp(data.subtype, data.title, data.payload); - // return; - //} else if ("errorPopUp" == data.type){ - // self.showPopUp(data.subtype, data.title, data.payload); - // return; - //} else if ("reload" == data.type){ - // return; - //} else if ("console" == data.type) { - // self.consoleMessage(data.subtype, data.payload); - //} else if (data.type == "status") { - // self.shortStatus(data.payload); - //} else { - // self.logMessage(data.time, data.subtype, data.payload); - //} - } - }; - - self.logMessage = function (timestamp, type="info", message) { - if (!timestamp) { - var today = new Date(); - var timestamp = today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); - } - self.logMessages.push({ - time: timestamp, - type: type, - msg: message.replace(/\n/gi, "
") - }); - }; - - self.consoleMessage = function (type, message) { - if (self.settings.settings.plugins.klipper.configuration.debug_logging() === true) { - if (type == "info"){ - console.info("OctoKlipper : " + message); - } else if (type == "debug"){ - console.debug("OctoKlipper : " + message); - } else { - console.error("OctoKlipper : " + message); - } - } - return - }; - - self.reloadConfig = function() { - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "reloadConfig"}) - } - - $.ajax(settings).done(function (response) { - self.consoleMessage( - "debug", - "Reloaded config file from Backend"); - }); - } - - self.onClearLog = function () { - self.logMessages.removeAll(); - }; - - self.isActive = function () { - return self.connectionState.isOperational(); - }; - - self.hasRight = function (right_role, type) { - var arg = eval("self.access.permissions.PLUGIN_KLIPPER_" + right_role); - - if (type == "Ko") { - return self.loginState.hasPermissionKo(arg); - } - return self.loginState.hasPermission(arg); - }; - - // OctoKlipper settings link - self.openOctoKlipperSettings = function (profile_type) { - if (!self.hasRight("CONFIG")) return; - - $("a#navbar_show_settings").click(); - $("li#settings_plugin_klipper_link a").click(); - if (profile_type) { - var query = "#klipper-settings a[data-profile-type='" + profile_type + "']"; - $(query).click(); - } - }; + self.showEditorDialog = function () { + if (!self.hasRight("CONFIG")) return; + var editorDialog = $("#klipper_editor"); + editorDialog.modal({ + show: "true", + width: "90%", + backdrop: "static", + }); } - OCTOPRINT_VIEWMODELS.push({ - construct: KlipperViewModel, - dependencies: [ - "settingsViewModel", - "loginStateViewModel", - "connectionViewModel", - "klipperLevelingViewModel", - "klipperMacroDialogViewModel", - "accessViewModel" - ], - elements: [ - "#tab_plugin_klipper_main", - "#sidebar_plugin_klipper", - "#navbar_plugin_klipper" - ] - }); + self.showLevelingDialog = function () { + var dialog = $("#klipper_leveling_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + keyboard: false, + }); + self.levelingViewModel.initView(); + }; + + self.showPidTuningDialog = function () { + var dialog = $("#klipper_pid_tuning_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + keyboard: false, + }); + }; + + self.showOffsetDialog = function () { + var dialog = $("#klipper_offset_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + }); + }; + + self.showGraphDialog = function () { + var dialog = $("#klipper_graph_dialog"); + dialog.modal({ + show: "true", + width: "90%", + minHeight: "500px", + maxHeight: "600px", + }); + }; + + self.executeMacro = function (macro) { + var paramObjRegex = /{(.*?)}/g; + + if (!self.hasRight("MACRO")) return; + + if (macro.macro().match(paramObjRegex) == null) { + OctoPrint.control.sendGcode( + // Use .split to create an array of strings which is sent to + // OctoPrint.control.sendGcode instead of a single string. + macro.macro().split(/\r\n|\r|\n/) + ); + } else { + self.paramMacroViewModel.process(macro); + + var dialog = $("#klipper_macro_dialog"); + dialog.modal({ + show: "true", + backdrop: "static", + }); + } + }; + + self.navbarClicked = function () { + $("#tab_plugin_klipper_main_link").find("a").click(); + }; + + self.onGetStatus = function () { + OctoPrint.control.sendGcode("Status"); + }; + + self.onRestartFirmware = function () { + OctoPrint.control.sendGcode("FIRMWARE_RESTART"); + }; + + self.onRestartHost = function () { + OctoPrint.control.sendGcode("RESTART"); + }; + + self.onAfterBinding = function () { + self.connectionState.selectedPort( + self.settings.settings.plugins.klipper.connection.port() + ); + }; + + self.onDataUpdaterPluginMessage = function (plugin, data) { + + if (plugin == "klipper") { + switch (data.type) { + case "PopUp": + self.showPopUp(data.subtype, data.title, data.payload); + break; + case "reload": + break; + case "console": + self.consoleMessage(data.subtype, data.payload); + break; + case "status": + self.shortStatus(data.payload, data.subtype); + break; + default: + self.logMessage(data.time, data.subtype, data.payload); + self.shortStatus(data.payload, data.subtype) + self.consoleMessage(data.subtype, data.payload); + } + } + }; + + + self.shortStatus = function(msg, type) { + + var baseText = gettext("Go to OctoKlipper Tab"); + if (msg.length > 36) { + var shortText = msg.substring(0, 31) + " [..]"; + self.shortStatus_navbar(shortText); + self.shortStatus_navbar_hover(msg); + } else { + self.shortStatus_navbar(msg); + self.shortStatus_navbar_hover(baseText); + } + message = msg.replace(/\n/gi, "
"); + self.shortStatus_sidebar(message); + }; + + + self.logMessage = function (timestamp, type = "info", message) { + + if (!timestamp) { + var today = new Date(); + var timestamp = + today.getHours() + ":" + today.getMinutes() + ":" + today.getSeconds(); + } + + if (type == "error") { + self.showPopUp(type, "Error:", message); + } + + self.logMessages.push({ + time: timestamp, + type: type, + msg: message.replace(/\n/gi, "
"), + }); + }; + + self.consoleMessage = function (type, message) { + if ( + self.settings.settings.plugins.klipper.configuration.debug_logging() === true + ) { + if (type == "info") { + console.info("OctoKlipper : " + message); + } else if (type == "debug") { + console.debug("OctoKlipper : " + message); + } else { + console.error("OctoKlipper : " + message); + } + } + return; + }; + + self.onClearLog = function () { + self.logMessages.removeAll(); + }; + + self.isActive = function () { + return self.connectionState.isOperational(); + }; + + self.hasRight = function (right_role) { + //if (self.loginState.isAdmin) return true; + if (right_role == "CONFIG") { + return self.loginState.hasPermission( + self.access.permissions.PLUGIN_KLIPPER_CONFIG + ); + } else if (right_role == "MACRO") { + return self.loginState.hasPermission( + self.access.permissions.PLUGIN_KLIPPER_MACRO + ); + } + }; + + self.hasRightKo = function (right_role) { + //if (self.loginState.isAdmin) return true; + if (right_role == "CONFIG") { + return self.loginState.hasPermissionKo( + self.access.permissions.PLUGIN_KLIPPER_CONFIG + ); + } else if (right_role == "MACRO") { + return self.loginState.hasPermissionKo( + self.access.permissions.PLUGIN_KLIPPER_MACRO + ); + } + }; + + self.saveOption = function(dir, option, value) { + if (! (_.includes(["fontsize", "confirm_reload", "parse_check"], option)) ) { + return; + } + + if (option && dir) { + let data = { + plugins: { + klipper:{ + [dir]: { + [option]: value + } + } + } + }; + OctoPrint.settings + .save(data); + } else if (option) { + let data = { + plugins: { + klipper:{ + [option]: value + } + } + }; + OctoPrint.settings + .save(data); + } + } + + self.requestRestart = function () { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + var request = function (index) { + OctoPrint.plugins.klipper.restartKlipper().done(function (response) { + self.consoleMessage("debug", "restartingKlipper"); + self.showPopUp("success", gettext("Restarted Klipper"), "command: " + response.command); + }); + if (index == 1) { + self.saveOption("configuration", "confirm_reload", false); + } + }; + + var html = "

" + + gettext("All ongoing Prints will be stopped!") + + "

"; + + if (self.settings.settings.plugins.klipper.configuration.confirm_reload() == true) { + showConfirmationDialog({ + title: gettext("Restart Klipper?"), + html: html, + proceed: [gettext("Restart"), gettext("Restart and don't ask this again.")], + onproceed: function (idx) { + if (idx > -1) { + request(idx); + } + }, + }); + } else { + request(0); + } + }; + + // OctoKlipper settings link + self.openOctoKlipperSettings = function (profile_type) { + if (!self.hasRight("CONFIG")) return; + + $("a#navbar_show_settings").click(); + $("li#settings_plugin_klipper_link a").click(); + if (profile_type) { + var query = "#klipper-settings a[data-profile-type='" + profile_type + "']"; + $(query).click(); + } + }; + + self.sleep = function (ms) { + return new Promise(resolve => setTimeout(resolve, ms)); + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperViewModel, + dependencies: [ + "settingsViewModel", + "loginStateViewModel", + "connectionViewModel", + "klipperLevelingViewModel", + "klipperMacroDialogViewModel", + "accessViewModel", + ], + elements: [ + "#tab_plugin_klipper_main", + "#sidebar_plugin_klipper", + "#navbar_plugin_klipper", + ], + }); }); diff --git a/octoprint_klipper/static/js/klipper_backup.js b/octoprint_klipper/static/js/klipper_backup.js new file mode 100644 index 0000000..1e35c7a --- /dev/null +++ b/octoprint_klipper/static/js/klipper_backup.js @@ -0,0 +1,297 @@ +// + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +$(function () { + function KlipperBackupViewModel(parameters) { + var self = this; + + self.loginState = parameters[0]; + self.klipperViewModel = parameters[1]; + self.access = parameters[2]; + + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); + + self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.Url = OctoPrint.getBlueprintUrl("klipper"); + + self.markedForFileRestore = ko.observableArray([]); + + self.CfgContent = ko.observable(); + + //uploads + self.maxUploadSize = ko.observable(0); + self.backupUploadData = undefined; + self.backupUploadName = ko.observable(); + self.isAboveUploadSize = function (data) { + return data.size > self.maxUploadSize(); + }; + + self.onStartupComplete = function () { + $('#klipper_backups_dialog').css('display', 'none'); + if (self.loginState.loggedIn()) { + self.listBakFiles(); + } + }; + + // initialize list helper + self.backups = new ItemListHelper( + "klipperBakFiles", + { + name: function (a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + date: function (a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + size: function (a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + }, + }, + {}, + "name", + [], + [], + 5 + ); + + self.listBakFiles = function () { + self.klipperViewModel.consoleMessage("debug", "listBakFiles"); + + OctoPrint.plugins.klipper.listCfgBak() + .done(function (response) { + self.backups.updateItems(response.files); + self.backups.resetPage(); + }); + }; + + self.showCfg = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + OctoPrint.plugins.klipper.getCfgBak(backup).done(function (response) { + $('#klipper_backups_dialog textarea').attr('rows', response.response.config.split(/\r\n|\r|\n/).length); + self.CfgContent(response.response.config); + }); + }; + + self.removeCfg = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + var perform = function () { + OctoPrint.plugins.klipper + .deleteBackup(backup) + .done(function () { + self.listBakFiles(); + }) + .fail(function (response) { + var html = "

" + _.sprintf(gettext("Failed to remove config %(name)s.

Please consult octoprint.log for details.

"), { name: _.escape(backup) }); + html += pnotifyAdditionalInfo('
' + _.escape(response.responseText) + "
"); + new PNotify({ + title: gettext("Could not remove config"), + text: html, + type: "error", + hide: false, + }); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext('You are about to delete backed config file "%(name)s".'), { + name: _.escape(backup), + }), + perform + ); + }; + + self.restoreBak = function (backup) { + if (!self.loginState.hasPermission(self.access.permissions.PLUGIN_KLIPPER_CONFIG)) return; + + var restore = function () { + OctoPrint.plugins.klipper.restoreBackup(backup).done(function (response) { + self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + backup + " / " + response.restored); + }); + }; + + var html = "

" + gettext("This will overwrite any file with the same name on the configpath.") + "

" + "

" + backup + "

"; + + showConfirmationDialog({ + title: gettext("Are you sure you want to restore now?"), + html: html, + proceed: gettext("Proceed"), + onproceed: restore, + }); + }; + + self.markFilesOnPage = function () { + self.markedForFileRestore(_.uniq(self.markedForFileRestore().concat(_.map(self.backups.paginatedItems(), "file")))); + }; + + self.markAllFiles = function () { + self.markedForFileRestore(_.map(self.backups.allItems, "file")); + }; + + self.clearMarkedFiles = function () { + self.markedForFileRestore.removeAll(); + }; + + self.restoreMarkedFiles = function () { + var perform = function () { + self._bulkRestore(self.markedForFileRestore()).done(function () { + self.markedForFileRestore.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to restore %(count)d backed config files."), { + count: self.markedForFileRestore().length, + }), + perform + ); + }; + + self.removeMarkedFiles = function () { + var perform = function () { + self._bulkRemove(self.markedForFileRestore()).done(function () { + self.markedForFileRestore.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to delete %(count)d backed config files."), { + count: self.markedForFileRestore().length, + }), + perform + ); + }; + + self._bulkRestore = function (files) { + var title, message, handler; + + title = gettext("Restoring klipper config files"); + self.klipperViewModel.consoleMessage("debug", title); + message = _.sprintf(gettext("Restoring %(count)d backed config files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .restoreBackup(filename) + .done(function (response) { + deferred.notify( + _.sprintf(gettext("Restored %(filename)s..."), { + filename: _.escape(filename), + }), + true + ); + self.klipperViewModel.consoleMessage("debug", "restoreCfg: " + filename + " / " + response); + self.markedForFileRestore.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Restoring of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + + var promise = deferred.promise(); + + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + }); + + return promise; + }; + + self._bulkRemove = function (files) { + var title, message, handler; + + title = gettext("Deleting backup files"); + message = _.sprintf(gettext("Deleting %(count)d backed files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .deleteBackup(filename) + .done(function () { + deferred.notify(_.sprintf(gettext("Deleted %(filename)s..."), { filename: _.escape(filename) }), true); + self.markedForFileRestore.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + var promise = deferred.promise(); + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + self.listBakFiles(); + }); + + return promise; + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperBackupViewModel, + dependencies: ["loginStateViewModel", "klipperViewModel", "accessViewModel"], + elements: ["#klipper_backups_dialog"], + }); +}); diff --git a/octoprint_klipper/static/js/klipper_editor.js b/octoprint_klipper/static/js/klipper_editor.js new file mode 100644 index 0000000..8e34c34 --- /dev/null +++ b/octoprint_klipper/static/js/klipper_editor.js @@ -0,0 +1,398 @@ +// + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. + +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +$(function () { + function KlipperEditorViewModel(parameters) { + var self = this; + var editor = null; + var editordialog = $("#klipper_editor"); + + self.settings = parameters[0]; + self.klipperViewModel = parameters[1]; + + self.CfgFilename = ko.observable(""); + self.CfgContent = ko.observable(""); + self.loadedConfig = ""; + self.CfgChangedExtern = false; + + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); + + $(window).on('resize', function() { + self.klipperViewModel.sleep(200).then( + function () { + self.setEditorDivSize(); + } + ); + }); + + self.onShown = function () { + self.checkExternChange(); + editor.focus(); + self.setEditorDivSize(); + }; + + self.close_selection = function (index) { + switch (index) { + case 0: + editordialog.modal('hide'); + break; + case 1: + self.editorFocusDelay(1000); + break; + case 2: + self.saveCfg({closing: true}); + break; + } + }; + + self.closeEditor = function () { + self.CfgContent(editor.getValue()); + if (self.loadedConfig != self.CfgContent()) { + + var opts = { + title: gettext("Closing without saving"), + message: gettext("Your file seems to have changed.") + + "
" + + gettext("Do you really want to close it?"), + selections: [gettext("Close"), gettext("Do not close"), gettext("Save & Close")], + maycancel: false, + onselect: function (index) { + if (index > -1) { + self.close_selection(index); + } + }, + }; + + showSelectionDialog(opts); + } else { + editordialog.modal('hide'); + } + }; + + self.addStyleAttribute = function ($element, styleAttribute) { + $element.attr('style', styleAttribute); + }; + + self.setEditorDivSize = function () { + var klipper_modal_body= $('#klipper_editor .modal-body'); + var klipper_config= $('#plugin-klipper-config'); + + var height = $(window).height() - $('#klipper_editor .modal-header').outerHeight() - $('#klipper_editor .modal-footer').outerHeight() - 118; + self.addStyleAttribute(klipper_modal_body, 'height: ' + height + 'px !important;'); + klipper_config.css('height', height); + if (editor) { + editor.resize(); + } + }; + + //initialize the modal window and return done when finished + self.process = function (config) { + return new Promise(function (resolve) { + self.loadedConfig = config.content; + self.CfgFilename(config.file); + self.CfgContent(config.content); + + if (editor) { + editor.session.setValue(self.CfgContent()); + self.CfgChangedExtern = false; + editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); + editor.clearSelection(); + self.klipperViewModel.sleep(500).then( + function() { + self.setEditorDivSize(); + resolve("done"); + } + ); + } + }); + }; + + self.onDataUpdaterPluginMessage = function (plugin, data) { + //receive from backend after a SAVE_CONFIG + if (plugin == "klipper" && data.type == "reload" && data.subtype == "config") { + self.klipperViewModel.consoleMessage("debug", "onDataUpdaterPluginMessage klipper reload baseconfig"); + self.ConfigChangedAfterSave_Config(); + } + }; + + //set externally changed config flag if the current file is the base config + self.ConfigChangedAfterSave_Config = function () { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + if (self.CfgFilename() == self.settings.settings.plugins.klipper.configuration.baseconfig()) { + self.CfgChangedExtern = true; + self.checkExternChange(); + } + }; + + //check if the config was externally changed and ask for a reload + self.checkExternChange = function() { + var baseconfig = self.settings.settings.plugins.klipper.configuration.baseconfig(); + if (self.CfgChangedExtern && self.CfgFilename() == baseconfig) { + if (editordialog.is(":visible")) { + + var perform = function () { + self.reloadFromFile(); + } + + var html = "

" + gettext("Reload Configfile after SAVE_CONFIG?") + "

"; + + showConfirmationDialog({ + title: gettext("Externally changed config") + " " + baseconfig, + html: html, + proceed: gettext("Proceed"), + onproceed: perform, + }); + } + } + }; + + self.askSaveFaulty = function () { + return new Promise(function (resolve) { + var html = "
" + + gettext("Your configuration seems to be faulty.") + + "
"; + + showConfirmationDialog({ + title: gettext("Save faulty Configuration?"), + html: html, + cancel: gettext("Do not save!"), + proceed: [gettext("Save anyway!"), gettext("Save anyway and don't ask this again.")], + onproceed: function (idx) { + if (idx == 0) { + resolve(true); + } else { + self.klipperViewModel.saveOption("configuration", "parse_check", false); + resolve(true); + } + }, + oncancel: function () { + resolve(false); + } + }); + }); + }; + + self.checkSyntax = function () { + return new Promise((resolve, reject) => { + if (editor.session) { + self.klipperViewModel.consoleMessage("debug", "checkSyntax started"); + + OctoPrint.plugins.klipper.checkCfg(editor.session.getValue()) + .done(function (response) { + if (response.is_syntax_ok == true) { + self.klipperViewModel.showPopUp("success", gettext("SyntaxCheck"), gettext("SyntaxCheck OK")); + self.editorFocusDelay(1000); + resolve(true); + } else { + self.editorFocusDelay(1000); + resolve(false); + } + }) + .fail(function () { + reject(false); + }); + } else { reject(false); } + }); + }; + + self.saveCfg = function (options) { + var options = options || {}; + var closing = options.closing || false; + + if (self.CfgFilename() != "") { + if (editor.session) { + if (self.settings.settings.plugins.klipper.configuration.parse_check() == true) { + + // check Syntax and wait for response + self.checkSyntax().then((syntaxOK) => { + if (syntaxOK === false) { + + // Ask if we should save a faulty config anyway + self.askSaveFaulty().then((areWeSaving) => { + if (areWeSaving === false) { + // Not saving + showMessageDialog( + gettext('Faulty config not saved!'), + { + title: gettext("Save Config"), + onclose: function () { self.editorFocusDelay(1000); } + } + ); + } else { + // Save anyway + self.saveRequest(closing); + } + }); + } else { + // Syntax is ok + self.saveRequest(closing); + } + }); + } else { + self.saveRequest(closing); + } + } + } else { + showMessageDialog( + gettext("No filename set"), + { + title: gettext("Save Config") + } + ); + } + }; + + self.minusFontsize = function () { + self.settings.settings.plugins.klipper.configuration.fontsize( + self.settings.settings.plugins.klipper.configuration.fontsize() - 1 + ); + + if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) { + self.settings.settings.plugins.klipper.configuration.fontsize(9); + } + + var fontsize = self.settings.settings.plugins.klipper.configuration.fontsize(); + if (editor) { + editor.setFontSize(fontsize); + editor.resize(); + } + + self.klipperViewModel.saveOption("configuration", "fontsize", fontsize); + }; + + self.plusFontsize = function () { + self.settings.settings.plugins.klipper.configuration.fontsize( + self.settings.settings.plugins.klipper.configuration.fontsize() + 1 + ); + + if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) { + self.settings.settings.plugins.klipper.configuration.fontsize(20); + } + + var fontsize = self.settings.settings.plugins.klipper.configuration.fontsize(); + if (editor) { + editor.setFontSize(fontsize); + editor.resize(); + } + self.klipperViewModel.saveOption("configuration", "fontsize", fontsize); + }; + + self.reloadFromFile = function () { + if (self.CfgFilename() != "") { + OctoPrint.plugins.klipper.getCfg(self.CfgFilename()) + .done(function (response) { + self.klipperViewModel.consoleMessage("debug", "reloadFromFile done"); + if (response.response.text != "") { + showMessageDialog( + response.response.text, + { + title: gettext("Reload File") + } + ); + } else { + self.klipperViewModel.showPopUp("success", gettext("Reload Config"), gettext("File reloaded.")); + self.CfgChangedExtern = false; + if (editor) { + editor.session.setValue(response.response.config); + self.loadedConfig = response.response.config; + editor.clearSelection(); + editor.focus(); + } + } + }) + .fail(function (response) { + showMessageDialog( + response, + { + title: gettext("Reload File") + } + ); + }); + } else { + showMessageDialog( + gettext("No filename set"), + { + title: gettext("Reload File") + } + ); + } + }; + + self.onStartup = function () { + ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/"); + editor = ace.edit("plugin-klipper-config"); + editor.setTheme("ace/theme/monokai"); + editor.session.setMode("ace/mode/klipper_config"); + editor.clearSelection(); + + editor.setOptions({ + hScrollBarAlwaysVisible: false, + vScrollBarAlwaysVisible: false, + autoScrollEditorIntoView: true, + showPrintMargin: false, + //maxLines: "Infinity" + }); + + editor.session.on('change', function (delta) { + self.CfgContent(editor.getValue()); + editor.resize(); + }); + }; + + self.editorFocusDelay = function (delay) { + self.klipperViewModel.sleep(delay).then( + function () { + editor.focus(); + } + ); + }; + + self.saveRequest = function (closing) { + self.klipperViewModel.consoleMessage("debug", "SaveCfg start"); + + OctoPrint.plugins.klipper.saveCfg(editor.session.getValue(), self.CfgFilename()) + .done(function (response) { + if (response.saved === true) { + self.klipperViewModel.showPopUp("success", gettext("Save Config"), gettext("File saved.")); + self.loadedConfig = editor.session.getValue(); //set loaded config to current for resetting dirtyEditor + if (closing) { + editordialog.modal('hide'); + } + if (self.settings.settings.plugins.klipper.configuration.restart_onsave() == true) { + self.klipperViewModel.requestRestart(); + } + } else { + showMessageDialog( + gettext('File not saved!'), + { + title: gettext("Save Config"), + onclose: function () { self.editorFocusDelay(1000); } + } + ); + } + }); + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperEditorViewModel, + dependencies: ["settingsViewModel", "klipperViewModel"], + elements: ["#klipper_editor"], + }); +}); diff --git a/octoprint_klipper/static/js/klipper_settings.js b/octoprint_klipper/static/js/klipper_settings.js index 74b9de1..29d2b06 100644 --- a/octoprint_klipper/static/js/klipper_settings.js +++ b/octoprint_klipper/static/js/klipper_settings.js @@ -13,220 +13,320 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -$(function() { - $('#klipper-settings a:first').tab('show'); - function KlipperSettingsViewModel(parameters) { - var self = this; - var obKlipperConfig = null; - var editor = null; +$(function () { + $("#klipper-settings a:first").tab("show"); + function KlipperSettingsViewModel(parameters) { + var self = this; - self.settings = parameters[0]; - self.klipperViewModel = parameters[1]; + self.settings = parameters[0]; + self.klipperViewModel = parameters[1]; + self.klipperEditorViewModel = parameters[2]; + self.klipperBackupViewModel = parameters[3]; + self.access = parameters[4]; - self.header = OctoPrint.getRequestHeaders({ - "content-type": "application/json", - "cache-control": "no-cache" - }); + self.header = OctoPrint.getRequestHeaders({ + "content-type": "application/json", + "cache-control": "no-cache", + }); - self.apiUrl = OctoPrint.getSimpleApiUrl("klipper"); + self.markedForFileRemove = ko.observableArray([]); + self.PathToConfigs = ko.observable(""); - self.onSettingsBeforeSave = function () { - if (editor.session && self.settings.settings.plugins.klipper.configuration.parse_check() === true) { - self.klipperViewModel.consoleMessage("debug", "onSettingsBeforeSave:") - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "checkConfig", - config: editor.session.getValue()}) - } + $(document).on('shown.bs.modal','#klipper_editor', function () { + self.klipperEditorViewModel.onShown(); + }); - $.ajax(settings).done(function (response) { - }); - } - } - - self.addMacro = function() { - self.settings.settings.plugins.klipper.macros.push({ - name: 'Macro', - macro: '', - sidebar: true, - tab: true - }); - } - - self.removeMacro = function(macro) { - self.settings.settings.plugins.klipper.macros.remove(macro); - } - - self.moveMacroUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro) - } - - self.moveMacroDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro) - } - - self.addProbePoint = function() { - self.settings.settings.plugins.klipper.probe.points.push( - { - name: 'point-#', - x:0, y:0, z:0 - } - ); - } - - self.removeProbePoint = function(point) { - self.settings.settings.plugins.klipper.probe.points.remove(point); - } - - self.moveProbePointUp = function(macro) { - self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro) - } - - self.moveProbePointDown = function(macro) { - self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro) - } - - self.moveItemDown = function(list, item) { - var i = list().indexOf(item); - if (i < list().length - 1) { - var rawList = list(); - list.splice(i, 2, rawList[i + 1], rawList[i]); - } - } - - self.moveItemUp = function(list, item) { - var i = list().indexOf(item); - if (i > 0) { - var rawList = list(); - list.splice(i-1, 2, rawList[i], rawList[i-1]); - } - } - - self.minusFontsize = function () { - self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() - 1); - if (self.settings.settings.plugins.klipper.configuration.fontsize() < 9) { - self.settings.settings.plugins.klipper.configuration.fontsize(9); - } - if (editor) { - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - } - } - - self.plusFontsize = function () { - self.settings.settings.plugins.klipper.configuration.fontsize(self.settings.settings.plugins.klipper.configuration.fontsize() + 1); - if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) { - self.settings.settings.plugins.klipper.configuration.fontsize(20); - } - if (editor) { - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - } - } - - self.loadLastSession = function () { - if (self.settings.settings.plugins.klipper.configuration.old_config() != "") { - self.klipperViewModel.consoleMessage("info","lastSession:" + self.settings.settings.plugins.klipper.configuration.old_config()) - if (editor.session) { - editor.session.setValue(self.settings.settings.plugins.klipper.configuration.old_config()); - editor.clearSelection(); - } - } - } - - self.reloadFromFile = function () { - if (editor.session) { - var settings = { - "crossDomain": true, - "url": self.apiUrl, - "method": "POST", - "headers": self.header, - "processData": false, - "dataType": "json", - "data": JSON.stringify({command: "reloadConfig"}) - } - - $.ajax(settings).done(function (response) { - editor.session.setValue(response["data"]); - editor.clearSelection(); - }); - } - } - - self.configBound = function (config) { - config.withSilence = function() { - this.notifySubscribers = function() { - if (!this.isSilent) { - ko.subscribable.fn.notifySubscribers.apply(this, arguments); - } - } - - this.silentUpdate = function(newValue) { - this.isSilent = true; - this(newValue); - this.isSilent = false; - }; - - return this; - } - - obKlipperConfig = config.withSilence(); - if (editor) { - editor.setValue(obKlipperConfig()); - editor.setFontSize(self.settings.settings.plugins.klipper.configuration.fontsize()); - editor.resize(); - editor.clearSelection(); - } - return obKlipperConfig; - } - - ace.config.set("basePath", "plugin/klipper/static/js/lib/ace/"); - editor = ace.edit("plugin-klipper-config"); - editor.setTheme("ace/theme/monokai"); - editor.session.setMode("ace/mode/klipper_config"); - editor.setOptions({ - hScrollBarAlwaysVisible: false, - vScrollBarAlwaysVisible: false, - autoScrollEditorIntoView: true, - showPrintMargin: false, - maxLines: "Infinity" - }) - - editor.session.on('change', function(delta) { - if (obKlipperConfig) { - obKlipperConfig.silentUpdate(editor.getValue()); - editor.resize(); - } - }); - - // Uncomment this if not using maxLines: "Infinity"... - // setInterval(function(){ editor.resize(); }, 500); - self.onDataUpdaterPluginMessage = function(plugin, data) { - if(plugin == "klipper") { - if ("reload" == data.type){ - if ("config" == data.subtype){ - if (editor.session) { - editor.session.setValue(data.payload); - editor.clearSelection(); - } - } - return - } - } - } + self.checkFontsize = function () { + if (self.settings.settings.plugins.klipper.configuration.fontsize() > 20) { + self.settings.settings.plugins.klipper.configuration.fontsize(20) + } else if (self.settings.settings.plugins.klipper.configuration.fontsize()< 9) { + self.settings.settings.plugins.klipper.configuration.fontsize(9) + } } - OCTOPRINT_VIEWMODELS.push({ - construct: KlipperSettingsViewModel, - dependencies: [ - "settingsViewModel", - "klipperViewModel" - ], - elements: ["#settings_plugin_klipper"] - }); + // initialize list helper + self.configs = new ItemListHelper( + "klipperCfgFiles", + { + name: function (a, b) { + // sorts ascending + if (a["name"].toLocaleLowerCase() < b["name"].toLocaleLowerCase()) return -1; + if (a["name"].toLocaleLowerCase() > b["name"].toLocaleLowerCase()) return 1; + return 0; + }, + date: function (a, b) { + // sorts descending + if (a["date"] > b["date"]) return -1; + if (a["date"] < b["date"]) return 1; + return 0; + }, + size: function (a, b) { + // sorts descending + if (a["bytes"] > b["bytes"]) return -1; + if (a["bytes"] < b["bytes"]) return 1; + return 0; + }, + }, + {}, + "name", + [], + [], + 15 + ); + + self.onStartupComplete = function () { + self.listCfgFiles(); + self.loadBaseConfig(); + }; + + self.listCfgFiles = function () { + self.klipperViewModel.consoleMessage("debug", "listCfgFiles started"); + + OctoPrint.plugins.klipper.listCfg().done(function (response) { + self.klipperViewModel.consoleMessage("debug", "listCfgFiles done"); + self.configs.updateItems(response.files); + self.PathToConfigs(gettext("Path: ") + response.path); + self.configs.resetPage(); + }); + }; + + self.loadBaseConfig = function () { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + var baseconfig = self.settings.settings.plugins.klipper.configuration.baseconfig(); + if (baseconfig != "") { + self.klipperViewModel.consoleMessage("debug", "loadBaseConfig:" + baseconfig); + OctoPrint.plugins.klipper.getCfg(baseconfig).done(function (response) { + var config = { + content: response.response.config, + file: baseconfig, + }; + self.klipperEditorViewModel.process(config).then(); + }); + } + }; + + self.removeCfg = function (config) { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + var perform = function () { + OctoPrint.plugins.klipper + .deleteCfg(config) + .done(function () { + self.listCfgFiles(); + }) + .fail(function (response) { + var html = "

" + _.sprintf(gettext("Failed to remove config %(name)s.

Please consult octoprint.log for details.

"), { name: _.escape(config) }); + html += pnotifyAdditionalInfo('
' + _.escape(response.responseText) + "
"); + new PNotify({ + title: gettext("Could not remove config"), + text: html, + type: "error", + hide: false, + }); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext('You are about to delete config file "%(name)s".'), { + name: _.escape(config), + }), + perform + ); + }; + + self.markFilesOnPage = function () { + self.markedForFileRemove(_.uniq(self.markedForFileRemove().concat(_.map(self.configs.paginatedItems(), "file")))); + }; + + self.markAllFiles = function () { + self.markedForFileRemove(_.map(self.configs.allItems, "file")); + }; + + self.clearMarkedFiles = function () { + self.markedForFileRemove.removeAll(); + }; + + self.removeMarkedFiles = function () { + var perform = function () { + self._bulkRemove(self.markedForFileRemove()).done(function () { + self.markedForFileRemove.removeAll(); + }); + }; + + showConfirmationDialog( + _.sprintf(gettext("You are about to delete %(count)d config files."), { + count: self.markedForFileRemove().length, + }), + perform + ); + }; + + self._bulkRemove = function (files) { + var title, message, handler; + + title = gettext("Deleting config files"); + message = _.sprintf(gettext("Deleting %(count)d config files..."), { + count: files.length, + }); + + handler = function (filename) { + return OctoPrint.plugins.klipper + .deleteCfg(filename) + .done(function () { + deferred.notify( + _.sprintf(gettext("Deleted %(filename)s..."), { + filename: _.escape(filename), + }), + true + ); + self.markedForFileRemove.remove(function (item) { + return item.name == filename; + }); + }) + .fail(function () { + deferred.notify(_.sprintf(gettext("Deleting of %(filename)s failed, continuing..."), { filename: _.escape(filename) }), false); + }); + }; + + var deferred = $.Deferred(); + var promise = deferred.promise(); + var options = { + title: title, + message: message, + max: files.length, + output: true, + }; + showProgressModal(options, promise); + + var requests = []; + _.each(files, function (filename) { + var request = handler(filename); + requests.push(request); + }); + + $.when.apply($, _.map(requests, wrapPromiseWithAlways)).done(function () { + deferred.resolve(); + self.listCfgFiles(); + }); + + return promise; + }; + + self.showBackupsDialog = function () { + self.klipperViewModel.consoleMessage("debug", "showBackupsDialog"); + self.klipperBackupViewModel.listBakFiles(); + var dialog = $("#klipper_backups_dialog"); + dialog.modal({ + show: "true", + }); + }; + + self.showEditor = function () { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + var editorDialog = $("#klipper_editor"); + editorDialog.modal({ + show: "true", + width: "90%", + backdrop: "static", + }); + } + + self.newFile = function () { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + var config = { + content: "", + file: "Change Filename", + }; + self.klipperEditorViewModel.process(config).then( + function() { self.showEditor(); } + ); + }; + + self.openConfig = function (file) { + if (!self.klipperViewModel.hasRight("CONFIG")) return; + + OctoPrint.plugins.klipper.getCfg(file).done(function (response) { + var config = { + content: response.response.config, + file: file, + }; + self.klipperEditorViewModel.process(config).then( + function() { self.showEditor(); } + ); + }); + }; + + self.addMacro = function () { + self.settings.settings.plugins.klipper.macros.push({ + name: "Macro", + macro: "", + sidebar: true, + tab: true, + }); + }; + + self.removeMacro = function (macro) { + self.settings.settings.plugins.klipper.macros.remove(macro); + }; + + self.moveMacroUp = function (macro) { + self.moveItemUp(self.settings.settings.plugins.klipper.macros, macro); + }; + + self.moveMacroDown = function (macro) { + self.moveItemDown(self.settings.settings.plugins.klipper.macros, macro); + }; + + self.addProbePoint = function () { + self.settings.settings.plugins.klipper.probe.points.push({ + name: "point-#", + x: 0, + y: 0, + z: 0, + }); + }; + + self.removeProbePoint = function (point) { + self.settings.settings.plugins.klipper.probe.points.remove(point); + }; + + self.moveProbePointUp = function (macro) { + self.moveItemUp(self.settings.settings.plugins.klipper.probe.points, macro); + }; + + self.moveProbePointDown = function (macro) { + self.moveItemDown(self.settings.settings.plugins.klipper.probe.points, macro); + }; + + self.moveItemDown = function (list, item) { + var i = list().indexOf(item); + if (i < list().length - 1) { + var rawList = list(); + list.splice(i, 2, rawList[i + 1], rawList[i]); + } + }; + + self.moveItemUp = function (list, item) { + var i = list().indexOf(item); + if (i > 0) { + var rawList = list(); + list.splice(i - 1, 2, rawList[i], rawList[i - 1]); + } + }; + + self.onDataUpdaterPluginMessage = function (plugin, data) { + if (plugin == "klipper" && data.type == "reload" && data.subtype == "configlist") { + self.klipperViewModel.consoleMessage("debug", "onDataUpdaterPluginMessage klipper reload configlist"); + self.listCfgFiles(); + } + }; + } + + OCTOPRINT_VIEWMODELS.push({ + construct: KlipperSettingsViewModel, + dependencies: ["settingsViewModel", "klipperViewModel", "klipperEditorViewModel", "klipperBackupViewModel", "accessViewModel"], + elements: ["#settings_plugin_klipper"], + }); }); diff --git a/octoprint_klipper/static/js/lib/ace/mode-klipper_config.js b/octoprint_klipper/static/js/lib/ace/mode-klipper_config.js index 92a538a..0aad9e4 100644 --- a/octoprint_klipper/static/js/lib/ace/mode-klipper_config.js +++ b/octoprint_klipper/static/js/lib/ace/mode-klipper_config.js @@ -160,7 +160,7 @@ ace.define("ace/mode/klipper_config_highlight_rules",[], function(require, expor caseInsensitive: true }, { token: "support.type", - regex: /[\^~!]*(?:z:)?[a-zA-Z]{1,2}\d{1,2}(?:\.\d{1,2})?/, + regex: /[\^~!]*(?:z:)?[a-zA-Z]{1,4}(?:gpio)?\d{1,2}(?:\.\d{1,2})?/, caseInsensitive: true }], "#config_line_start_gcode": [{ @@ -192,8 +192,8 @@ ace.define("ace/mode/klipper_config_highlight_rules",[], function(require, expor }] }], "#config_line": [{ - token: ["variable.name", "variable.name"], - regex: /(?!gcode|sensor_type|rpi:)(\w+)(\s*[:]\s*)/, + token: ["variable.name", "variable.name", "variable.name", "variable.name"], + regex: /(?!gcode|sensor_type|rpi:)(\w+)(\s*[:]\s*\w+[:])|(?!gcode|sensor_type|rpi:)(\w+)(\s*[:]\s*)/, push: [{ token: "text", regex: /$/, diff --git a/octoprint_klipper/templates/klipper_backups_dialog.jinja2 b/octoprint_klipper/templates/klipper_backups_dialog.jinja2 new file mode 100644 index 0000000..1754550 --- /dev/null +++ b/octoprint_klipper/templates/klipper_backups_dialog.jinja2 @@ -0,0 +1,110 @@ + diff --git a/octoprint_klipper/templates/klipper_editor.jinja2 b/octoprint_klipper/templates/klipper_editor.jinja2 new file mode 100644 index 0000000..23f131b --- /dev/null +++ b/octoprint_klipper/templates/klipper_editor.jinja2 @@ -0,0 +1,46 @@ + diff --git a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 index a0508e8..5c8b675 100644 --- a/octoprint_klipper/templates/klipper_graph_dialog.jinja2 +++ b/octoprint_klipper/templates/klipper_graph_dialog.jinja2 @@ -1,12 +1,12 @@