Compare commits
42 Commits
8f757af9de
...
main
Author | SHA1 | Date | |
---|---|---|---|
c0759f0cbe
|
|||
47dc68028c | |||
e44cac165a
|
|||
3d2fd37191
|
|||
836c101e6f
|
|||
8cbfffba94 | |||
7280277029
|
|||
a203972157 | |||
7f7018f183 | |||
44cfe464c5 | |||
df2dc1097e | |||
68b4a03a3f | |||
815aae5a4d | |||
6a98956b03 | |||
0602345af9 | |||
c8d19197e6 | |||
45cf485d9d | |||
741e23af56 | |||
2284a1d1dd | |||
d309c40fc3 | |||
fc1ca1e1b0 | |||
12090f7930 | |||
6dd063382c | |||
550a1c80a4 | |||
80bc3e4a11 | |||
791743b765 | |||
e337f78740 | |||
514ba20c41 | |||
003eb04013 | |||
5380b59ee2 | |||
eeb6744947 | |||
d3764d3c4a | |||
9fae276011 | |||
c3e0bd043f | |||
a6266ef98b | |||
e7d309efae | |||
9ba719e8d3 | |||
fe8aa24ca8 | |||
6478f54689 | |||
ec63dde2b0 | |||
01835ec2d7 | |||
a1ffe4d4ba |
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,3 +9,6 @@ export_presets.cfg
|
|||||||
# Mono-specific ignores
|
# Mono-specific ignores
|
||||||
.mono/
|
.mono/
|
||||||
data_*/
|
data_*/
|
||||||
|
|
||||||
|
# VSCode config folder
|
||||||
|
.vscode/
|
||||||
|
77
Main.gd
Normal file
77
Main.gd
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
var mime_types := MimeTypeHelper.generate_db()
|
||||||
|
|
||||||
|
var _server: HTTPServer = null
|
||||||
|
|
||||||
|
onready var server_ui := $ServerUI
|
||||||
|
onready var server_dir = server_ui.server_dir
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _start_server(port: int = 3001) -> void:
|
||||||
|
if _server:
|
||||||
|
return
|
||||||
|
|
||||||
|
_server = HTTPServer.new()
|
||||||
|
_server.endpoint(HTTPServer.Method.GET, "/", funcref(self, "_serve_file"))
|
||||||
|
|
||||||
|
if _server.listen(port) != OK:
|
||||||
|
# TODO: show error to user here
|
||||||
|
return
|
||||||
|
|
||||||
|
server_ui.is_server_running = true
|
||||||
|
|
||||||
|
|
||||||
|
func _stop_server() -> void:
|
||||||
|
if _server:
|
||||||
|
_server.stop()
|
||||||
|
_server = null
|
||||||
|
|
||||||
|
server_ui.is_server_running = false
|
||||||
|
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if _server == null:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not _server.take_connection():
|
||||||
|
# TODO: show error to user here
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
func _serve_file(request: HTTPServer.Request, response: HTTPServer.Response) -> void:
|
||||||
|
var file_name: String = request.endpoint()
|
||||||
|
if file_name == "/": # if the request is for root, serve index
|
||||||
|
file_name = "index.html"
|
||||||
|
var f := File.new()
|
||||||
|
var success = f.open(server_dir.plus_file(file_name), File.READ)
|
||||||
|
|
||||||
|
if success == OK: # TODO: handle other errors like file not found
|
||||||
|
var mime := mime_types.get(file_name)
|
||||||
|
response.type(mime)
|
||||||
|
|
||||||
|
var data = f.get_buffer(f.get_len())
|
||||||
|
|
||||||
|
response.data(data)
|
||||||
|
|
||||||
|
else:
|
||||||
|
response.type(mime_types.get("txt"))
|
||||||
|
response.status(500)
|
||||||
|
response.data("Internal Server Error")
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ServerUI_start_server_button_pressed(port: int, new_dir: String) -> void:
|
||||||
|
server_dir = new_dir
|
||||||
|
_start_server(port)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ServerUI_stop_server_button_pressed() -> void:
|
||||||
|
_stop_server()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_ServerUI_server_folder_changed(new_path: String) -> void:
|
||||||
|
server_dir = new_path
|
15
Main.tscn
Normal file
15
Main.tscn
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
[gd_scene load_steps=3 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://Main.gd" type="Script" id=1]
|
||||||
|
[ext_resource path="res://ServerUI.tscn" type="PackedScene" id=2]
|
||||||
|
|
||||||
|
[node name="Main" type="Control"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource( 1 )
|
||||||
|
|
||||||
|
[node name="ServerUI" parent="." instance=ExtResource( 2 )]
|
||||||
|
|
||||||
|
[connection signal="server_folder_changed" from="ServerUI" to="." method="_on_ServerUI_server_folder_changed"]
|
||||||
|
[connection signal="start_server_button_pressed" from="ServerUI" to="." method="_on_ServerUI_start_server_button_pressed"]
|
||||||
|
[connection signal="stop_server_button_pressed" from="ServerUI" to="." method="_on_ServerUI_stop_server_button_pressed"]
|
@ -1,7 +1,9 @@
|
|||||||
# Godot frontend for Ticle
|
# Godot frontend for Tickle
|
||||||
|
|
||||||
Ticle is a WIP tiny static site "generator" that parses Markdown files right in the browser, with no compilation to HTML necessary, intended for equally tiny blogs.
|
Tickle is a WIP tiny static site "generator" that parses Markdown files right in the browser, with no compilation to HTML necessary, intended for equally tiny blogs.
|
||||||
|
|
||||||
This project aims to provide a nice frontend/UI to manage Ticle files and an easy way to run your site locally.
|
This project aims to provide a nice frontend/UI to manage Tickle files and an easy way to run your site locally.
|
||||||
|
|
||||||
|
It uses a slightly modified version of [godot-http-server](https://github.com/velopman/godot-http-server) for the server side.
|
||||||
|
|
||||||
|
The version of Godot used for this project is 3.5rc5.
|
||||||
|
359
ServerUI.gd
Normal file
359
ServerUI.gd
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
extends Control
|
||||||
|
|
||||||
|
const SERVER_STATUS_TEXT: Dictionary = {
|
||||||
|
true: "Server is running!",
|
||||||
|
false: "Server is not running.",
|
||||||
|
}
|
||||||
|
|
||||||
|
const START_BUTTON_SERVER_TEXT: Dictionary = {
|
||||||
|
true: "Stop server",
|
||||||
|
false: "Start server",
|
||||||
|
}
|
||||||
|
|
||||||
|
const ACCEPTED_FILE_FORMATS := ["md"]
|
||||||
|
|
||||||
|
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 open_browser_button := $"%OpenBrowserButton"
|
||||||
|
|
||||||
|
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 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 server_port_changed(new_port) # new_port: int
|
||||||
|
#signal open_browser_button_pressed(port) # port: int
|
||||||
|
#signal files_selection_changed(new_files) # new_files: Array<String>
|
||||||
|
|
||||||
|
var server_dir: String setget set_server_dir
|
||||||
|
var is_server_running: bool = false setget set_server_running
|
||||||
|
|
||||||
|
var working_files: Array = [] # Array<FileDef>
|
||||||
|
|
||||||
|
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:
|
||||||
|
if directory.list_dir_begin() != OK:
|
||||||
|
push_error("Directory error") # TODO: show a user-facing error
|
||||||
|
|
||||||
|
var file_name: String = directory.get_next()
|
||||||
|
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"]
|
||||||
|
|
||||||
|
file_name = directory.get_next()
|
||||||
|
|
||||||
|
directory.list_dir_end()
|
||||||
|
|
||||||
|
_reconstruct_tree_from_working_files()
|
||||||
|
emit_signal("server_folder_changed", server_dir)
|
||||||
|
|
||||||
|
|
||||||
|
func _reconstruct_tree_from_working_files() -> void:
|
||||||
|
current_file = null
|
||||||
|
current_tree_selection = null
|
||||||
|
|
||||||
|
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
|
||||||
|
file_tree_context_menu.popup()
|
||||||
|
|
||||||
|
if is_server_running:
|
||||||
|
return # the file tree can't be edited while the server is running, so disable moving items as well
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
func _on_OpenServerFolderButton_pressed() -> void:
|
||||||
|
server_folder_dialog.popup()
|
||||||
|
|
||||||
|
|
||||||
|
func set_server_running(running: bool) -> void:
|
||||||
|
is_server_running = running
|
||||||
|
|
||||||
|
port_spin_box.editable = !running
|
||||||
|
document_date_lineedit.editable = !running
|
||||||
|
document_title_lineedit.editable = !running
|
||||||
|
document_input_textedit.readonly = running
|
||||||
|
open_browser_button.disabled = !running
|
||||||
|
_set_file_tree_disabled(running)
|
||||||
|
_set_context_menu_disabled(running)
|
||||||
|
|
||||||
|
server_status_label.text = SERVER_STATUS_TEXT[running]
|
||||||
|
start_server_button.text = START_BUTTON_SERVER_TEXT[running]
|
||||||
|
|
||||||
|
|
||||||
|
func _generate_filestxt():
|
||||||
|
var files := ""
|
||||||
|
|
||||||
|
for file in working_files:
|
||||||
|
file = file as FileDef
|
||||||
|
if file.include_in_filestxt:
|
||||||
|
files += ("%s %s %s" % [file.file_path, file.date, file.title]).strip_edges(false, true) + "\n"
|
||||||
|
|
||||||
|
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 _set_file_tree_disabled(disabled: bool) -> void:
|
||||||
|
var root = file_tree.get_root()
|
||||||
|
var tree_item = root.get_children() as TreeItem
|
||||||
|
while tree_item != null:
|
||||||
|
tree_item.set_editable(0, !disabled)
|
||||||
|
tree_item = tree_item.get_next()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_context_menu_disabled(disabled: bool) -> void:
|
||||||
|
for i in file_tree_context_menu.get_item_count():
|
||||||
|
file_tree_context_menu.set_item_disabled(i, disabled)
|
||||||
|
|
||||||
|
|
||||||
|
func _commit_all_files() -> void:
|
||||||
|
for file in working_files:
|
||||||
|
file = file as FileDef
|
||||||
|
file.commit(server_dir)
|
||||||
|
|
||||||
|
|
||||||
|
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_OpenBrowserButton_pressed() -> void:
|
||||||
|
if OS.shell_open("http://localhost:%s" % port_spin_box.value) != OK:
|
||||||
|
push_error("Error opening browser!") # TODO: show a user-facing error
|
||||||
|
|
||||||
|
|
||||||
|
func _on_RefreshFilesButton_pressed() -> void:
|
||||||
|
_commit_all_files()
|
||||||
|
_generate_filestxt()
|
||||||
|
self.server_dir = server_dir
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
# warning-ignore:return_value_discarded
|
||||||
|
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()
|
||||||
|
|
||||||
|
file.commit(server_dir)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
func commit(server_dir: String) -> void:
|
||||||
|
var f := File.new()
|
||||||
|
var open := f.open(server_dir.plus_file(file_path), File.WRITE)
|
||||||
|
if open == OK:
|
||||||
|
f.store_string(content)
|
||||||
|
|
||||||
|
if timer && !timer.is_stopped(): # if there's an autosave pending, stop it since we're setting it here.
|
||||||
|
timer.stop()
|
||||||
|
|
||||||
|
else:
|
||||||
|
push_error("Error committing file %s, code %s" % [file_path, open]) # TODO: show a user-facing error
|
263
ServerUI.tscn
Normal file
263
ServerUI.tscn
Normal file
@ -0,0 +1,263 @@
|
|||||||
|
[gd_scene load_steps=2 format=2]
|
||||||
|
|
||||||
|
[ext_resource path="res://ServerUI.gd" type="Script" id=1]
|
||||||
|
|
||||||
|
[node name="ServerUI" type="Control"]
|
||||||
|
anchor_right = 1.0
|
||||||
|
anchor_bottom = 1.0
|
||||||
|
script = ExtResource( 1 )
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="."]
|
||||||
|
anchor_left = 0.0380859
|
||||||
|
anchor_top = 0.087
|
||||||
|
anchor_right = 0.961914
|
||||||
|
anchor_bottom = 0.936667
|
||||||
|
margin_top = -0.200005
|
||||||
|
custom_constants/separation = 9
|
||||||
|
__meta__ = {
|
||||||
|
"_edit_use_anchors_": true
|
||||||
|
}
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer"]
|
||||||
|
margin_right = 1182.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
alignment = 1
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
margin_left = 142.0
|
||||||
|
margin_top = 9.0
|
||||||
|
margin_right = 227.0
|
||||||
|
margin_bottom = 23.0
|
||||||
|
text = "Server folder:"
|
||||||
|
|
||||||
|
[node name="ServerPathLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 231.0
|
||||||
|
margin_top = 9.0
|
||||||
|
margin_right = 470.0
|
||||||
|
margin_bottom = 23.0
|
||||||
|
text = "/home/username/long/path/to/server"
|
||||||
|
|
||||||
|
[node name="OpenServerFolderButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
margin_left = 474.0
|
||||||
|
margin_right = 533.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
rect_min_size = Vector2( 0, 32 )
|
||||||
|
text = "Open..."
|
||||||
|
|
||||||
|
[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
margin_left = 537.0
|
||||||
|
margin_top = 9.0
|
||||||
|
margin_right = 611.0
|
||||||
|
margin_bottom = 23.0
|
||||||
|
text = "Server port:"
|
||||||
|
|
||||||
|
[node name="PortSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 615.0
|
||||||
|
margin_right = 689.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
min_value = 81.0
|
||||||
|
max_value = 8000.0
|
||||||
|
value = 3001.0
|
||||||
|
|
||||||
|
[node name="StartServerButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 693.0
|
||||||
|
margin_right = 778.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
rect_min_size = Vector2( 0, 32 )
|
||||||
|
text = "Start server"
|
||||||
|
|
||||||
|
[node name="ServerStatusLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 782.0
|
||||||
|
margin_top = 9.0
|
||||||
|
margin_right = 918.0
|
||||||
|
margin_bottom = 23.0
|
||||||
|
text = "Server is not running."
|
||||||
|
|
||||||
|
[node name="OpenBrowserButton" type="Button" parent="VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 922.0
|
||||||
|
margin_right = 1040.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
rect_min_size = Vector2( 0, 32 )
|
||||||
|
size_flags_horizontal = 12
|
||||||
|
disabled = true
|
||||||
|
text = "Open in browser"
|
||||||
|
|
||||||
|
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer"]
|
||||||
|
margin_top = 41.0
|
||||||
|
margin_right = 1182.0
|
||||||
|
margin_bottom = 611.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
split_offset = -263
|
||||||
|
|
||||||
|
[node name="VBoxContainer2" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"]
|
||||||
|
margin_right = 322.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer2"]
|
||||||
|
margin_right = 322.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
|
||||||
|
margin_top = 9.0
|
||||||
|
margin_right = 30.0
|
||||||
|
margin_bottom = 23.0
|
||||||
|
size_flags_horizontal = 0
|
||||||
|
text = "Files"
|
||||||
|
|
||||||
|
[node name="RefreshFilesButton" type="Button" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 262.0
|
||||||
|
margin_right = 322.0
|
||||||
|
margin_bottom = 32.0
|
||||||
|
rect_min_size = Vector2( 0, 32 )
|
||||||
|
size_flags_horizontal = 10
|
||||||
|
text = "Refresh"
|
||||||
|
|
||||||
|
[node name="FileTree" type="Tree" parent="VBoxContainer/HSplitContainer/VBoxContainer2"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_top = 36.0
|
||||||
|
margin_right = 322.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
allow_rmb_select = true
|
||||||
|
hide_root = true
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer"]
|
||||||
|
margin_left = 334.0
|
||||||
|
margin_right = 1182.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer"]
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"]
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
|
||||||
|
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 24.0
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
|
||||||
|
margin_top = 5.0
|
||||||
|
margin_right = 32.0
|
||||||
|
margin_bottom = 19.0
|
||||||
|
text = "Title:"
|
||||||
|
|
||||||
|
[node name="DocTitleLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 36.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 24.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "(Optional)"
|
||||||
|
|
||||||
|
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
|
||||||
|
margin_top = 28.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 52.0
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
|
||||||
|
margin_top = 5.0
|
||||||
|
margin_right = 34.0
|
||||||
|
margin_bottom = 19.0
|
||||||
|
text = "Date:"
|
||||||
|
|
||||||
|
[node name="DocDateLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_left = 38.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 24.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
placeholder_text = "(Optional)"
|
||||||
|
|
||||||
|
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
|
||||||
|
margin_top = 56.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 570.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
split_offset = 328
|
||||||
|
|
||||||
|
[node name="ContentEditContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer"]
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 514.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 14.0
|
||||||
|
text = "Content"
|
||||||
|
|
||||||
|
[node name="DocInputTextEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_top = 18.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 514.0
|
||||||
|
size_flags_horizontal = 3
|
||||||
|
size_flags_vertical = 3
|
||||||
|
show_line_numbers = true
|
||||||
|
wrap_enabled = true
|
||||||
|
|
||||||
|
[node name="ContentPreviewContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer"]
|
||||||
|
visible = false
|
||||||
|
margin_left = 390.0
|
||||||
|
margin_right = 848.0
|
||||||
|
margin_bottom = 514.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentPreviewContainer"]
|
||||||
|
margin_right = 458.0
|
||||||
|
margin_bottom = 14.0
|
||||||
|
text = "Content Preview (Not accurate!)"
|
||||||
|
|
||||||
|
[node name="ContentPreviewRichTextLabel" type="RichTextLabel" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentPreviewContainer"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_top = 18.0
|
||||||
|
margin_right = 458.0
|
||||||
|
margin_bottom = 514.0
|
||||||
|
size_flags_vertical = 3
|
||||||
|
|
||||||
|
[node name="ServerFolderDialog" type="FileDialog" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
anchor_left = 0.5
|
||||||
|
anchor_top = 0.5
|
||||||
|
anchor_right = 0.5
|
||||||
|
anchor_bottom = 0.5
|
||||||
|
margin_left = -482.0
|
||||||
|
margin_top = -281.0
|
||||||
|
margin_right = 482.0
|
||||||
|
margin_bottom = 281.0
|
||||||
|
window_title = "Open a Directory"
|
||||||
|
mode = 2
|
||||||
|
access = 2
|
||||||
|
|
||||||
|
[node name="FileTreeContextMenu" type="PopupMenu" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
margin_right = 95.0
|
||||||
|
margin_bottom = 56.0
|
||||||
|
rect_min_size = Vector2( 95, 56 )
|
||||||
|
items = [ "Move Up", null, 0, false, false, 0, 0, null, "", false, "Move Down", null, 0, false, false, 1, 0, null, "", false ]
|
||||||
|
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenServerFolderButton" to="." method="_on_OpenServerFolderButton_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/StartServerButton" to="." method="_on_StartServerButton_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HBoxContainer/OpenBrowserButton" to="." method="_on_OpenBrowserButton_pressed"]
|
||||||
|
[connection signal="pressed" from="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2/RefreshFilesButton" to="." method="_on_RefreshFilesButton_pressed"]
|
||||||
|
[connection signal="item_edited" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_edited"]
|
||||||
|
[connection signal="item_rmb_selected" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_rmb_selected"]
|
||||||
|
[connection signal="item_selected" from="VBoxContainer/HSplitContainer/VBoxContainer2/FileTree" to="." method="_on_FileTree_item_selected"]
|
||||||
|
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer/DocTitleLineEdit" to="." method="_on_DocTitleLineEdit_text_changed"]
|
||||||
|
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2/DocDateLineEdit" to="." method="_on_DocDateLineEdit_text_changed"]
|
||||||
|
[connection signal="text_changed" from="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer/DocInputTextEdit" to="." method="_on_DocInputTextEdit_text_changed"]
|
||||||
|
[connection signal="dir_selected" from="ServerFolderDialog" to="." method="set_server_dir"]
|
||||||
|
[connection signal="id_pressed" from="FileTreeContextMenu" to="." method="_on_FileTreeContextMenu_id_pressed"]
|
165
addons/http_server/http_server.gd
Normal file
165
addons/http_server/http_server.gd
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
class_name HTTPServer extends TCP_Server
|
||||||
|
|
||||||
|
|
||||||
|
# Public constants
|
||||||
|
|
||||||
|
const Method = preload("res://addons/http_server/method.gd")
|
||||||
|
const Request = preload("res://addons/http_server/request.gd")
|
||||||
|
const Response = preload("res://addons/http_server/response.gd")
|
||||||
|
const Status = preload("res://addons/http_server/status.gd")
|
||||||
|
|
||||||
|
|
||||||
|
# Private variables
|
||||||
|
|
||||||
|
var __endpoints: Dictionary = {
|
||||||
|
# key: [Int, String], array with 0 index representing method, 1 index representing endpoint
|
||||||
|
# value: FuncRef, reference to function to call
|
||||||
|
}
|
||||||
|
var __fallback: FuncRef = null
|
||||||
|
var __server: TCP_Server = null
|
||||||
|
|
||||||
|
|
||||||
|
# Public methods
|
||||||
|
|
||||||
|
func endpoint(type: int, endpoint: String, function: FuncRef, binds: Array = []) -> void:
|
||||||
|
var endpoint_hash: Array = [type, endpoint]
|
||||||
|
if endpoint_hash in __endpoints:
|
||||||
|
print(
|
||||||
|
"[ERR] Endpoint already defined type: %s, endpoint: %s" % [
|
||||||
|
Method.type_to_identifier(type),
|
||||||
|
endpoint,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
__endpoints[endpoint_hash] = function
|
||||||
|
|
||||||
|
|
||||||
|
func fallback(function: FuncRef) -> void:
|
||||||
|
__fallback = function
|
||||||
|
|
||||||
|
|
||||||
|
func take_connection() -> StreamPeerTCP:
|
||||||
|
if !is_listening():
|
||||||
|
print(
|
||||||
|
"[ERR] Server is not listening, please initialize and listen before calling `take_connection`"
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var connection: StreamPeerTCP = .take_connection()
|
||||||
|
|
||||||
|
if connection:
|
||||||
|
__process_connection(connection)
|
||||||
|
|
||||||
|
return connection
|
||||||
|
|
||||||
|
|
||||||
|
# Private methods
|
||||||
|
|
||||||
|
func __process_connection(connection: StreamPeerTCP) -> void:
|
||||||
|
var content: PoolByteArray = PoolByteArray([])
|
||||||
|
|
||||||
|
while true:
|
||||||
|
var bytes = connection.get_available_bytes()
|
||||||
|
if bytes == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
var data = connection.get_partial_data(bytes)
|
||||||
|
content.append_array(data[1])
|
||||||
|
|
||||||
|
if content.empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var content_string: String = content.get_string_from_utf8()
|
||||||
|
var content_parts: Array = content_string.split("\r\n")
|
||||||
|
|
||||||
|
if content_parts.empty():
|
||||||
|
connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8())
|
||||||
|
return
|
||||||
|
|
||||||
|
var request_line = content_parts[0]
|
||||||
|
var request_line_parts = request_line.split(" ")
|
||||||
|
|
||||||
|
var method: String = request_line_parts[0]
|
||||||
|
var endpoint: String = request_line_parts[1]
|
||||||
|
|
||||||
|
var headers: Dictionary = {}
|
||||||
|
var header_index: int = content_parts.find("")
|
||||||
|
|
||||||
|
if header_index == -1:
|
||||||
|
print(
|
||||||
|
"[ERR] Error parsing request data: %s" % [String(content)]
|
||||||
|
)
|
||||||
|
connection.put_data(__response_from_status(Status.BAD_REQUEST).to_utf8())
|
||||||
|
return
|
||||||
|
|
||||||
|
for i in range(1, header_index):
|
||||||
|
var header_parts: Array = content_parts[i].split(":", true, 1)
|
||||||
|
var header = header_parts[0].strip_edges().to_lower()
|
||||||
|
var value = header_parts[1].strip_edges()
|
||||||
|
|
||||||
|
headers[header] = value
|
||||||
|
|
||||||
|
var body: String = ""
|
||||||
|
if header_index != content_parts.size() - 1:
|
||||||
|
var body_parts: Array = content_parts.slice(header_index + 1, content_parts.size())
|
||||||
|
body = PoolStringArray(body_parts).join("\r\n")
|
||||||
|
|
||||||
|
var response: Response = __process_request(method, endpoint, headers, body)
|
||||||
|
connection.put_data(response.get_data())
|
||||||
|
|
||||||
|
|
||||||
|
func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response:
|
||||||
|
var type: int = Method.description_to_type(method)
|
||||||
|
|
||||||
|
var request: Request = Request.new(
|
||||||
|
type,
|
||||||
|
endpoint,
|
||||||
|
headers,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
var endpoint_func: FuncRef = null
|
||||||
|
var endpoint_parts: PoolStringArray = endpoint.split("/", false)
|
||||||
|
|
||||||
|
while !endpoint_func:
|
||||||
|
var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")]
|
||||||
|
if __endpoints.has(endpoint_hash):
|
||||||
|
endpoint_func = __endpoints[endpoint_hash]
|
||||||
|
elif endpoint_parts.empty():
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
endpoint_parts.remove(endpoint_parts.size() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
if !endpoint_func:
|
||||||
|
print(
|
||||||
|
"[WRN] Recieved request for unknown endpoint, method: %s, endpoint: %s" % [method, endpoint]
|
||||||
|
)
|
||||||
|
if __fallback:
|
||||||
|
endpoint_func = __fallback
|
||||||
|
else:
|
||||||
|
return __response_from_status(Status.NOT_FOUND)
|
||||||
|
|
||||||
|
var response: Response = Response.new()
|
||||||
|
|
||||||
|
|
||||||
|
if !endpoint_func.is_valid():
|
||||||
|
print(
|
||||||
|
"[ERR] FuncRef for endpoint not valid, method: %s, endpoint: %s" % [method, endpoint]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
print(
|
||||||
|
"[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint]
|
||||||
|
)
|
||||||
|
|
||||||
|
endpoint_func.call_func(request, response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
func __response_from_status(code: int) -> Response:
|
||||||
|
var response: Response = Response.new()
|
||||||
|
response.status(code)
|
||||||
|
|
||||||
|
return response
|
61
addons/http_server/method.gd
Normal file
61
addons/http_server/method.gd
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Public Constants
|
||||||
|
|
||||||
|
enum {
|
||||||
|
GET = 0,
|
||||||
|
HEAD,
|
||||||
|
POST,
|
||||||
|
PUT,
|
||||||
|
DELETE,
|
||||||
|
CONNECT,
|
||||||
|
OPTIONS,
|
||||||
|
TRACE,
|
||||||
|
PATCH
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Private constants
|
||||||
|
|
||||||
|
const __DESCRIPTIONS: Dictionary = {
|
||||||
|
GET: "Get",
|
||||||
|
HEAD: "Head",
|
||||||
|
POST: "Post",
|
||||||
|
PUT: "Put",
|
||||||
|
DELETE: "Delete",
|
||||||
|
CONNECT: "Connect",
|
||||||
|
OPTIONS: "Options",
|
||||||
|
TRACE: "Trace",
|
||||||
|
PATCH: "Patch",
|
||||||
|
}
|
||||||
|
|
||||||
|
const __TYPES: Dictionary = {
|
||||||
|
"GET": GET,
|
||||||
|
"HEAD": HEAD,
|
||||||
|
"POST": POST,
|
||||||
|
"PUT": PUT,
|
||||||
|
"DELETE": DELETE,
|
||||||
|
"CONNECT": CONNECT,
|
||||||
|
"OPTIONS": OPTIONS,
|
||||||
|
"TRACE": TRACE,
|
||||||
|
"PATCH": PATCH,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Public methods
|
||||||
|
|
||||||
|
static func description_to_type(description: String) -> int:
|
||||||
|
return identifier_to_type(description.to_upper())
|
||||||
|
|
||||||
|
|
||||||
|
static func identifier_to_type(identifier: String) -> int:
|
||||||
|
if __TYPES.has(identifier):
|
||||||
|
return __TYPES[identifier]
|
||||||
|
|
||||||
|
return -1
|
||||||
|
|
||||||
|
|
||||||
|
static func type_to_description(type: int) -> String:
|
||||||
|
return __DESCRIPTIONS[type]
|
||||||
|
|
||||||
|
|
||||||
|
static func type_to_identifier(type: int) -> String:
|
||||||
|
return type_to_description(type).to_upper()
|
8694
addons/http_server/mimetypes.gd
Normal file
8694
addons/http_server/mimetypes.gd
Normal file
File diff suppressed because it is too large
Load Diff
7
addons/http_server/plugin.cfg
Normal file
7
addons/http_server/plugin.cfg
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[plugin]
|
||||||
|
|
||||||
|
name="HTTPServer"
|
||||||
|
description="HTTP Server implementation for receiving HTTP requests from external sources."
|
||||||
|
author="velopman"
|
||||||
|
version="0.1"
|
||||||
|
script="plugin.gd"
|
10
addons/http_server/plugin.gd
Normal file
10
addons/http_server/plugin.gd
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
tool
|
||||||
|
extends EditorPlugin
|
||||||
|
|
||||||
|
|
||||||
|
func _enter_tree() -> void:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
func _exit_tree() -> void:
|
||||||
|
pass
|
68
addons/http_server/request.gd
Normal file
68
addons/http_server/request.gd
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
|
||||||
|
# Public constants
|
||||||
|
|
||||||
|
const Method = preload("res://addons/http_server/method.gd")
|
||||||
|
|
||||||
|
# Private variables
|
||||||
|
|
||||||
|
var __body: String = ""
|
||||||
|
var __endpoint: String = ""
|
||||||
|
var __headers: Dictionary = {
|
||||||
|
# key: String, header name
|
||||||
|
# value: Variant, header value
|
||||||
|
}
|
||||||
|
var __json_data = null # Variant
|
||||||
|
var __type: int = Method.GET
|
||||||
|
|
||||||
|
|
||||||
|
# Lifecyle methods
|
||||||
|
|
||||||
|
func _init(type: int, endpoint: String, headers: Dictionary, body: String) -> void:
|
||||||
|
__body = body
|
||||||
|
__endpoint = endpoint
|
||||||
|
__headers = headers
|
||||||
|
__type = type
|
||||||
|
|
||||||
|
|
||||||
|
# Public methods
|
||||||
|
|
||||||
|
func body() -> String:
|
||||||
|
return __body
|
||||||
|
|
||||||
|
|
||||||
|
func endpoint() -> String:
|
||||||
|
return __endpoint
|
||||||
|
|
||||||
|
|
||||||
|
func header(name: String = "", default = null): # Variant
|
||||||
|
return __headers.get(name, default)
|
||||||
|
|
||||||
|
|
||||||
|
func headers() -> Dictionary:
|
||||||
|
return __headers
|
||||||
|
|
||||||
|
|
||||||
|
func json(): # Variant
|
||||||
|
if __json_data != null:
|
||||||
|
return __json_data
|
||||||
|
|
||||||
|
var content_type = header("content-type")
|
||||||
|
if content_type != "application/json":
|
||||||
|
print(
|
||||||
|
"[WRN] Attempting to call get_json on a request with content-type: %s" % [content_type]
|
||||||
|
)
|
||||||
|
return null
|
||||||
|
|
||||||
|
var result = JSON.parse(__body)
|
||||||
|
if result.error:
|
||||||
|
print(
|
||||||
|
"[ERR] Error parsing request json: %s" % [result.error_string]
|
||||||
|
)
|
||||||
|
|
||||||
|
__json_data = result.result
|
||||||
|
|
||||||
|
return __json_data
|
||||||
|
|
||||||
|
|
||||||
|
func type() -> int:
|
||||||
|
return __type
|
108
addons/http_server/response.gd
Normal file
108
addons/http_server/response.gd
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
# Public Constants
|
||||||
|
|
||||||
|
const Status = preload("res://addons/http_server/status.gd")
|
||||||
|
|
||||||
|
|
||||||
|
# Private variables
|
||||||
|
|
||||||
|
var __data = "" # variant
|
||||||
|
var __headers: Dictionary = {
|
||||||
|
# key: String, header name
|
||||||
|
# value: Variant, header value
|
||||||
|
}
|
||||||
|
var __status: int = 200
|
||||||
|
var __type: MimeTypeHelper.MimeType
|
||||||
|
|
||||||
|
|
||||||
|
# Public methods
|
||||||
|
|
||||||
|
func data(data) -> void: # data: Variant
|
||||||
|
__data = data
|
||||||
|
|
||||||
|
|
||||||
|
func header(name: String, value) -> void: # value: Variant
|
||||||
|
__headers[name.to_lower()] = value
|
||||||
|
|
||||||
|
|
||||||
|
func json(data) -> void: # data: Variant
|
||||||
|
header("content-type", "application/json")
|
||||||
|
__data = data
|
||||||
|
|
||||||
|
|
||||||
|
func status(status: int) -> void:
|
||||||
|
__status = status
|
||||||
|
|
||||||
|
|
||||||
|
func type(type: MimeTypeHelper.MimeType) -> void:
|
||||||
|
__type = type
|
||||||
|
|
||||||
|
|
||||||
|
func to_utf8() -> PoolByteArray:
|
||||||
|
var content = PoolStringArray()
|
||||||
|
|
||||||
|
content.append(Status.code_to_status_line(__status))
|
||||||
|
|
||||||
|
var data = __data
|
||||||
|
if !data:
|
||||||
|
data = Status.code_to_description(__status)
|
||||||
|
|
||||||
|
if __headers.get("content-type", "") == "application/json":
|
||||||
|
data = JSON.print(data)
|
||||||
|
|
||||||
|
__headers['content-length'] = len(data)
|
||||||
|
__headers["content-type"] = "application/octet-stream" if !__type else __type.full_type
|
||||||
|
|
||||||
|
for header in __headers:
|
||||||
|
content.append("%s: %s" % [header, String(__headers[header])])
|
||||||
|
|
||||||
|
content.append("")
|
||||||
|
|
||||||
|
if data:
|
||||||
|
content.append(data)
|
||||||
|
|
||||||
|
return content.join("\r\n").to_utf8()
|
||||||
|
|
||||||
|
|
||||||
|
func get_data() -> PoolByteArray:
|
||||||
|
var res = __response_headers()
|
||||||
|
|
||||||
|
var data = __data
|
||||||
|
if !data:
|
||||||
|
return res
|
||||||
|
|
||||||
|
var type: MimeTypeHelper.MimeType = __type
|
||||||
|
if !type:
|
||||||
|
type = MimeTypeHelper.MimeType.new()
|
||||||
|
|
||||||
|
if data is String: # else, assume data is PoolByteArray
|
||||||
|
data = data.to_utf8()
|
||||||
|
|
||||||
|
res.append_array(data)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
|
|
||||||
|
# Private methods
|
||||||
|
|
||||||
|
func __response_headers() -> PoolByteArray:
|
||||||
|
var res = PoolStringArray()
|
||||||
|
|
||||||
|
res.append(Status.code_to_status_line(__status))
|
||||||
|
|
||||||
|
var data = __data
|
||||||
|
if !data:
|
||||||
|
data = Status.code_to_description(__status)
|
||||||
|
|
||||||
|
__headers["content-length"] = len(data)
|
||||||
|
|
||||||
|
__headers["content-type"] = "application/octet-stream" if !__type else __type.full_type
|
||||||
|
|
||||||
|
for header in __headers:
|
||||||
|
res.append("%s: %s" % [header, String(__headers[header])])
|
||||||
|
|
||||||
|
res.append("")
|
||||||
|
|
||||||
|
var s = res.join("\r\n")
|
||||||
|
s = s + "\r\n"
|
||||||
|
|
||||||
|
return s.to_utf8()
|
147
addons/http_server/status.gd
Normal file
147
addons/http_server/status.gd
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
# Public constants
|
||||||
|
|
||||||
|
enum {
|
||||||
|
CONTINUE = 100
|
||||||
|
SWITCHING_PROTOCOLS = 101
|
||||||
|
PROCESSING = 102
|
||||||
|
EARLY_HINTS = 103
|
||||||
|
OK = 200
|
||||||
|
CREATED = 201
|
||||||
|
ACCEPTED = 202
|
||||||
|
NON_AUTHORITATIVE_INFORMATION = 203
|
||||||
|
NO_CONTENT = 204
|
||||||
|
RESET_CONTENT = 205
|
||||||
|
PARTIAL_CONTENT = 206
|
||||||
|
MULTI_STATUS = 207
|
||||||
|
ALREADY_REPORTED = 208
|
||||||
|
IM_USED = 226
|
||||||
|
MULTIPLE_CHOICE = 300
|
||||||
|
MOVED_PERMANENTLY = 301
|
||||||
|
FOUND = 302
|
||||||
|
SEE_OTHER = 303
|
||||||
|
NOT_MODIFIED = 304
|
||||||
|
TEMPORARY_REDIRECT = 307
|
||||||
|
PERMANENT_REDIRECT = 308
|
||||||
|
BAD_REQUEST = 400
|
||||||
|
UNAUTHORIZED = 401
|
||||||
|
PAYMENT_REQUIRED = 402
|
||||||
|
FORBIDDEN = 403
|
||||||
|
NOT_FOUND = 404
|
||||||
|
METHOD_NOT_ALLOWED = 405
|
||||||
|
NOT_ACCEPTABLE = 406
|
||||||
|
PROXY_AUTHENTICATION_REQUIRED = 407
|
||||||
|
REQUEST_TIMEOUT = 408
|
||||||
|
CONFLICT = 409
|
||||||
|
GONE = 410
|
||||||
|
LENGTH_REQUIRED = 411
|
||||||
|
PRECONDITION_FAILED = 412
|
||||||
|
PAYLOAD_TOO_LARGE = 413
|
||||||
|
URI_TOO_LONG = 414
|
||||||
|
UNSUPPORTED_MEDIA_TYPE = 415
|
||||||
|
RANGE_NOT_SATISFIABLE = 416
|
||||||
|
EXPECTATION_FAILED = 417
|
||||||
|
IM_A_TEAPOT = 418
|
||||||
|
MISDIRECTED_REQUEST = 421
|
||||||
|
UNPROCESSABLE_ENTITY = 422
|
||||||
|
LOCKED = 423
|
||||||
|
FAILED_DEPENDENCY = 424
|
||||||
|
TOO_EARLY = 425
|
||||||
|
UPGRADE_REQUIRED = 426
|
||||||
|
PRECONDITION_REQUIRED = 428
|
||||||
|
TOO_MANY_REQUESTS = 429
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE = 431
|
||||||
|
UNAVAILABLE_FOR_LEGAL_REASONS = 451
|
||||||
|
INTERNAL_SERVER_ERROR = 500
|
||||||
|
NOT_IMPLEMENTED = 501
|
||||||
|
BAD_GATEWAY = 502
|
||||||
|
SERVICE_UNAVAILABLE = 503
|
||||||
|
GATEWAY_TIMEOUT = 504
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED = 505
|
||||||
|
VARIANT_ALSO_NEGOTIATES = 506
|
||||||
|
INSUFFICIENT_STORAGE = 507
|
||||||
|
LOOP_DETECTED = 508
|
||||||
|
NOT_EXTENDED = 510
|
||||||
|
NETWORK_AUTHENTICATION_REQUIRED = 511
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Private constants
|
||||||
|
|
||||||
|
const __DESCRIPTIONS: Dictionary = {
|
||||||
|
CONTINUE: "Continue",
|
||||||
|
SWITCHING_PROTOCOLS: "Switching Protocols",
|
||||||
|
PROCESSING: "Processing",
|
||||||
|
EARLY_HINTS: "Early Hints",
|
||||||
|
OK: "Ok",
|
||||||
|
CREATED: "Created",
|
||||||
|
ACCEPTED: "Accepted",
|
||||||
|
NON_AUTHORITATIVE_INFORMATION: "Non-Authoritative Information",
|
||||||
|
NO_CONTENT: "No Content",
|
||||||
|
RESET_CONTENT: "Reset Content",
|
||||||
|
PARTIAL_CONTENT: "Partial Content",
|
||||||
|
MULTI_STATUS: "Multi-Status",
|
||||||
|
ALREADY_REPORTED: "Already Reported",
|
||||||
|
IM_USED: "IM Used",
|
||||||
|
MULTIPLE_CHOICE: "Multiple Choice",
|
||||||
|
MOVED_PERMANENTLY: "Moved Permanently",
|
||||||
|
FOUND: "Found",
|
||||||
|
SEE_OTHER: "See Other",
|
||||||
|
NOT_MODIFIED: "Not Modified",
|
||||||
|
TEMPORARY_REDIRECT: "Temporary Redirect",
|
||||||
|
PERMANENT_REDIRECT: "Permanent Redirect",
|
||||||
|
BAD_REQUEST: "Bad Request",
|
||||||
|
UNAUTHORIZED: "Unauthorized",
|
||||||
|
PAYMENT_REQUIRED: "Payment Required",
|
||||||
|
FORBIDDEN: "Forbidden",
|
||||||
|
NOT_FOUND: "Not Found",
|
||||||
|
METHOD_NOT_ALLOWED: "Method Not Allowed",
|
||||||
|
NOT_ACCEPTABLE: "Not Acceptable",
|
||||||
|
PROXY_AUTHENTICATION_REQUIRED: "Proxy Authentication Requested",
|
||||||
|
REQUEST_TIMEOUT: "Request Timeout",
|
||||||
|
CONFLICT: "Conflict",
|
||||||
|
GONE: "Gone",
|
||||||
|
LENGTH_REQUIRED: "Length Required",
|
||||||
|
PRECONDITION_FAILED: "Precondition Failed",
|
||||||
|
PAYLOAD_TOO_LARGE: "Payload Too Large",
|
||||||
|
URI_TOO_LONG: "URI Too long",
|
||||||
|
UNSUPPORTED_MEDIA_TYPE: "Unsupported Media Type",
|
||||||
|
RANGE_NOT_SATISFIABLE: "Range Not Satisfiable",
|
||||||
|
EXPECTATION_FAILED: "Expectation Failed",
|
||||||
|
IM_A_TEAPOT: "I'm A Teapot",
|
||||||
|
MISDIRECTED_REQUEST: "Misdirected Request",
|
||||||
|
UNPROCESSABLE_ENTITY: "Unprocessable Entity",
|
||||||
|
LOCKED: "Locked",
|
||||||
|
FAILED_DEPENDENCY: "Failed Dependency",
|
||||||
|
TOO_EARLY: "Too Early",
|
||||||
|
UPGRADE_REQUIRED: "Upgrade Required",
|
||||||
|
PRECONDITION_REQUIRED: "Precondition Required",
|
||||||
|
TOO_MANY_REQUESTS: "Too Many Requests",
|
||||||
|
REQUEST_HEADER_FIELDS_TOO_LARGE: "Request Header Fields Too Large",
|
||||||
|
UNAVAILABLE_FOR_LEGAL_REASONS: "Unavailable For Legal Reasons",
|
||||||
|
INTERNAL_SERVER_ERROR: "Internal Server Error",
|
||||||
|
NOT_IMPLEMENTED: "Not Implemented",
|
||||||
|
BAD_GATEWAY: "Bad Gateway",
|
||||||
|
SERVICE_UNAVAILABLE: "Service Unavailable",
|
||||||
|
GATEWAY_TIMEOUT: "Gateway Timeout",
|
||||||
|
HTTP_VERSION_NOT_SUPPORTED: "HTTP Version Not Supported",
|
||||||
|
VARIANT_ALSO_NEGOTIATES: "Variant Also Negotiates",
|
||||||
|
INSUFFICIENT_STORAGE: "Insufficient Storage",
|
||||||
|
LOOP_DETECTED: "Loop detected",
|
||||||
|
NOT_EXTENDED: "Not Extended",
|
||||||
|
NETWORK_AUTHENTICATION_REQUIRED: "Network Authentication Required",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Public methods
|
||||||
|
|
||||||
|
static func code_to_description(code: int) -> String:
|
||||||
|
return __DESCRIPTIONS[code]
|
||||||
|
|
||||||
|
|
||||||
|
static func code_to_identifier(code: int) -> String:
|
||||||
|
return code_to_description(code).to_upper().replace(" ", "_").replace("'", "")
|
||||||
|
|
||||||
|
|
||||||
|
static func code_to_status_line(code: int) -> String:
|
||||||
|
return "HTTP/1.1 %d %s" % [code, code_to_identifier(code)]
|
||||||
|
|
@ -8,11 +8,37 @@
|
|||||||
|
|
||||||
config_version=4
|
config_version=4
|
||||||
|
|
||||||
|
_global_script_classes=[ {
|
||||||
|
"base": "TCP_Server",
|
||||||
|
"class": "HTTPServer",
|
||||||
|
"language": "GDScript",
|
||||||
|
"path": "res://addons/http_server/http_server.gd"
|
||||||
|
}, {
|
||||||
|
"base": "Reference",
|
||||||
|
"class": "MimeTypeHelper",
|
||||||
|
"language": "GDScript",
|
||||||
|
"path": "res://addons/http_server/mimetypes.gd"
|
||||||
|
} ]
|
||||||
|
_global_script_class_icons={
|
||||||
|
"HTTPServer": "",
|
||||||
|
"MimeTypeHelper": ""
|
||||||
|
}
|
||||||
|
|
||||||
[application]
|
[application]
|
||||||
|
|
||||||
config/name="Ticle Frontend"
|
config/name="Tickle Frontend"
|
||||||
|
run/main_scene="res://Main.tscn"
|
||||||
config/icon="res://icon.png"
|
config/icon="res://icon.png"
|
||||||
|
|
||||||
|
[display]
|
||||||
|
|
||||||
|
window/size/width=1280
|
||||||
|
window/size/height=720
|
||||||
|
|
||||||
|
[editor_plugins]
|
||||||
|
|
||||||
|
enabled=PoolStringArray( "res://addons/http_server/plugin.cfg" )
|
||||||
|
|
||||||
[gui]
|
[gui]
|
||||||
|
|
||||||
common/drop_mouse_on_gui_input_disabled=true
|
common/drop_mouse_on_gui_input_disabled=true
|
||||||
|
BIN
server_files/dogpepsi.jpg
Normal file
BIN
server_files/dogpepsi.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
35
server_files/dogpepsi.jpg.import
Normal file
35
server_files/dogpepsi.jpg.import
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="StreamTexture"
|
||||||
|
path="res://.import/dogpepsi.jpg-600a9f60613039ee9dabc11447d355f1.stex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://server_files/dogpepsi.jpg"
|
||||||
|
dest_files=[ "res://.import/dogpepsi.jpg-600a9f60613039ee9dabc11447d355f1.stex" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_mode=0
|
||||||
|
compress/bptc_ldr=0
|
||||||
|
compress/normal_map=0
|
||||||
|
flags/repeat=0
|
||||||
|
flags/filter=true
|
||||||
|
flags/mipmaps=false
|
||||||
|
flags/anisotropic=false
|
||||||
|
flags/srgb=2
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/HDR_as_SRGB=false
|
||||||
|
process/invert_color=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
stream=false
|
||||||
|
size_limit=0
|
||||||
|
detect_3d=true
|
||||||
|
svg/scale=1.0
|
2
server_files/files.txt
Normal file
2
server_files/files.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
first.md 2022-03-03 first title
|
||||||
|
second.md 2022-03-05 second title
|
47
server_files/first.md
Normal file
47
server_files/first.md
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
# Importance of Second Opinions
|
||||||
|
|
||||||
|
|
||||||
|
A Mayo Clinic study found that
|
||||||
|
Only 1/10 cases of patients seeking second opinions
|
||||||
|
received confirmation the first diagnosis was complete and correct
|
||||||
|
2/10 cases received a "distinctly different" diagnosis
|
||||||
|
|
||||||
|
Yale Medecine recommends to seek a second opinion
|
||||||
|
when the diagnosis is cancer
|
||||||
|
or
|
||||||
|
when surgery is recommended
|
||||||
|
|
||||||
|
Please tell your loved ones to seek 2nd opinions!
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
First Opinions are often bad:
|
||||||
|
|
||||||
|
- https://newsnetwork.mayoclinic.org/discussion/mayo-clinic-researchers-demonstrate-value-of-second-opinions/
|
||||||
|
- https://newsnetwork.mayoclinic.org/discussion/mayo-clinic-researchers-demonstrate-value-of-second-opinions/
|
||||||
|
|
||||||
|
|
||||||
|
## Key points:
|
||||||
|
|
||||||
|
> The Mayo Clinic study found that as many as
|
||||||
|
> - 9 out of 10 patients seeking a second opinion go home with a new or refined diagnosis.
|
||||||
|
> - 2 out of ten received a “distinctly different” diagnosis.
|
||||||
|
>- only 1 out of 10 referred patients receive confirmation that the original
|
||||||
|
> diagnosis was complete and correct.
|
||||||
|
> This is _out of patients seeking a 2nd opinion_, not patients in general
|
||||||
|
|
||||||
|
Paper referenced by articles found at: https://onlinelibrary.wiley.com/doi/abs/10.1111/jep.12747
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
When to Seek 2nd Opinions according to Yale
|
||||||
|
|
||||||
|
- https://www.yalemedicine.org/news/second-opinions
|
||||||
|
|
||||||
|
Key points:
|
||||||
|
|
||||||
|
> Seek 2nd opinion:
|
||||||
|
> - When the diagnosis is cancer
|
||||||
|
> - **When surgery is recommended**
|
||||||
|
|
||||||
|
-----
|
248
server_files/index.html
Normal file
248
server_files/index.html
Normal file
@ -0,0 +1,248 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Tickle</title>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Lobster&family=Nunito+Sans:ital,wght@0,400;0,700;1,400&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--accent: hotpink;
|
||||||
|
--background: #fdfdfd;
|
||||||
|
font-family: "Nunito Sans", sans-serif;
|
||||||
|
}
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.trigger {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
.left-nav > * {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
.left-nav > .menu {
|
||||||
|
padding: 1em;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
transition: all 0.3s ease-out;
|
||||||
|
gap: 1em;
|
||||||
|
}
|
||||||
|
.left-nav > .menu a {
|
||||||
|
text-decoration: none;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--accent);
|
||||||
|
box-shadow: rgba(100, 100, 111, 0.2) 0px 7px 29px 0px;
|
||||||
|
padding: 0.2em 0.4em;
|
||||||
|
}
|
||||||
|
.left-nav > .menu a:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
|
.left-nav > .trigger:checked ~ .menu {
|
||||||
|
transform: translateX(0);
|
||||||
|
transition-duration: 0.1s;
|
||||||
|
}
|
||||||
|
.burger {
|
||||||
|
width: 2em;
|
||||||
|
height: 2em;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-content: center;
|
||||||
|
text-align: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
line-height: 2em;
|
||||||
|
border-radius: 0.2em;
|
||||||
|
box-shadow: rgba(149, 157, 165, 0.2) 0px 8px 24px;
|
||||||
|
transition: background 0.5s ease;
|
||||||
|
}
|
||||||
|
.left-nav > .trigger:checked ~ .burger {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left-nav > .trigger:checked ~ .burger,
|
||||||
|
#Loading {
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
background: rgba(17, 17, 17, 0.2);
|
||||||
|
}
|
||||||
|
#Loading {
|
||||||
|
position: absolute;
|
||||||
|
text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 8rem;
|
||||||
|
color: var(--accent);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease-in, height 0s linear 0.3s;
|
||||||
|
height: 0;
|
||||||
|
display: flex;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.is-loading #Loading {
|
||||||
|
opacity: 1;
|
||||||
|
height: 100%;
|
||||||
|
transition: opacity 1s ease-in, height 0 linear;
|
||||||
|
}
|
||||||
|
#Loading::after {
|
||||||
|
content: "❤";
|
||||||
|
animation: beat 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
}
|
||||||
|
@keyframes beat {
|
||||||
|
0% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
5% {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
39% {
|
||||||
|
transform: scale(0.85);
|
||||||
|
}
|
||||||
|
45% {
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
60% {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: scale(0.9);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#Body {
|
||||||
|
padding: 3em;
|
||||||
|
max-width: 52em;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
#Body h1,
|
||||||
|
#Body h2,
|
||||||
|
#Body h3,
|
||||||
|
#Body h4 {
|
||||||
|
color: var(--accent);
|
||||||
|
font-family: "Lobster", serif;
|
||||||
|
}
|
||||||
|
#Body a {
|
||||||
|
color: var(--background);
|
||||||
|
background-color: var(--accent);
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0.1em 0.4em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<nav class="left-nav">
|
||||||
|
<input id="main-nav" type="checkbox" class="trigger" />
|
||||||
|
<label for="main-nav" class="burger">≡</label>
|
||||||
|
<div id="Menu" class="menu"></div>
|
||||||
|
</nav>
|
||||||
|
<header id="Menu"></header>
|
||||||
|
<main id="Body"></main>
|
||||||
|
<div id="Loading"></div>
|
||||||
|
<script type="module">
|
||||||
|
/**
|
||||||
|
* markdown parser. Remove if you don't use markdown
|
||||||
|
*/
|
||||||
|
// @ts-ignore
|
||||||
|
import { micromark } from "https://esm.sh/micromark@3?bundle";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useful to check for transitions while developing styles, if the loading screen disappears too fast
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const wait = (val) => new Promise((ok) => setTimeout(ok, 1, val));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The elements we will need
|
||||||
|
*/
|
||||||
|
const [Menu, Body, Loading] = ["Menu", "Body", "Loading"].map((id) =>
|
||||||
|
document.getElementById(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cache the original title to append it to the page title
|
||||||
|
*/
|
||||||
|
const mainTitle = document.title;
|
||||||
|
|
||||||
|
const startLoading = () => document.body.classList.add("is-loading");
|
||||||
|
const stopLoading = () => document.body.classList.remove("is-loading");
|
||||||
|
|
||||||
|
const getCurrentPage = () =>
|
||||||
|
window.location.hash[1] === "/" ? window.location.hash.slice(2) : "";
|
||||||
|
|
||||||
|
const onHashChange = (evt) => {
|
||||||
|
const path = getCurrentPage();
|
||||||
|
if (!path) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
startLoading();
|
||||||
|
return fetch(`./${path}`)
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then(wait)
|
||||||
|
.then((text) => {
|
||||||
|
const [, title] = text.match(/^(#\s\w+)/) ||
|
||||||
|
text.match(/(.*?)\n===+/m) || [, path];
|
||||||
|
document.title = `${title} | ${mainTitle}`;
|
||||||
|
Body.innerHTML = micromark(text);
|
||||||
|
stopLoading();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener("hashchange", onHashChange);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the article list, parses it, creates the menu items
|
||||||
|
*/
|
||||||
|
const start = () => {
|
||||||
|
startLoading();
|
||||||
|
fetch("./files.txt")
|
||||||
|
.then((response) => response.text())
|
||||||
|
.then((lines) => {
|
||||||
|
Menu.innerHTML = lines
|
||||||
|
.split(`\n`)
|
||||||
|
.map((line, index) => {
|
||||||
|
const today = new Date().toISOString().split("T")[0];
|
||||||
|
const result = line.match(
|
||||||
|
/(?<name>.+)\.(?<ext>\w{2,3})(?:\s(?<date>[\d-]+)?(?<title>.+))?/
|
||||||
|
);
|
||||||
|
if (!result) {
|
||||||
|
console.log(`could not parse line ${index}: [${line}]`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
groups: { name, ext, date = today, title = name },
|
||||||
|
} = result;
|
||||||
|
const href = `/${name}.${ext}`;
|
||||||
|
if (!getCurrentPage()) {
|
||||||
|
window.location.hash = `#${href}`;
|
||||||
|
}
|
||||||
|
return { name, href, date, title };
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort(({ date: a }, { date: b }) => a - b)
|
||||||
|
.map(
|
||||||
|
({ href, title }) => `<a data-link href="#${href}">${title}</a>`
|
||||||
|
)
|
||||||
|
.join(`\n`);
|
||||||
|
|
||||||
|
onHashChange();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
start();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
server_files/index.md
Normal file
1
server_files/index.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
# Title
|
BIN
server_files/picture.png
Normal file
BIN
server_files/picture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.0 MiB |
35
server_files/picture.png.import
Normal file
35
server_files/picture.png.import
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="texture"
|
||||||
|
type="StreamTexture"
|
||||||
|
path="res://.import/picture.png-f7b364942b19109d5768d9400167481e.stex"
|
||||||
|
metadata={
|
||||||
|
"vram_texture": false
|
||||||
|
}
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://server_files/picture.png"
|
||||||
|
dest_files=[ "res://.import/picture.png-f7b364942b19109d5768d9400167481e.stex" ]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
compress/mode=0
|
||||||
|
compress/lossy_quality=0.7
|
||||||
|
compress/hdr_mode=0
|
||||||
|
compress/bptc_ldr=0
|
||||||
|
compress/normal_map=0
|
||||||
|
flags/repeat=0
|
||||||
|
flags/filter=true
|
||||||
|
flags/mipmaps=false
|
||||||
|
flags/anisotropic=false
|
||||||
|
flags/srgb=2
|
||||||
|
process/fix_alpha_border=true
|
||||||
|
process/premult_alpha=false
|
||||||
|
process/HDR_as_SRGB=false
|
||||||
|
process/invert_color=false
|
||||||
|
process/normal_map_invert_y=false
|
||||||
|
stream=false
|
||||||
|
size_limit=0
|
||||||
|
detect_3d=true
|
||||||
|
svg/scale=1.0
|
20
server_files/second.md
Normal file
20
server_files/second.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Testing markdown
|
||||||
|
|
||||||
|
This is a test of **markdown** parsing. Again. Because why not?
|
||||||
|
|
||||||
|
Why:
|
||||||
|
|
||||||
|
1. Item1
|
||||||
|
2. Item2
|
||||||
|
|
||||||
|
> Blockquote
|
||||||
|
|
||||||
|
Image:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
```
|
||||||
|
code_block!
|
||||||
|
```
|
||||||
|
|
||||||
|
`code not block`
|
Reference in New Issue
Block a user