extends Control const SERVER_STATUS_TEXT: Dictionary = { "RUNNING": "Server is running!", "NOT_RUNNING": "Server is not running." } const ACCEPTED_FILE_FORMATS := ["md"] # server should ignore these file types when adding endpoints export(bool) var enable_file_autosave := true # if true, will save the active file's contents every save_file_timeout seconds. export(float) var save_file_timeout := 4.0 # the time to save document content after the edited signal of TextEdit enum ContextMenuOptions { MOVE_UP, MOVE_DOWN, } onready var server_path_label := $"%ServerPathLabel" onready var port_spin_box := $"%PortSpinBox" onready var start_server_button := $"%StartServerButton" onready var server_status_label := $"%ServerStatusLabel" onready var document_title_lineedit := $"%DocTitleLineEdit" onready var document_date_lineedit := $"%DocDateLineEdit" onready var file_tree := $"%FileTree" onready var file_tree_context_menu := $"%FileTreeContextMenu" onready var document_input_textedit := $"%DocInputTextEdit" onready var content_preview_richtextlabel := $"%ContentPreviewRichTextLabel" onready var server_folder_dialog := $"%ServerFolderDialog" signal server_folder_changed(new_path) # new_path: String signal server_port_changed(new_port) # new_port: int signal start_server_button_pressed(port, path) # port: int, path: String signal stop_server_button_pressed() # emit from %StartServerButton, when the server is not running. signal open_browser_button_pressed(port) # port: int signal files_selection_changed(new_files) # new_files: Array var server_dir: String setget set_server_dir var is_server_running: bool = false setget set_server_running var working_files: Array = [] # Array var current_file: FileDef = null var current_tree_selection: TreeItem = null func _ready() -> void: # TODO: put this in a config file if !OS.has_feature("editor"): self.server_dir = OS.get_executable_path().get_base_dir() else: self.server_dir = ProjectSettings.globalize_path("res://") func set_server_dir(dir: String) -> void: server_dir = dir server_path_label.text = dir file_tree.clear() working_files.clear() var root: TreeItem = file_tree.create_item() root.set_text(0, "Server files") var directory := Directory.new() if directory.open(dir) == OK: directory.list_dir_begin() var file_name: String = directory.get_next() var idx: int = 0 while file_name != "": if !directory.current_is_dir() && (file_name.get_extension() in ACCEPTED_FILE_FORMATS): var fd = FileDef(file_name, false) working_files.append(fd) fd.content = _get_file_content(file_name) var in_filestxt := _is_file_in_filestxt(file_name) fd.include_in_filestxt = in_filestxt if in_filestxt: var file_metadata := _get_file_metadata(file_name) fd.title = file_metadata["title"] fd.date = file_metadata["date"] idx += 1 file_name = directory.get_next() directory.list_dir_end() _reconstruct_tree_from_working_files() func _reconstruct_tree_from_working_files() -> void: file_tree.clear() var root: TreeItem = file_tree.create_item() root.set_text(0, "Server files") for i in working_files.size(): var fd = working_files[i] as FileDef var item: TreeItem = file_tree.create_item(root) item.set_cell_mode(0, TreeItem.CELL_MODE_CHECK) item.set_metadata(0, {"file_def": fd, "id": i}) item.set_editable(0, true) item.set_text(0, fd.file_path) item.set_checked(0, fd.include_in_filestxt) func _on_FileTree_item_edited() -> void: var item: TreeItem = file_tree.get_edited() var fd: FileDef = item.get_metadata(0)["file_def"] fd.include_in_filestxt = item.is_checked(0) func _on_FileTree_item_selected() -> void: var item: TreeItem = file_tree.get_selected() current_tree_selection = item current_file = item.get_metadata(0)["file_def"] as FileDef document_input_textedit.text = current_file.content document_date_lineedit.text = current_file.date document_title_lineedit.text = current_file.title func _on_FileTree_item_rmb_selected(position: Vector2) -> void: current_tree_selection = file_tree.get_selected() file_tree_context_menu.rect_position = position + file_tree.rect_global_position if (current_tree_selection.get_metadata(0)["id"] as int) == 0: file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_UP, true) else: file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_UP, false) if (current_tree_selection.get_metadata(0)["id"] as int) == working_files.size() - 1: file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_DOWN, true) else: file_tree_context_menu.set_item_disabled(ContextMenuOptions.MOVE_DOWN, false) file_tree_context_menu.popup() func _on_OpenServerFolderButton_pressed() -> void: server_folder_dialog.popup() func set_server_running(running: bool) -> void: is_server_running = running if is_server_running: server_status_label.text = SERVER_STATUS_TEXT.RUNNING start_server_button.text = "Stop server" port_spin_box.editable = false document_date_lineedit.editable = false document_title_lineedit.editable = false document_input_textedit.readonly = true else: server_status_label.text = SERVER_STATUS_TEXT.NOT_RUNNING start_server_button.text = "Start server" port_spin_box.editable = !false document_date_lineedit.editable = !false document_title_lineedit.editable = !false document_input_textedit.readonly = !true func _generate_filestxt(): var files := "" for file in working_files: file = file as FileDef if file.include_in_filestxt: files += "%s %s %s\n" % [file.file_path, file.date, file.title] var f := File.new() if f.open(server_dir.plus_file("files.txt"), File.WRITE) == OK: f.store_string(files) f.close() else: push_error("File.txt open for save error!") # TODO: show a user-facing error func _on_StartServerButton_pressed() -> void: if is_server_running: emit_signal("stop_server_button_pressed") else: _generate_filestxt() emit_signal("start_server_button_pressed", port_spin_box.value, server_dir) func _is_file_in_filestxt(path: String) -> bool: var f := File.new() if f.open(server_dir.plus_file("files.txt"), File.READ) == OK: var text := f.get_as_text() return path in text else: return false func _get_file_metadata(path: String) -> Dictionary: var f := File.new() var res = { "file_path": path, "date": "", "title": "", "content": "", } if f.open(server_dir.plus_file("files.txt"), File.READ) == OK: var text := f.get_as_text() var lines := text.split("\n") for line in lines: line = line as String if line.empty(): continue var def = line.split(" ", true, 2) if def[0] == path: # res["file_path"] = def[0] res["date"] = def[1] if def.size() > 1 else "" res["title"] = def[2] if def.size() > 2 else "" break res["content"] = _get_file_content(path) return res func _get_file_content(path: String) -> String: var content: String = "" var f := File.new() if f.open(server_dir.plus_file(path), File.READ) == OK: content = f.get_as_text() return content func _on_FileTreeContextMenu_id_pressed(id: int) -> void: var idx = current_tree_selection.get_metadata(0)["id"] as int var fd = working_files[idx] as FileDef match id: ContextMenuOptions.MOVE_DOWN: working_files.remove(idx) working_files.insert(idx + 1, fd) _reconstruct_tree_from_working_files() ContextMenuOptions.MOVE_UP: working_files.remove(idx) working_files.insert(idx - 1, fd) _reconstruct_tree_from_working_files() func _on_DocTitleLineEdit_text_changed(new_text: String) -> void: if current_file: current_file.title = new_text func _on_DocDateLineEdit_text_changed(new_text: String) -> void: if current_file: current_file.date = new_text func _on_DocInputTextEdit_text_changed() -> void: if current_file: var new_text: String = document_input_textedit.text current_file.content = new_text if !current_file.timer: var t := Timer.new() t.wait_time = save_file_timeout t.one_shot = true current_file.timer = t add_child(t) t.connect("timeout", self, "_on_EditedTimeout_timeout", [t, current_file]) else: current_file.timer.stop() current_file.timer.start() func _on_EditedTimeout_timeout(timer: Timer, file: FileDef) -> void: file.timer = null timer.queue_free() var f := File.new() if f.open(server_dir.plus_file(file.file_path), File.WRITE) == OK: f.store_string(file.content) func FileDef( file_path: String, include_in_filestxt: bool, title: String = "", date: String = "") -> FileDef: var fd := FileDef.new() fd.file_path = file_path fd.include_in_filestxt = include_in_filestxt fd.title = title fd.date = date return fd class FileDef: var file_path: String # relative var include_in_filestxt: bool = true var title: String # optional var date: String # optional, YYYY-MM-DD var content: String var timer: Timer