Compare commits
8 commits
fe2f4b7f7b
...
c8276bf6b5
Author | SHA1 | Date | |
---|---|---|---|
c8276bf6b5 | |||
439ffba196 | |||
47f0e3ef53 | |||
bfe1f9a8d5 | |||
cc98984ce4 | |||
dda4ce2646 | |||
3db7ab1daf | |||
4c9ac8c2f6 |
12 changed files with 280 additions and 20 deletions
3
.gitmodules
vendored
3
.gitmodules
vendored
|
@ -4,3 +4,6 @@
|
|||
[submodule "addons/SUI"]
|
||||
path = addons/SUI
|
||||
url = https://git.staropensource.de/StarOpenSource/SUI-distrib.git
|
||||
[submodule "dist/submodules/spdx-license-identifiers"]
|
||||
path = dist/submodules/spdx-license-identifiers
|
||||
url = https://github.com/spdx/license-list-data
|
||||
|
|
|
@ -21,4 +21,4 @@ Title: Basteibrücke morgens
|
|||
Copyright holder: Thomas Wolf
|
||||
-> Website: www.foto-tw.de
|
||||
Upload URL: https://commons.wikimedia.org/wiki/File:Basteibr%C3%BCcke_morgens.jpg
|
||||
License: CC BY-SA 3.0 (https://creativecommons.org/licenses/by-sa/3.0/legalcode
|
||||
License: CC BY-SA 3.0-DE (https://creativecommons.org/licenses/by-sa/3.0/de/legalcode
|
||||
|
|
1
assets/licenses.json
Symbolic link
1
assets/licenses.json
Symbolic link
|
@ -0,0 +1 @@
|
|||
../dist/submodules/spdx-license-identifiers/json/licenses.json
|
2
dist/submodules/CORE
vendored
2
dist/submodules/CORE
vendored
|
@ -1 +1 @@
|
|||
Subproject commit 14db770ef58d92c11834ac22c39e7e63f3236058
|
||||
Subproject commit 6f480e406d07841cdde451b35e8ac554d51566c8
|
1
dist/submodules/spdx-license-identifiers
vendored
Submodule
1
dist/submodules/spdx-license-identifiers
vendored
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit 3bf4c120dabbedbcae53ff7618889d7d9561464e
|
18
example/manifest.json
Normal file
18
example/manifest.json
Normal file
|
@ -0,0 +1,18 @@
|
|||
{
|
||||
"version": {
|
||||
"minimum": 1.0,
|
||||
"target": 1.0
|
||||
},
|
||||
"resolution": {
|
||||
"x": 1440,
|
||||
"y": 810
|
||||
},
|
||||
"title": "Example presentation",
|
||||
"authors": [ "JeremyStarTM" ],
|
||||
"contributors": [ "Contributors" ],
|
||||
"license": "AGPL-3.0-or-later",
|
||||
"entrypoint": "entrypoint.gd",
|
||||
"slides": 5,
|
||||
"animations": false,
|
||||
"display_end_text_after_last_slide": true
|
||||
}
|
|
@ -18,7 +18,7 @@ encrypt_directory=false
|
|||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=2
|
||||
debug/export_console_wrapper=0
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
|
@ -81,7 +81,7 @@ encrypt_directory=false
|
|||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=2
|
||||
debug/export_console_wrapper=0
|
||||
binary_format/embed_pck=true
|
||||
texture_format/bptc=true
|
||||
texture_format/s3tc=true
|
||||
|
|
|
@ -27,7 +27,7 @@ window/stretch/mode="canvas_items"
|
|||
|
||||
[editor]
|
||||
|
||||
run/main_run_args="%command% ++ "
|
||||
run/main_run_args="%command% ++ example"
|
||||
export/convert_text_resources_to_binary=false
|
||||
|
||||
[editor_plugins]
|
||||
|
|
|
@ -91,13 +91,13 @@ anchor_top = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -179.0
|
||||
offset_top = -88.5
|
||||
offset_top = -113.5
|
||||
offset_right = 179.0
|
||||
offset_bottom = 88.5
|
||||
offset_bottom = 113.5
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Present" parent="Buttons" instance=ExtResource("6_xj8q1")]
|
||||
[node name="PresentZip" parent="Buttons" instance=ExtResource("6_xj8q1")]
|
||||
layout_mode = 1
|
||||
anchors_preset = 5
|
||||
anchor_left = 0.5
|
||||
|
@ -107,7 +107,20 @@ offset_left = -175.0
|
|||
offset_right = 175.0
|
||||
offset_bottom = 53.0
|
||||
grow_vertical = 1
|
||||
text = "[center]Open presentation[/center]"
|
||||
text = "[center]Open archive[/center]"
|
||||
|
||||
[node name="PresentDir" parent="Buttons" instance=ExtResource("6_xj8q1")]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -175.0
|
||||
offset_top = -55.5
|
||||
offset_right = 175.0
|
||||
offset_bottom = -2.5
|
||||
text = "[center]Open directory[/center]"
|
||||
|
||||
[node name="Docs" parent="Buttons" instance=ExtResource("6_xj8q1")]
|
||||
layout_mode = 1
|
||||
|
@ -117,9 +130,9 @@ anchor_top = 0.5
|
|||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -175.0
|
||||
offset_top = -26.5
|
||||
offset_top = 2.5
|
||||
offset_right = 175.0
|
||||
offset_bottom = 26.5
|
||||
offset_bottom = 55.5
|
||||
text = "[center]View documentation[/center]"
|
||||
|
||||
[node name="ClosePresencode" parent="Buttons" instance=ExtResource("6_xj8q1")]
|
||||
|
|
|
@ -14,7 +14,7 @@ func _init() -> void:
|
|||
if OS.is_debug_build(): core_config.logger_level = CoreTypes.LoggerLevel.DIAG
|
||||
else: core_config.logger_level = CoreTypes.LoggerLevel.INFO
|
||||
# Preinitialize CORE Framework
|
||||
core = Core.new(core_config)
|
||||
core = await Core.new(core_config)
|
||||
|
||||
# Initialization
|
||||
func _ready() -> void:
|
||||
|
@ -55,16 +55,20 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.""")
|
|||
# Initialize internals
|
||||
logger.info("Initializing internals")
|
||||
|
||||
# Load state manager
|
||||
# Load presentation loader
|
||||
var presenloader: Node = Node.new()
|
||||
presenloader.name = "Presentation loader"
|
||||
presenloader.name = "Presenloader"
|
||||
presenloader.set_script(load("res://src/presenloader.gd"))
|
||||
get_tree().root.add_child(presenloader)
|
||||
|
||||
# Decide whenether to load a presentation or the UI
|
||||
if args["load_presentation"]:
|
||||
logger.verb("Loading presentation")
|
||||
logger.crash("Not implemented.")
|
||||
|
||||
var error: String = presenloader.load_presentation(args["presentation_path"])
|
||||
|
||||
if error != "":
|
||||
logger.error("Presencode is unable to load the presentation you tried to open.\nError thrown by presenloader.gd:\n" + error)
|
||||
else:
|
||||
logger.verb("Loading user interface")
|
||||
|
||||
|
@ -72,6 +76,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.""")
|
|||
var userinterface: NinePatchRect = load("res://scenesrc/UserInterface.tscn").instantiate()
|
||||
userinterface.name = "UserInterface"
|
||||
userinterface.core_config = core_config.duplicate()
|
||||
userinterface.presenloader = presenloader
|
||||
get_tree().root.add_child.call_deferred(userinterface)
|
||||
|
||||
# Perform cleanup
|
||||
|
@ -148,11 +153,12 @@ Further arguments:
|
|||
processed["presentation"] = true
|
||||
|
||||
# Check if file exists
|
||||
if !FileAccess.file_exists(arg):
|
||||
if !FileAccess.file_exists(arg) and !DirAccess.dir_exists_absolute(arg):
|
||||
logger.error(core.misc.stringify_variables("Unable to parse argument %arg%: Invalid file path %arg%", { "arg": arg }))
|
||||
continue
|
||||
|
||||
# Update 'args'
|
||||
args["load_presentation"] = true
|
||||
args["presentation_path"] = arg
|
||||
|
||||
return args
|
||||
|
|
|
@ -1,10 +1,148 @@
|
|||
extends Node
|
||||
|
||||
# Presencode version
|
||||
const version: int = 1
|
||||
const version_float: float = float(version)
|
||||
|
||||
# CORE
|
||||
@onready var core: Core = get_node("/root/CORE")
|
||||
@onready var logger: CoreLoggerInstance = core.logger.get_instance("src/presenloader.gd", self)
|
||||
|
||||
# Presentation zip/dir info
|
||||
var path: String = ""
|
||||
var is_file: bool = true
|
||||
var manifest: Dictionary = {}
|
||||
|
||||
# Access methods
|
||||
var reader: ZIPReader = ZIPReader.new()
|
||||
var diraccess: DirAccess = null
|
||||
|
||||
func _ready() -> void:
|
||||
# Register cleanup hook
|
||||
core.register_cleanup_hook(func() -> void: queue_free())
|
||||
|
||||
logger.info("Presentation loader is ready")
|
||||
|
||||
func load_presentation(load_path: String) -> String:
|
||||
logger.info("Validating presentation located at " + load_path)
|
||||
path = load_path
|
||||
is_file = FileAccess.file_exists(path)
|
||||
|
||||
# Check if exists
|
||||
logger.verb("Checking if presentation exists")
|
||||
if !is_file and !DirAccess.dir_exists_absolute(path):
|
||||
return "Could not locate file or directory at " + path
|
||||
|
||||
if is_file:
|
||||
# Refuse loading legacy presentations
|
||||
if path.ends_with(".pcpa"):
|
||||
return "Legacy Presencode presentation (.pcpa files) cannot be opened in newer Presencode versions. Please\nuse Presencode commit 19d3e8a0d00849c6668b31cb7ffda542826ba533 to view legacy Presencode presentations"
|
||||
# This check simply exists to ensure that Presencode presentation can be easily recognized by the file extension.
|
||||
if !path.ends_with(".pcar"): return "The file does not end in .pcar (Presencode Archive). Please rename the file."
|
||||
|
||||
# Open archive
|
||||
logger.verb("Opening archive")
|
||||
var error: Error = reader.open(path)
|
||||
if error != Error.OK:
|
||||
return core.misc.stringify_variables("Can't open ZIP archive: %error_string% (%error%)", { "error": error, "error_string": error_string(error) }, true)
|
||||
else:
|
||||
# Open directory
|
||||
logger.verb("Opening directory")
|
||||
diraccess = DirAccess.open(path)
|
||||
if diraccess == null:
|
||||
return core.misc.stringify_variables("Can't open directory: %error_string% (%error%)", { "error": DirAccess.get_open_error(), "error_string": error_string(DirAccess.get_open_error()) }, true)
|
||||
|
||||
var output: String = check_required_files()
|
||||
if output != "": return output
|
||||
|
||||
output = parse_manifest()
|
||||
if output != "": return output
|
||||
|
||||
var output_array: Array[String] = check_manifest()
|
||||
if output_array != []: return core.misc.format_stringarray(output_array, "- ", "", "\n", "\n")
|
||||
|
||||
return ""
|
||||
|
||||
func check_required_files() -> String:
|
||||
logger.verb("Checking for required files")
|
||||
# Define variables
|
||||
var files_present: Dictionary = { "manifest": false, "slides": false }
|
||||
var files: PackedStringArray = PackedStringArray([])
|
||||
|
||||
# Update 'files' appropriately
|
||||
if is_file:
|
||||
files = reader.get_files()
|
||||
else:
|
||||
files = diraccess.get_files()
|
||||
for directory in diraccess.get_directories():
|
||||
files.append(directory + "/")
|
||||
|
||||
# Iterate through files and directories
|
||||
for file in files:
|
||||
if file == "manifest.json": files_present["manifest"] = true
|
||||
elif file.begins_with("slides/"): files_present["slides"] = true
|
||||
elif file.begins_with("src/"): pass
|
||||
elif file.begins_with("assets/"): pass
|
||||
else: logger.warn(core.misc.stringify_variables("Unknown file/directory %file% inside presentation directory, ignoring", { "file": file }))
|
||||
|
||||
# Check if required files exist
|
||||
if !files_present["manifest"]:
|
||||
return "The presentation manifest is missing. Make sure it is named 'manifest.json'"
|
||||
if !files_present["slides"]: return "The 'slides' directory is missing. You can't have a presentation without slides, dingus!"
|
||||
|
||||
return ""
|
||||
|
||||
func parse_manifest() -> String:
|
||||
logger.verb("Parsing manifest")
|
||||
var manifest_raw: String = ""
|
||||
|
||||
if is_file: manifest_raw = reader.read_file("manifest.json").get_string_from_utf8()
|
||||
else:
|
||||
var file: FileAccess = FileAccess.open(path + "/manifest.json", FileAccess.READ)
|
||||
if file == null:
|
||||
return core.misc.stringify_variables("Can't read manifest: %error_string% (%error%)", { "error": FileAccess.get_open_error(), "error_string": error_string(FileAccess.get_open_error()) })
|
||||
manifest_raw = file.get_as_text()
|
||||
file.close()
|
||||
|
||||
var json: JSON = JSON.new()
|
||||
var error: Error = json.parse(manifest_raw)
|
||||
if error != OK:
|
||||
return core.misc.stringify_variables("Can't parse manifest: %error% (line %line%)", { "error": json.get_error_message(), "line": json.get_error_line() })
|
||||
if typeof(json.data) != Variant.Type.TYPE_DICTIONARY:
|
||||
return core.misc.stringify_variables("Can't parse manifest: Parsed manifest is not of type Dictionary but instead %type%", { "type": type_string(typeof(json.data)) })
|
||||
|
||||
manifest = json.data
|
||||
|
||||
return ""
|
||||
|
||||
func check_manifest() -> Array[String]:
|
||||
logger.verb("Checking manifest")
|
||||
var schema: CoreValidationSchema = core.validation.get_schema({
|
||||
"version": {
|
||||
"minimum": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_FLOAT ]).is_not_empty().has_minimum_float(version_float).has_maximum_float(version_float),
|
||||
"target": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_FLOAT ]).is_not_empty().has_minimum_float(version_float)
|
||||
},
|
||||
"resolution": {
|
||||
"x": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_FLOAT ]).is_not_empty().has_minimum_float(500.0),
|
||||
"y": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_FLOAT ]).is_not_empty().has_minimum_float(500.0)
|
||||
},
|
||||
"title": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_STRING ]).is_not_empty(),
|
||||
"authors": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_ARRAY ]).is_not_empty(),
|
||||
"contributors": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_ARRAY ]),
|
||||
"license": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_STRING ]).has_values(Callable(func() -> Array:
|
||||
# Retrieve SPDX license identifiers from res://assets/licenses.json (symlinked to res://dist/submodules/spdx-license-identifiers/json/licenses.json)
|
||||
var licenses: Array = []
|
||||
var file: FileAccess = FileAccess.open("res://assets/licenses.json", FileAccess.READ)
|
||||
var spdx_licenses: Dictionary = JSON.parse_string(file.get_as_text())
|
||||
file.close()
|
||||
for license in spdx_licenses["licenses"]:
|
||||
licenses.append(license["licenseId"])
|
||||
return licenses
|
||||
).call()),
|
||||
"entrypoint": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_STRING ]).is_not_empty().contains([ ".gd" ], 1),
|
||||
"slides": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_FLOAT ]).has_minimum_float(1.0),
|
||||
"animations": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_BOOL ]),
|
||||
"display_end_text_after_last_slide": core.validation.get_single_schema(self).matches_type([ Variant.Type.TYPE_BOOL ])
|
||||
}, manifest, self)
|
||||
|
||||
return schema.evaluate()
|
||||
|
|
|
@ -5,6 +5,9 @@ var core_config: CoreConfiguration
|
|||
@onready var core: Core = get_node("/root/CORE")
|
||||
@onready var logger: CoreLoggerInstance = core.logger.get_instance("src/userinterface.gd", self)
|
||||
|
||||
# Internal infrastructure
|
||||
var presenloader: Node
|
||||
|
||||
# Variables
|
||||
var shutdown: bool = false
|
||||
var cleanup_hook: int
|
||||
|
@ -32,7 +35,8 @@ var splashes: Array[String] = [
|
|||
"xD",
|
||||
"Now in 2D!",
|
||||
"very bad",
|
||||
"beta and alpha males are overrated, i'm a release male"
|
||||
"beta and alpha males are overrated, i'm a release male",
|
||||
"uses .pcar files!"
|
||||
]
|
||||
|
||||
# Threads
|
||||
|
@ -99,12 +103,88 @@ func add_connections() -> void:
|
|||
$Splash/Switcher.connect("pressed", func() -> void: update_splash())
|
||||
|
||||
# Buttons
|
||||
$Buttons/Present.connect("pressed", func() -> void:
|
||||
logger.error("Not implemented.")
|
||||
)
|
||||
$Buttons/PresentZip.connect("pressed", Callable(self, "display_open_dialog").bind(false))
|
||||
$Buttons/PresentDir.connect("pressed", Callable(self, "display_open_dialog").bind(true))
|
||||
$Buttons/Docs.connect("pressed", func() -> void: OS.shell_open("https://presencode.jstm.staropensource.de"))
|
||||
$Buttons/ClosePresencode.connect("pressed", func() -> void: core.quit_safely(0))
|
||||
|
||||
func display_open_dialog(directory: bool) -> void:
|
||||
var file_dialog: FileDialog = FileDialog.new()
|
||||
|
||||
# AcceptDialog settings
|
||||
file_dialog.title = "Open a Presencode-compatible presentation"
|
||||
file_dialog.ok_button_text = "Load"
|
||||
file_dialog.visible = true
|
||||
|
||||
# ConfirmationDialog settings
|
||||
file_dialog.size = Vector2i(500, 500)
|
||||
file_dialog.min_size = Vector2i(250, 250)
|
||||
|
||||
# FileDialog settings
|
||||
file_dialog.access = FileDialog.Access.ACCESS_FILESYSTEM
|
||||
if directory: file_dialog.file_mode = FileDialog.FileMode.FILE_MODE_OPEN_DIR
|
||||
else: file_dialog.file_mode = FileDialog.FileMode.FILE_MODE_OPEN_FILE
|
||||
if !directory: file_dialog.filters = PackedStringArray([ "*.pcar ; Presencode Archives" ])
|
||||
file_dialog.mode_overrides_title = false
|
||||
file_dialog.show_hidden_files = false
|
||||
|
||||
# Add connections
|
||||
file_dialog.connect("file_selected", Callable(self, "handle_open_dialog_logic").bind(file_dialog))
|
||||
file_dialog.connect("dir_selected", Callable(self, "handle_open_dialog_logic").bind(file_dialog))
|
||||
file_dialog.connect("canceled", func() -> void:
|
||||
get_tree().root.remove_child(file_dialog)
|
||||
file_dialog.queue_free()
|
||||
)
|
||||
|
||||
# Display dialog
|
||||
get_tree().root.add_child(file_dialog)
|
||||
|
||||
# Center dialog
|
||||
# (we do this after add_child because FileDialog seems
|
||||
# to run some logic related to 'size' during _ready())
|
||||
file_dialog.position = core.misc.get_center(get_tree().root.size, file_dialog.size)
|
||||
|
||||
func handle_open_dialog_logic(path: String, file_dialog: FileDialog) -> void:
|
||||
# Remove dialog
|
||||
get_tree().root.remove_child(file_dialog)
|
||||
file_dialog.queue_free()
|
||||
|
||||
# Check if presentation is valid
|
||||
var error: String = presenloader.load_presentation(path)
|
||||
if error != "":
|
||||
# Display errors in dialog
|
||||
var error_dialog: AcceptDialog = AcceptDialog.new()
|
||||
|
||||
# Configure dialog
|
||||
error_dialog.title = "Can't load presentation"
|
||||
error_dialog.dialog_text = "Presencode is unable to load the presentation you tried to open.\nError thrown by presenloader.gd:\n" + error
|
||||
error_dialog.ok_button_text = "ACK"
|
||||
error_dialog.visible = true
|
||||
|
||||
# Add connections
|
||||
error_dialog.connect("confirmed", func() -> void:
|
||||
get_tree().root.remove_child(error_dialog)
|
||||
error_dialog.queue_free()
|
||||
)
|
||||
error_dialog.connect("canceled", func() -> void:
|
||||
get_tree().root.remove_child(error_dialog)
|
||||
error_dialog.queue_free()
|
||||
)
|
||||
|
||||
# Display dialog
|
||||
get_tree().root.add_child(error_dialog)
|
||||
|
||||
# Center dialog
|
||||
# (we do this after add_child because AcceptDialog seems
|
||||
# to run some logic related to 'size' during _ready())
|
||||
error_dialog.position = core.misc.get_center(get_tree().root.size, error_dialog.size)
|
||||
|
||||
# Don't unload the user interface, just exit
|
||||
return
|
||||
|
||||
# Unload user interface
|
||||
unload()
|
||||
|
||||
# Updates the splash text
|
||||
func update_splash() -> void:
|
||||
var new_splash: String = splashes.pick_random()
|
||||
|
|
Loading…
Reference in a new issue