Compare commits

...

47 Commits

Author SHA1 Message Date
3437aa0abd Merge branch 'main' into theme 2022-08-23 09:20:42 +00:00
e80043bd6b change font to Fira
remove erroneous vscode folder
2022-08-23 12:19:15 +03:00
c0759f0cbe add vscode folder to gitignore 2022-08-23 11:47:34 +03:00
d7b1645943 change the clear color to be darker 2022-07-18 23:16:53 +03:00
4076dbc251 add more controls to theme 2022-07-18 22:58:25 +03:00
47dc68028c Merge pull request 'make the endpoints at request time' (#9) from serve-on-request into main
Reviewed-on: #9
2022-07-04 20:04:06 +00:00
e44cac165a sync http-server with upstream and properly check for endpoints 2022-07-04 06:44:00 +03:00
b1a7b736b4 add theme
currently supported controls: Button, LineEdit, TextEdit, and VScrollBar.
2022-07-04 04:10:20 +03:00
3d2fd37191 make the endpoints at request time 2022-07-03 23:37:47 +03:00
836c101e6f properly update and use server_dir from ServerUI 2022-07-02 13:41:15 +03:00
8cbfffba94 Merge pull request 'add proper image handling to http server' (#7) from utf8-test into main
Reviewed-on: #7
2022-07-02 10:31:17 +00:00
7280277029 add proper image handling to http server 2022-07-02 02:11:29 +03:00
a203972157 update readme (#2) 2022-06-29 17:54:45 +03:00
7f7018f183 hide content preview container for now 2022-06-29 17:42:25 +03:00
44cfe464c5 change project title (#2) 2022-06-29 17:38:02 +03:00
df2dc1097e clean current file and tree item references when regenerating tree 2022-06-29 17:27:16 +03:00
68b4a03a3f implement refresh button 2022-06-29 16:53:46 +03:00
815aae5a4d implement open in browser button 2022-06-29 16:22:53 +03:00
6a98956b03 update to godot 3.5rc5 2022-06-29 16:15:46 +03:00
0602345af9 remove warnings in ServerUI 2022-06-29 16:14:14 +03:00
c8d19197e6 add newline to files.txt after stripping right edge 2022-06-29 14:02:37 +03:00
45cf485d9d disable file tree context menu while server is running 2022-06-29 13:55:44 +03:00
741e23af56 strip right edge when making files.txt 2022-06-29 13:51:46 +03:00
2284a1d1dd remove conditional when disabling nodes 2022-06-29 13:44:52 +03:00
d309c40fc3 disable file tree editing while server is running 2022-06-29 13:37:03 +03:00
fc1ca1e1b0 format MimeTypeHelper 2022-06-29 13:36:41 +03:00
12090f7930 improve file reading in ServerUI, remove dirty flag from FileDef 2022-06-25 20:29:18 +03:00
6dd063382c fixes #4 2022-06-25 20:21:50 +03:00
550a1c80a4 add server start functionality to ServerUI 2022-06-25 19:37:09 +03:00
80bc3e4a11 Merge pull request 'mimetypes' (#1) from mimetypes into main
Reviewed-on: yagich/ticle-godot-frontend#1
2022-06-25 14:45:50 +00:00
791743b765 Merge branch 'main' into mimetypes 2022-06-25 14:40:49 +00:00
e337f78740 remove redundant get_mime_type() function 2022-06-25 17:37:26 +03:00
514ba20c41 update Main scene file to instance the new ServerUI scene 2022-06-25 12:20:27 +03:00
003eb04013 improvement: use mimetype class & get rid of warnings 2022-06-25 10:56:45 +02:00
5380b59ee2 improvement: add a mimetype class 2022-06-25 10:56:00 +02:00
eeb6744947 add optional autosave to file content 2022-06-23 21:58:18 +03:00
d3764d3c4a add files.txt generation function 2022-06-23 21:35:07 +03:00
9fae276011 make sure tree items can't be moved out of bounds 2022-06-23 19:52:32 +03:00
c3e0bd043f add file reordering context menu 2022-06-23 19:52:32 +03:00
a6266ef98b add UI script, forgot to commit woopsie 2022-06-23 19:52:32 +03:00
e7d309efae add godot version disclaimer 2022-06-23 19:52:32 +03:00
9ba719e8d3 add initial UI 2022-06-21 18:23:15 +03:00
fe8aa24ca8 update ticle 2022-06-21 17:07:23 +03:00
6478f54689 add initial server implementation 2022-06-21 15:48:14 +03:00
ec63dde2b0 add main html and test files 2022-06-21 15:47:39 +03:00
01835ec2d7 add godot-http-server 2022-06-20 14:33:14 +03:00
a1ffe4d4ba add godot-http-server credit 2022-06-20 14:27:00 +03:00
53 changed files with 10841 additions and 4 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ export_presets.cfg
# Mono-specific ignores
.mono/
data_*/
# VSCode config folder
.vscode/

77
Main.gd Normal file
View 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

17
Main.tscn Normal file
View File

@ -0,0 +1,17 @@
[gd_scene load_steps=4 format=2]
[ext_resource path="res://Main.gd" type="Script" id=1]
[ext_resource path="res://ServerUI.tscn" type="PackedScene" id=2]
[ext_resource path="res://theme/default_theme.theme" type="Theme" id=3]
[node name="Main" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
theme = ExtResource( 3 )
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"]

View File

@ -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
View 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
View 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 = 42.0
alignment = 1
[node name="Label" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_left = 95.0
margin_top = 12.0
margin_right = 189.0
margin_bottom = 29.0
text = "Server folder:"
[node name="ServerPathLabel" type="Label" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 193.0
margin_top = 12.0
margin_right = 446.0
margin_bottom = 29.0
text = "/home/username/long/path/to/server"
[node name="OpenServerFolderButton" type="Button" parent="VBoxContainer/HBoxContainer"]
margin_left = 450.0
margin_right = 517.0
margin_bottom = 42.0
rect_min_size = Vector2( 0, 32 )
text = "Open..."
[node name="Label2" type="Label" parent="VBoxContainer/HBoxContainer"]
margin_left = 521.0
margin_top = 12.0
margin_right = 603.0
margin_bottom = 29.0
text = "Server port:"
[node name="PortSpinBox" type="SpinBox" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 607.0
margin_right = 687.0
margin_bottom = 42.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 = 691.0
margin_right = 792.0
margin_bottom = 42.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 = 796.0
margin_top = 12.0
margin_right = 947.0
margin_bottom = 29.0
text = "Server is not running."
[node name="OpenBrowserButton" type="Button" parent="VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 951.0
margin_right = 1086.0
margin_bottom = 42.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 = 51.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 = 560.0
size_flags_horizontal = 3
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer2"]
margin_right = 322.0
margin_bottom = 40.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
margin_top = 11.0
margin_right = 34.0
margin_bottom = 28.0
size_flags_horizontal = 0
text = "Files"
[node name="RefreshFilesButton" type="Button" parent="VBoxContainer/HSplitContainer/VBoxContainer2/HBoxContainer2"]
unique_name_in_owner = true
margin_left = 251.0
margin_right = 322.0
margin_bottom = 40.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 = 44.0
margin_right = 322.0
margin_bottom = 560.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 = 560.0
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer"]
margin_right = 848.0
margin_bottom = 560.0
size_flags_vertical = 3
[node name="VBoxContainer" type="VBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer"]
margin_right = 848.0
margin_bottom = 560.0
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_right = 848.0
margin_bottom = 42.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
margin_top = 12.0
margin_right = 34.0
margin_bottom = 29.0
text = "Title:"
[node name="DocTitleLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer"]
unique_name_in_owner = true
margin_left = 38.0
margin_right = 848.0
margin_bottom = 42.0
size_flags_horizontal = 3
placeholder_text = "(Optional)"
[node name="HBoxContainer2" type="HBoxContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_top = 46.0
margin_right = 848.0
margin_bottom = 88.0
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
margin_top = 12.0
margin_right = 37.0
margin_bottom = 29.0
text = "Date:"
[node name="DocDateLineEdit" type="LineEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
margin_left = 41.0
margin_right = 848.0
margin_bottom = 42.0
size_flags_horizontal = 3
placeholder_text = "(Optional)"
[node name="HSplitContainer" type="HSplitContainer" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer"]
margin_top = 92.0
margin_right = 848.0
margin_bottom = 560.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 = 468.0
size_flags_vertical = 3
[node name="Label" type="Label" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
margin_right = 848.0
margin_bottom = 17.0
text = "Content"
[node name="DocInputTextEdit" type="TextEdit" parent="VBoxContainer/HSplitContainer/VBoxContainer/HBoxContainer/VBoxContainer/HSplitContainer/ContentEditContainer"]
unique_name_in_owner = true
margin_top = 21.0
margin_right = 848.0
margin_bottom = 468.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"]

View 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

View 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()

File diff suppressed because it is too large Load Diff

View 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"

View File

@ -0,0 +1,10 @@
tool
extends EditorPlugin
func _enter_tree() -> void:
pass
func _exit_tree() -> void:
pass

View 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

View 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()

View 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)]

View File

@ -8,14 +8,42 @@
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]
config/name="Ticle Frontend"
config/name="Tickle Frontend"
run/main_scene="res://Main.tscn"
boot_splash/bg_color=Color( 0.141176, 0.141176, 0.141176, 1 )
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]
common/drop_mouse_on_gui_input_disabled=true
theme/custom="res://theme/default_theme.theme"
[physics]
@ -23,4 +51,5 @@ common/enable_pause_aware_picking=true
[rendering]
environment/default_clear_color=Color( 0.1023, 0.11, 0.102942, 1 )
environment/default_environment="res://default_env.tres"

BIN
server_files/dogpepsi.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View 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
View 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
View 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
View 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">&#8801;</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
View File

@ -0,0 +1 @@
# Title

BIN
server_files/picture.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View 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
View 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:
![alt text](picture.png)
```
code_block!
```
`code not block`

Binary file not shown.

BIN
theme/FiraSans-Light.otf Normal file

Binary file not shown.

BIN
theme/FiraSans-Regular.otf Normal file

Binary file not shown.

6
theme/FontRg.tres Normal file
View File

@ -0,0 +1,6 @@
[gd_resource type="DynamicFont" load_steps=2 format=2]
[ext_resource path="res://theme/FiraSans-Regular.otf" type="DynamicFontData" id=1]
[resource]
font_data = ExtResource( 1 )

7
theme/FontWriter.tres Normal file
View File

@ -0,0 +1,7 @@
[gd_resource type="DynamicFont" load_steps=2 format=2]
[ext_resource path="res://theme/FiraSans-Light.otf" type="DynamicFontData" id=1]
[resource]
size = 18
font_data = ExtResource( 1 )

BIN
theme/FreeMono.ttf Normal file

Binary file not shown.

BIN
theme/FreeMonoBold.ttf Normal file

Binary file not shown.

BIN
theme/FreeSans.ttf Normal file

Binary file not shown.

BIN
theme/default_theme.theme Normal file

Binary file not shown.

View File

@ -0,0 +1,14 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 10.0
bg_color = Color( 0.168, 0.192, 0.2, 1 )
border_width_bottom = 3
border_color = Color( 0.2788, 0.41, 0.296293, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,17 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 10.0
bg_color = Color( 0.0705882, 0.164706, 0.196078, 1 )
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 5
border_color = Color( 0.164706, 0.85098, 0.254902, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,14 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 10.0
bg_color = Color( 0.0705882, 0.164706, 0.196078, 1 )
border_width_bottom = 3
border_color = Color( 0.164706, 0.85098, 0.254902, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,17 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 10.0
bg_color = Color( 0.0705882, 0.164706, 0.196078, 1 )
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color( 0.164706, 0.85098, 0.254902, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,17 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 12.0
bg_color = Color( 0.0705882, 0.164706, 0.196078, 1 )
border_width_left = 2
border_width_top = 3
border_width_right = 3
border_width_bottom = 6
border_color = Color( 0.164706, 0.85098, 0.254902, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,17 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
content_margin_left = 8.0
content_margin_right = 8.0
content_margin_top = 10.0
content_margin_bottom = 10.0
bg_color = Color( 0.168, 0.192, 0.2, 1 )
border_width_left = 2
border_width_top = 3
border_width_right = 3
border_width_bottom = 6
border_color = Color( 0.2788, 0.41, 0.296293, 1 )
corner_radius_top_left = 4
corner_radius_top_right = 4
corner_radius_bottom_right = 4
corner_radius_bottom_left = 4

View File

@ -0,0 +1,8 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
bg_color = Color( 0.615686, 0.921569, 0.658824, 1 )
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12

View File

@ -0,0 +1,8 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
bg_color = Color( 0.1764, 0.63, 0.23688, 1 )
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12

View File

@ -0,0 +1,8 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
bg_color = Color( 0.164706, 0.85098, 0.254902, 1 )
corner_radius_top_left = 12
corner_radius_top_right = 12
corner_radius_bottom_right = 12
corner_radius_bottom_left = 12

View File

@ -0,0 +1,13 @@
[gd_resource type="StyleBoxFlat" format=2]
[resource]
bg_color = Color( 0.168627, 0.192157, 0.2, 0 )
border_width_left = 3
border_width_top = 4
border_width_right = 3
border_width_bottom = 4
border_color = Color( 0.168627, 0.192157, 0.2, 0 )
corner_radius_top_left = 10
corner_radius_top_right = 10
corner_radius_bottom_right = 10
corner_radius_bottom_left = 10

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="320.47662mm"
height="317.19357mm"
viewBox="0 0 320.47662 317.19357"
version="1.1"
id="svg5"
inkscape:version="1.2 (1:1.2.1+202207142221+cd75a1ee6d)"
sodipodi:docname="checkmark.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#505050"
bordercolor="#eeeeee"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#505050"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="0.56"
inkscape:cx="353.57143"
inkscape:cy="677.67857"
inkscape:window-width="1920"
inkscape:window-height="1016"
inkscape:window-x="1440"
inkscape:window-y="255"
inkscape:window-maximized="1"
inkscape:current-layer="layer3" />
<defs
id="defs2" />
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="enabled-unchecked"
style="display:inline"
transform="translate(21.187991,13.079903)">
<rect
style="display:inline;fill:#122a32;fill-opacity:1;stroke:#2ad941;stroke-width:12;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="rect8653"
width="141.06651"
height="141.06651"
x="-15.187991"
y="-7.0799026"
ry="16"
rx="16"
inkscape:label="enabled-unchecked" />
</g>
<g
inkscape:groupmode="layer"
id="layer3"
inkscape:label="enabled-checked"
style="display:inline"
transform="translate(181.23232,167.12706)">
<g
id="g1009">
<rect
style="fill:#122a32;fill-opacity:1;stroke:#2ad941;stroke-width:12;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="rect61"
width="141.06651"
height="141.06651"
x="-7.8222208"
y="3.0000012"
ry="16"
rx="16" />
<path
style="fill:none;stroke:#2ad941;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 16.639226,54.462173 54.694346,120.37558 108.78285,26.691533"
id="path3909" />
</g>
</g>
<g
inkscape:groupmode="layer"
id="layer4"
inkscape:label="disabled-unchecked"
style="display:inline"
transform="translate(21.148772,161.05655)">
<rect
style="display:inline;fill:#2b3133;fill-opacity:1;stroke:#47694c;stroke-width:12;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="rect4965"
width="141.06651"
height="141.06651"
x="152.26134"
y="-154.46455"
ry="16"
rx="16"
inkscape:label="disabled-unchecked" />
</g>
<g
inkscape:groupmode="layer"
id="layer5"
inkscape:label="disabled-checked"
style="display:inline"
transform="translate(21.187991,13.079903)">
<g
id="g988">
<rect
style="fill:#2b3133;fill-opacity:1;stroke:#47694c;stroke-width:12;stroke-linecap:square;stroke-dasharray:none;stroke-opacity:1;stop-color:#000000"
id="rect6419"
width="141.06651"
height="141.06651"
x="-15.187979"
y="157.04716"
ry="16"
rx="16" />
<path
style="fill:none;stroke:#47694c;stroke-width:12;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="M 9.27349,208.50934 47.32861,274.42275 101.4171,180.7387"
id="path6421" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/checkmark.svg-c593fbe7a89846c0bcb217f6ec227e9b.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://theme/textures/checkmark.svg"
dest_files=[ "res://.import/checkmark.svg-c593fbe7a89846c0bcb217f6ec227e9b.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 714 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/checkmark_disabled-checked.png-5aba26a5162a237fec800f8930559eb4.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://theme/textures/checkmark_disabled-checked.png"
dest_files=[ "res://.import/checkmark_disabled-checked.png-5aba26a5162a237fec800f8930559eb4.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/checkmark_disabled-unchecked.png-8bfdd3103b550628b3014ec8cdc80614.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://theme/textures/checkmark_disabled-unchecked.png"
dest_files=[ "res://.import/checkmark_disabled-unchecked.png-8bfdd3103b550628b3014ec8cdc80614.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 771 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/checkmark_enabled-checked.png-625146d8028545daff92e68e0d2b234e.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://theme/textures/checkmark_enabled-checked.png"
dest_files=[ "res://.import/checkmark_enabled-checked.png-625146d8028545daff92e68e0d2b234e.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

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 B

View File

@ -0,0 +1,35 @@
[remap]
importer="texture"
type="StreamTexture"
path="res://.import/checkmark_enabled-unchecked.png-d280edb9026f565cdf11b0af8d45d426.stex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://theme/textures/checkmark_enabled-unchecked.png"
dest_files=[ "res://.import/checkmark_enabled-unchecked.png-d280edb9026f565cdf11b0af8d45d426.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