Compare commits
43 Commits
6478f54689
...
theme
Author | SHA1 | Date | |
---|---|---|---|
3437aa0abd | |||
e80043bd6b
|
|||
c0759f0cbe
|
|||
d7b1645943
|
|||
4076dbc251
|
|||
47dc68028c | |||
e44cac165a
|
|||
b1a7b736b4
|
|||
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 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -9,3 +9,6 @@ export_presets.cfg
|
||||
# Mono-specific ignores
|
||||
.mono/
|
||||
data_*/
|
||||
|
||||
# VSCode config folder
|
||||
.vscode/
|
||||
|
86
Main.gd
86
Main.gd
@ -1,23 +1,15 @@
|
||||
extends Control
|
||||
|
||||
const mime_types: Dictionary = {
|
||||
"html": "text/html",
|
||||
"htm": "text/html",
|
||||
"md": "text/plain",
|
||||
"css": "text/css",
|
||||
"txt": "text/plain",
|
||||
"png": "image/png",
|
||||
"jpg": "image/jpeg",
|
||||
"jpeg": "image/jpeg",
|
||||
}
|
||||
var mime_types := MimeTypeHelper.generate_db()
|
||||
|
||||
var _server: HTTPServer = null
|
||||
|
||||
var files: Array = []
|
||||
onready var server_ui := $ServerUI
|
||||
onready var server_dir = server_ui.server_dir
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_start_server()
|
||||
pass
|
||||
|
||||
|
||||
func _start_server(port: int = 3001) -> void:
|
||||
@ -25,57 +17,61 @@ func _start_server(port: int = 3001) -> void:
|
||||
return
|
||||
|
||||
_server = HTTPServer.new()
|
||||
var dir := Directory.new()
|
||||
if dir.open("res://server_files") == OK:
|
||||
dir.list_dir_begin()
|
||||
var file_name := dir.get_next()
|
||||
while file_name != "":
|
||||
if !dir.current_is_dir():
|
||||
if file_name.get_extension() == "import":
|
||||
file_name = dir.get_next()
|
||||
continue
|
||||
_server.endpoint(HTTPServer.Method.GET, "/", funcref(self, "_serve_file"))
|
||||
|
||||
print(file_name)
|
||||
if _server.listen(port) != OK:
|
||||
# TODO: show error to user here
|
||||
return
|
||||
|
||||
_server.endpoint(HTTPServer.Method.GET, "/%s" % file_name, funcref(self, "_serve_file"), [file_name])
|
||||
|
||||
file_name = dir.get_next()
|
||||
|
||||
_server.endpoint(HTTPServer.Method.GET, "/", funcref(self, "_serve_file"), ["index.html"])
|
||||
|
||||
|
||||
_server.listen(port)
|
||||
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
|
||||
|
||||
_server.take_connection()
|
||||
if not _server.take_connection():
|
||||
# TODO: show error to user here
|
||||
return
|
||||
|
||||
|
||||
func _serve_file(request: HTTPServer.Request, response: HTTPServer.Response, binds: Array) -> void:
|
||||
var file_name: String = binds[0] as String
|
||||
print(file_name)
|
||||
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)
|
||||
|
||||
var f = File.new()
|
||||
f.open("res://server_files/%s" % 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 mime = get_mime_type(file_name)
|
||||
var data = f.get_buffer(f.get_len())
|
||||
|
||||
response.header("content-type", get_mime_type(file_name))
|
||||
response.data(data)
|
||||
|
||||
response.data(f.get_as_text())
|
||||
# else:
|
||||
# response.header("content-type", "text/plain")
|
||||
# response.data("500 - Read Error")
|
||||
else:
|
||||
response.type(mime_types.get("txt"))
|
||||
response.status(500)
|
||||
response.data("Internal Server Error")
|
||||
|
||||
|
||||
func get_mime_type(file_name: String) -> String:
|
||||
var ext := file_name.get_extension().to_lower()
|
||||
return mime_types[ext] if ext in mime_types else "application/octet-stream"
|
||||
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
|
||||
|
11
Main.tscn
11
Main.tscn
@ -1,8 +1,17 @@
|
||||
[gd_scene load_steps=2 format=2]
|
||||
[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"]
|
||||
|
@ -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 = 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"]
|
@ -13,7 +13,7 @@ const Status = preload("res://addons/http_server/status.gd")
|
||||
|
||||
var __endpoints: Dictionary = {
|
||||
# key: [Int, String], array with 0 index representing method, 1 index representing endpoint
|
||||
# value: [FuncRef, Array], index 0 = reference to function to call, index 1 = binds to pass to func
|
||||
# value: FuncRef, reference to function to call
|
||||
}
|
||||
var __fallback: FuncRef = null
|
||||
var __server: TCP_Server = null
|
||||
@ -32,7 +32,7 @@ func endpoint(type: int, endpoint: String, function: FuncRef, binds: Array = [])
|
||||
)
|
||||
return
|
||||
|
||||
__endpoints[endpoint_hash] = [function, binds]
|
||||
__endpoints[endpoint_hash] = function
|
||||
|
||||
|
||||
func fallback(function: FuncRef) -> void:
|
||||
@ -106,7 +106,7 @@ func __process_connection(connection: StreamPeerTCP) -> void:
|
||||
body = PoolStringArray(body_parts).join("\r\n")
|
||||
|
||||
var response: Response = __process_request(method, endpoint, headers, body)
|
||||
connection.put_data(response.to_utf8())
|
||||
connection.put_data(response.get_data())
|
||||
|
||||
|
||||
func __process_request(method: String, endpoint: String, headers: Dictionary, body: String) -> Response:
|
||||
@ -121,22 +121,15 @@ func __process_request(method: String, endpoint: String, headers: Dictionary, bo
|
||||
|
||||
var endpoint_func: FuncRef = null
|
||||
var endpoint_parts: PoolStringArray = endpoint.split("/", false)
|
||||
var binds
|
||||
|
||||
# special case for if endpoint is just root
|
||||
if endpoint == "/":
|
||||
var endpoint_hash: Array = [type, "/"]
|
||||
while !endpoint_func:
|
||||
var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")]
|
||||
if __endpoints.has(endpoint_hash):
|
||||
endpoint_func = __endpoints[endpoint_hash][0]
|
||||
binds = __endpoints[endpoint_hash][1]
|
||||
else:
|
||||
while (!endpoint_func && !endpoint_parts.empty()):
|
||||
var endpoint_hash: Array = [type, "/" + endpoint_parts.join("/")]
|
||||
if __endpoints.has(endpoint_hash):
|
||||
endpoint_func = __endpoints[endpoint_hash][0]
|
||||
binds = __endpoints[endpoint_hash][1]
|
||||
else:
|
||||
endpoint_parts.remove(endpoint_parts.size() - 1)
|
||||
endpoint_func = __endpoints[endpoint_hash]
|
||||
elif endpoint_parts.empty():
|
||||
break
|
||||
else:
|
||||
endpoint_parts.remove(endpoint_parts.size() - 1)
|
||||
|
||||
|
||||
if !endpoint_func:
|
||||
@ -160,10 +153,7 @@ func __process_request(method: String, endpoint: String, headers: Dictionary, bo
|
||||
"[INF] Recieved request method: %s, endpoint: %s" % [method, endpoint]
|
||||
)
|
||||
|
||||
if !binds:
|
||||
endpoint_func.call_func(request, response)
|
||||
else:
|
||||
endpoint_func.call_func(request, response, binds)
|
||||
endpoint_func.call_func(request, response)
|
||||
|
||||
return response
|
||||
|
||||
|
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
@ -11,6 +11,7 @@ var __headers: Dictionary = {
|
||||
# value: Variant, header value
|
||||
}
|
||||
var __status: int = 200
|
||||
var __type: MimeTypeHelper.MimeType
|
||||
|
||||
|
||||
# Public methods
|
||||
@ -32,6 +33,10 @@ func status(status: int) -> void:
|
||||
__status = status
|
||||
|
||||
|
||||
func type(type: MimeTypeHelper.MimeType) -> void:
|
||||
__type = type
|
||||
|
||||
|
||||
func to_utf8() -> PoolByteArray:
|
||||
var content = PoolStringArray()
|
||||
|
||||
@ -45,6 +50,7 @@ func to_utf8() -> PoolByteArray:
|
||||
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])])
|
||||
@ -55,3 +61,48 @@ func to_utf8() -> PoolByteArray:
|
||||
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()
|
||||
|
@ -13,17 +13,29 @@ _global_script_classes=[ {
|
||||
"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": ""
|
||||
"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" )
|
||||
@ -31,6 +43,7 @@ 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]
|
||||
|
||||
@ -38,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"
|
||||
|
@ -1,3 +1,2 @@
|
||||
first.md 2022-01-06 first article
|
||||
second.md 2021-05-03 second article
|
||||
second.md
|
||||
first.md 2022-03-03 first title
|
||||
second.md 2022-03-05 second title
|
||||
|
@ -1,405 +1,248 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Parcel Sandbox</title>
|
||||
<title>Tickle</title>
|
||||
<meta charset="UTF-8" />
|
||||
<style></style>
|
||||
<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>
|
||||
<template id="my-link">
|
||||
<style>
|
||||
:host {
|
||||
--background-regular: hsla(196, 61%, 58%, 0.75);
|
||||
--background-active: red;
|
||||
text-decoration: none;
|
||||
color: #18272f;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
:host span {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host::before {
|
||||
content: "";
|
||||
background-color: var(--background-regular);
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 3px;
|
||||
width: 100%;
|
||||
height: 8px;
|
||||
z-index: -1;
|
||||
transition: all 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
:host(:hover)::before {
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:host([active])::before {
|
||||
background-color: var(--background-active);
|
||||
}
|
||||
</style>
|
||||
|
||||
<span><slot /></span>
|
||||
</template>
|
||||
<template id="my-menu">
|
||||
<style>
|
||||
:host ul,
|
||||
:host li {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
:host nav {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
||||
<nav>
|
||||
<slot />
|
||||
</nav>
|
||||
</template>
|
||||
<my-menu id="menu">
|
||||
<my-link main href="d">Home</my-link>
|
||||
<h2>Articles</h2>
|
||||
</my-menu>
|
||||
<div id="App"></div>
|
||||
|
||||
<script>
|
||||
//@ts-check
|
||||
|
||||
/**********************************************************************
|
||||
*
|
||||
* UTILITIES
|
||||
*
|
||||
* A few common methods to use in the project
|
||||
*
|
||||
*********************************************************************/
|
||||
|
||||
const Signal = () => {
|
||||
const listeners = new Set();
|
||||
|
||||
return {
|
||||
remove: listeners.delete.bind(listeners),
|
||||
add(/** @type {(arg:any)=>void} */ listener) {
|
||||
listeners.add(listener);
|
||||
return listeners.delete.bind(listeners, listener);
|
||||
},
|
||||
emit(/** @type {any} */ data) {
|
||||
listeners.forEach((l) => l(data));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const getText = (/** @type {string} */ file) =>
|
||||
fetch(`./${file}`)
|
||||
.then((response) => response.text())
|
||||
.catch((err) => {
|
||||
console.error(`could not find file "${file}"`);
|
||||
throw err;
|
||||
});
|
||||
|
||||
const parseMarkdown = (/** @type {string} */ text) =>
|
||||
text
|
||||
// lists
|
||||
.replace(
|
||||
/^\s*\n((?:\*\s.+\s*\n)+)([^\*])/gm,
|
||||
(_, bullets, next) =>
|
||||
`<ul>${bullets.replace(
|
||||
/^\*\s(.+)/gm,
|
||||
"<li>$1</li>"
|
||||
)}\n</ul>\n\n${next}`
|
||||
)
|
||||
.replace(
|
||||
/^\s*\n((?:\d\..+\s*\n)+)([^\*])/gm,
|
||||
(_, bullets, next) =>
|
||||
`<ol>${bullets.replace(
|
||||
/^\d\.\s(.+)/gm,
|
||||
"<li>$1</li>"
|
||||
)}\n</ol>\n\n${next}`
|
||||
)
|
||||
// blockquotes
|
||||
.replace(/^\>(.+)/gm, "<blockquote>$1</blockquote>")
|
||||
// headers
|
||||
.replace(/(#+)(.+)/g, (_, { length: l }, t) => `<h${l}>${t}</h${l}>`)
|
||||
.replace(/^(.+)\n\=+/gm, "<h1>$1</h1>")
|
||||
.replace(/^(.+)\n\-+/gm, "<h2>$1</h2>")
|
||||
//images
|
||||
.replace(/\!\[([^\]]+)\]\(([^\)]+)\)/g, '<img src="$2" alt="$1" />')
|
||||
//links
|
||||
.replace(
|
||||
/[\[]{1}([^\]]+)[\]]{1}[\(]{1}([^\)\"]+)(\"(.+)\")?[\)]{1}/g,
|
||||
'<a href="$2" title="$4">$1</a>'
|
||||
)
|
||||
//font styles
|
||||
.replace(/[\*\_]{2}([^\*\_]+)[\*\_]{2}/g, "<strong>$1</strong>")
|
||||
.replace(/[\*\_]{1}([^\*\_]+)[\*\_]{1}/g, "<em>$1</em>")
|
||||
.replace(/[\~]{2}([^\~]+)[\~]{2}/g, "<del>$1</del>")
|
||||
//pre
|
||||
.replace(/^\s*\n\`\`\`(([^\s]+))?/gm, '<pre class="$2">')
|
||||
.replace(/^\`\`\`\s*\n/gm, "</pre>\n\n")
|
||||
//code
|
||||
.replace(/[\`]{1}([^\`]+)[\`]{1}/g, "<code>$1</code>")
|
||||
//p
|
||||
.replace(/^\s*(\n)?(.+)/gm, (m) => {
|
||||
return /\<(\/)?(h\d|ul|ol|li|blockquote|pre|img)/.test(m)
|
||||
? m
|
||||
: "<p>" + m + "</p>";
|
||||
})
|
||||
//strip p from pre
|
||||
.replace(/(\<pre.+\>)\s*\n\<p\>(.+)\<\/p\>/gm, "$1$2")
|
||||
.trim();
|
||||
|
||||
const getMarkdown = (/** @type {string} */ file) =>
|
||||
getText(file).then(parseMarkdown);
|
||||
<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
|
||||
*
|
||||
* @param {string} tag
|
||||
* @param {Record<string, any>} props
|
||||
* @param {string|Node[]} children
|
||||
* @returns
|
||||
*/
|
||||
const el = (tag = "div", props = {}, children = []) => {
|
||||
const node = document.createElement(tag);
|
||||
Object.keys(props).forEach((key) => {
|
||||
node.setAttribute(key, props[key]);
|
||||
});
|
||||
if (typeof children == "string") {
|
||||
children = [document.createTextNode(children)];
|
||||
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;
|
||||
}
|
||||
children.forEach((child) => node.appendChild(child));
|
||||
return node;
|
||||
};
|
||||
|
||||
const makeTitelize = (alwaysLowCaps = [], alwaysUpperCaps = []) => {
|
||||
const specials = [...alwaysLowCaps, ...alwaysUpperCaps].reduce(
|
||||
(result, word) =>
|
||||
result.set(new RegExp("\\b" + word + "\\b", "gi"), word),
|
||||
/** @type {Map<RegExp, string>}*/ (new Map())
|
||||
);
|
||||
const titelize = (/** @type {string} */ text) => {
|
||||
text = text
|
||||
.replace(/_-\//g, " ")
|
||||
.replace(/\.\w+$/, "")
|
||||
.replace(/\s+/, " ")
|
||||
.split(" ")
|
||||
.map((word) =>
|
||||
word.length > 1
|
||||
? word[0].toUpperCase() + word.slice(1).toLowerCase()
|
||||
: word
|
||||
)
|
||||
.join(" ");
|
||||
for (const [key, value] of specials) {
|
||||
text = text.replace(key, value);
|
||||
}
|
||||
return text;
|
||||
};
|
||||
return titelize;
|
||||
};
|
||||
|
||||
const titelize = makeTitelize(["the", "a"], ["TV", "ID", "AI"]);
|
||||
|
||||
const Router = (() => {
|
||||
const onRouteChange = Signal();
|
||||
let route = "";
|
||||
|
||||
const set = (/** @type {string} */ newRoute) => {
|
||||
if (newRoute === route) {
|
||||
return false;
|
||||
}
|
||||
window.location.hash = newRoute;
|
||||
route = newRoute;
|
||||
onRouteChange.emit(route);
|
||||
return true;
|
||||
};
|
||||
|
||||
const get = () => window.location.hash.slice(1).replace(/\//gi, "/");
|
||||
|
||||
const is = (href) => href === get();
|
||||
|
||||
window.addEventListener("popstate", () => set(get()));
|
||||
|
||||
return { set, get, is, onRouteChange };
|
||||
})();
|
||||
|
||||
const getTemplateClone = (/** @type {string} */ id) => {
|
||||
const templateModel = /** @type {HTMLTemplateElement} */ (
|
||||
document.getElementById(id)
|
||||
);
|
||||
const template = /** @type {HTMLElement} */ (
|
||||
templateModel.content.cloneNode(true)
|
||||
);
|
||||
return template;
|
||||
};
|
||||
|
||||
/**********************************************************************
|
||||
* WEB COMPONENTS
|
||||
*
|
||||
* Sources:
|
||||
* https://web.dev/custom-elements-best-practices/
|
||||
* https://googlechromelabs.github.io/howto-components/
|
||||
*
|
||||
* A set of neat components to use in the page
|
||||
*
|
||||
*********************************************************************/
|
||||
|
||||
class CustomElement extends HTMLElement {
|
||||
/** @type {ShadowRoot} */
|
||||
shadow = this.attachShadow({ mode: "closed" });
|
||||
|
||||
/**
|
||||
* A user may set a property on an instance of an element, before its prototype has been connected to this class.
|
||||
* Will check for any instance properties and run them through the proper class setters.
|
||||
* @param {string} prop
|
||||
*/
|
||||
_syncProperty(prop) {
|
||||
if (this.hasOwnProperty(prop)) {
|
||||
let value = this[prop];
|
||||
delete this[prop];
|
||||
this[prop] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class MyLink extends CustomElement {
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow.append(getTemplateClone("my-link"));
|
||||
this.shadow.addEventListener("click", this._onClick.bind(this));
|
||||
Router.onRouteChange.add(this.updateActive.bind(this));
|
||||
}
|
||||
|
||||
static get observedAttributes() {
|
||||
return ["href", "active", "main"];
|
||||
}
|
||||
|
||||
_onClick() {
|
||||
if (this.href) {
|
||||
Router.set(this.href);
|
||||
}
|
||||
}
|
||||
|
||||
attributeChangedCallback(property, oldValue, newValue) {
|
||||
if (oldValue === newValue) {
|
||||
return;
|
||||
}
|
||||
this[property] = newValue;
|
||||
}
|
||||
|
||||
updateActive() {
|
||||
if (Router.is(this.href)) {
|
||||
this.setAttribute("active", "");
|
||||
} else {
|
||||
this.removeAttribute("active");
|
||||
}
|
||||
}
|
||||
|
||||
set href(/** @type {string}*/ value) {
|
||||
this.setAttribute("href", value);
|
||||
this.updateActive();
|
||||
}
|
||||
|
||||
get href() {
|
||||
return this.getAttribute("href");
|
||||
}
|
||||
|
||||
set main(/** @type {boolean}*/ value) {
|
||||
if (value) {
|
||||
this.setAttribute("main", "");
|
||||
} else {
|
||||
this.removeAttribute("main");
|
||||
}
|
||||
}
|
||||
|
||||
get main() {
|
||||
return this.hasAttribute("main");
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
["active", "main"].forEach((prop) => this._syncProperty(prop));
|
||||
this.updateActive();
|
||||
if (this.getAttribute("main")) {
|
||||
console.log("sdfsdff");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("my-link", MyLink);
|
||||
|
||||
class MyMenu extends CustomElement {
|
||||
_handled = new Set();
|
||||
constructor() {
|
||||
super();
|
||||
this.shadow.append(getTemplateClone("my-menu"));
|
||||
const slot = this.shadow.querySelector("slot");
|
||||
slot.addEventListener("slotchange", (event) => {
|
||||
for (const child of slot.assignedElements()) {
|
||||
if (this._handled.has(child) || !(child instanceof MyLink)) {
|
||||
continue;
|
||||
}
|
||||
this._handled.add(child);
|
||||
// TODO: pre-fetch
|
||||
//console.log("new child: ", child);
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define("my-menu", MyMenu);
|
||||
};
|
||||
|
||||
/**********************************************************************
|
||||
* MARKDOWN PARSING
|
||||
*********************************************************************/
|
||||
window.addEventListener("hashchange", onHashChange);
|
||||
|
||||
const load = (/** @type {string} */ file) =>
|
||||
getMarkdown(file).then((md) => {
|
||||
app.innerHTML = md;
|
||||
});
|
||||
/**
|
||||
* 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`);
|
||||
|
||||
/**********************************************************************
|
||||
* BOOSTRAPPING
|
||||
*********************************************************************/
|
||||
|
||||
getText("files.txt").then((lines) => {
|
||||
lines
|
||||
.split(`\n`)
|
||||
.map((line) => {
|
||||
const [file, maybeDate, ...rest] = line.split(/\s/);
|
||||
const href = file.trim();
|
||||
let date = maybeDate ? new Date(maybeDate) : new Date();
|
||||
if (isNaN(date.getTime())) {
|
||||
date = new Date();
|
||||
rest.unshift(maybeDate);
|
||||
}
|
||||
const textContent = rest.length
|
||||
? rest.join(" ").trim()
|
||||
: titelize(file);
|
||||
return { href, date, textContent };
|
||||
})
|
||||
.sort(({ date: a }, { date: b }) => a.getTime() - b.getTime())
|
||||
.forEach(({ href, date, textContent }) => {
|
||||
const link = /** @type {MyLink} */ el(
|
||||
"my-link",
|
||||
{ href },
|
||||
textContent
|
||||
);
|
||||
document.getElementById("menu").appendChild(link);
|
||||
onHashChange();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const app = document.getElementById("App");
|
||||
|
||||
const BLOCKQUOTE = Symbol("blockquote");
|
||||
const PARAGRAPH = Symbol("paragraph");
|
||||
const LIST = Symbol("list");
|
||||
|
||||
Router.onRouteChange.add((route) => load(route));
|
||||
start();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
@ -17,4 +17,4 @@ Image:
|
||||
code_block!
|
||||
```
|
||||
|
||||
`code not block`
|
||||
`code not block`
|
BIN
theme/FiraSans-ExtraLight.otf
Normal file
BIN
theme/FiraSans-ExtraLight.otf
Normal file
Binary file not shown.
BIN
theme/FiraSans-Light.otf
Normal file
BIN
theme/FiraSans-Light.otf
Normal file
Binary file not shown.
BIN
theme/FiraSans-Regular.otf
Normal file
BIN
theme/FiraSans-Regular.otf
Normal file
Binary file not shown.
6
theme/FontRg.tres
Normal file
6
theme/FontRg.tres
Normal 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
7
theme/FontWriter.tres
Normal 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
BIN
theme/FreeMono.ttf
Normal file
Binary file not shown.
BIN
theme/FreeMonoBold.ttf
Normal file
BIN
theme/FreeMonoBold.ttf
Normal file
Binary file not shown.
BIN
theme/FreeSans.ttf
Normal file
BIN
theme/FreeSans.ttf
Normal file
Binary file not shown.
BIN
theme/default_theme.theme
Normal file
BIN
theme/default_theme.theme
Normal file
Binary file not shown.
14
theme/styleboxes/button/BtnDisabledStylebox.tres
Normal file
14
theme/styleboxes/button/BtnDisabledStylebox.tres
Normal 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
|
17
theme/styleboxes/button/BtnHoverStylebox.tres
Normal file
17
theme/styleboxes/button/BtnHoverStylebox.tres
Normal 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
|
14
theme/styleboxes/button/BtnNormalStylebox.tres
Normal file
14
theme/styleboxes/button/BtnNormalStylebox.tres
Normal 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
|
17
theme/styleboxes/button/BtnPressedStylebox.tres
Normal file
17
theme/styleboxes/button/BtnPressedStylebox.tres
Normal 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
|
17
theme/styleboxes/lineedit/LineEditNormalStylebox.tres
Normal file
17
theme/styleboxes/lineedit/LineEditNormalStylebox.tres
Normal 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
|
17
theme/styleboxes/lineedit/LineEditReadonlyStylebox.tres
Normal file
17
theme/styleboxes/lineedit/LineEditReadonlyStylebox.tres
Normal 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
|
@ -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
|
@ -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
|
8
theme/styleboxes/scrollbar/ScrollBarGrabberStylebox.tres
Normal file
8
theme/styleboxes/scrollbar/ScrollBarGrabberStylebox.tres
Normal 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
|
13
theme/styleboxes/scrollbar/ScrollBarScrollStylebox.tres
Normal file
13
theme/styleboxes/scrollbar/ScrollBarScrollStylebox.tres
Normal 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
|
118
theme/textures/checkmark.svg
Normal file
118
theme/textures/checkmark.svg
Normal 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 |
35
theme/textures/checkmark.svg.import
Normal file
35
theme/textures/checkmark.svg.import
Normal 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
|
BIN
theme/textures/checkmark_disabled-checked.png
Normal file
BIN
theme/textures/checkmark_disabled-checked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 714 B |
35
theme/textures/checkmark_disabled-checked.png.import
Normal file
35
theme/textures/checkmark_disabled-checked.png.import
Normal 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
|
BIN
theme/textures/checkmark_disabled-unchecked.png
Normal file
BIN
theme/textures/checkmark_disabled-unchecked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 349 B |
35
theme/textures/checkmark_disabled-unchecked.png.import
Normal file
35
theme/textures/checkmark_disabled-unchecked.png.import
Normal 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
|
BIN
theme/textures/checkmark_enabled-checked.png
Normal file
BIN
theme/textures/checkmark_enabled-checked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 771 B |
35
theme/textures/checkmark_enabled-checked.png.import
Normal file
35
theme/textures/checkmark_enabled-checked.png.import
Normal 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
|
BIN
theme/textures/checkmark_enabled-unchecked.png
Normal file
BIN
theme/textures/checkmark_enabled-unchecked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 357 B |
35
theme/textures/checkmark_enabled-unchecked.png.import
Normal file
35
theme/textures/checkmark_enabled-unchecked.png.import
Normal 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
|
Reference in New Issue
Block a user