Compare commits

...

8 commits

12 changed files with 280 additions and 20 deletions

3
.gitmodules vendored
View file

@ -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

View file

@ -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
View file

@ -0,0 +1 @@
../dist/submodules/spdx-license-identifiers/json/licenses.json

@ -1 +1 @@
Subproject commit 14db770ef58d92c11834ac22c39e7e63f3236058
Subproject commit 6f480e406d07841cdde451b35e8ac554d51566c8

@ -0,0 +1 @@
Subproject commit 3bf4c120dabbedbcae53ff7618889d7d9561464e

18
example/manifest.json Normal file
View 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
}

View file

@ -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

View file

@ -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]

View file

@ -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")]

View file

@ -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

View file

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

View file

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