Skip to content

Commit 8c2ed5d

Browse files
committed
Add better exporting/saving for Chromium-based browsers
This uses the new web filesystem access APis, making GodSVG no longer save a new file to the downloads folder. This instead prompts the user once to save a file and stores a handle to that file that we can later write to all we want. There are some limitations; Mainly, the handle for each file has to be requested again any time the user loads up GodSVG again, but this isn't that problematic and right now I'm doing it any time the user saves the file again. See: https://developer.chrome.com/docs/capabilities/web-apis/file-system-access
1 parent 5e2df81 commit 8c2ed5d

2 files changed

Lines changed: 126 additions & 8 deletions

File tree

src/config_classes/TabData.gd

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,11 @@ func _save_svg_text() -> void:
7373
func save_to_bound_path() -> void:
7474
if Configs.savedata.get_active_tab() != self:
7575
return
76-
FileAccess.open(svg_file_path, FileAccess.WRITE).store_string(State.get_export_text())
76+
var export_text := State.get_export_text()
77+
if not OS.has_feature("web"):
78+
FileAccess.open(svg_file_path, FileAccess.WRITE).store_string(export_text)
79+
else:
80+
FileUtils.web_save(export_text.to_utf8_buffer(), "svg")
7781
queue_sync()
7882

7983
func setup_svg_text(new_text: String, fully_load := true) -> void:

src/utils/FileUtils.gd

Lines changed: 121 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ static func compare_svg_to_disk_contents(idx := -1) -> FileState:
3838
static func _save_svg_with_custom_final_callback(final_callback: Callable) -> void:
3939
var active_tab := Configs.savedata.get_active_tab()
4040
var file_path := active_tab.svg_file_path
41-
if not file_path.is_empty() and FileAccess.file_exists(file_path):
41+
if not file_path.is_empty() and (FileAccess.file_exists(file_path) or OS.has_feature("web")):
4242
active_tab.save_to_bound_path()
4343
if final_callback.is_valid():
4444
final_callback.call()
@@ -62,7 +62,8 @@ static func open_export_dialog(export_data: ImageExportData, final_callback := C
6262
buffer = ImageExportData.svg_to_buffer()
6363
else:
6464
buffer = export_data.image_to_buffer(export_data.generate_image())
65-
_web_save(buffer, ImageExportData.image_types_dict[export_data.format])
65+
66+
web_save(buffer, export_data.format, true, true)
6667
if final_callback.is_valid():
6768
final_callback.call()
6869
else:
@@ -96,7 +97,7 @@ static func open_export_dialog(export_data: ImageExportData, final_callback := C
9697
static func open_xml_export_dialog(xml: String, file_name: String) -> void:
9798
OS.request_permissions()
9899
if OS.has_feature("web"):
99-
_web_save(xml.to_utf8_buffer(), "application/xml")
100+
web_quick_save(xml.to_utf8_buffer(), "xml")
100101
else:
101102
if _is_native_preferred():
102103
var native_callback :=\
@@ -472,6 +473,7 @@ static func _close_tabs_internal(indices: Array[int]) -> void:
472473
static var _change_callback: JavaScriptObject
473474
static var _cancel_callback: JavaScriptObject
474475
static var _file_load_callbacks: Array[JavaScriptObject] = []
476+
static var _file_save_callback: JavaScriptObject
475477

476478
static var _web_file_data_cache: Dictionary[String, Variant] = {}
477479

@@ -639,9 +641,121 @@ static func _web_on_file_dialog_canceled(_args: Array) -> void:
639641
var window = JavaScriptBridge.get_interface("window")
640642
window.godsvgDialogClosed = true
641643

644+
static func _web_on_file_saved(args: Array) -> void:
645+
var file_handle: JavaScriptObject = args[0]
646+
var quick_export: bool = args[1]
647+
648+
if not quick_export:
649+
var active_tab := Configs.savedata.get_active_tab()
650+
active_tab.svg_file_path = file_handle.name
651+
active_tab.marked_unsaved = false
652+
else:
653+
HandlerGUI.remove_all_menus()
642654

643-
static func _web_save(buffer: PackedByteArray, format_name: String) -> void:
644-
var file_name := Utils.get_file_name(Configs.savedata.get_active_tab().svg_file_path)
655+
## Currently used for SVG exports on web.
656+
static func web_quick_save(buffer: PackedByteArray, format: String) -> void:
657+
var active_tab := Configs.savedata.get_active_tab()
658+
var file_name := Utils.get_file_name(active_tab.svg_file_path)
659+
var format_type := ImageExportData.image_types_dict[format]
645660
if file_name.is_empty():
646-
file_name = "export"
647-
JavaScriptBridge.download_buffer(buffer, file_name, format_name)
661+
file_name = "export." + format
662+
JavaScriptBridge.download_buffer(buffer, file_name, format_type)
663+
664+
## Currently used for saving on web. [br]
665+
## [param format]: The file format ("svg", etc) [br]
666+
## [param ignore_handles]: Doesn't write/read to existing file handles, makes new ones instead. [br]
667+
## [param quick_export]: Toggle on for exports
668+
static func web_save(buffer: PackedByteArray, format: String, ignore_handles: bool = false, quick_export: bool = false) -> void:
669+
var window := JavaScriptBridge.get_interface("window")
670+
671+
var active_tab := Configs.savedata.get_active_tab()
672+
var file_path = active_tab.svg_file_path
673+
if file_path.is_empty():
674+
if not active_tab.presented_name.is_empty() and not quick_export:
675+
file_path = active_tab.presented_name
676+
else:
677+
file_path = "export." + format
678+
var file_name := Utils.get_file_name(file_path)
679+
var format_type := ImageExportData.image_types_dict[format]
680+
681+
# Currently, only Chromium-based browsers support the new file picker system.
682+
if window["showSaveFilePicker"] == null:
683+
JavaScriptBridge.download_buffer(buffer, file_name, format_type)
684+
return
685+
686+
# Initializing file handles.
687+
if window["godsvgFileHandles"] == null:
688+
window.godsvgFileHandles = JavaScriptBridge.create_object("Object")
689+
690+
# Creating our save function; Should probably be moved to the global context.
691+
JavaScriptBridge.eval("""
692+
window.godsvgSaveFile = function(id, buffer, onSave, ignoreHandles=false, quickExport=false, options={}) {
693+
const writeFile = (fileHandle, buff) => {
694+
return fileHandle.createWritable().then((writable) => {
695+
writable.write(buff).then(() => {
696+
onSave(fileHandle, quickExport);
697+
writable.close();
698+
});
699+
});
700+
};
701+
702+
if (quickExport) {
703+
ignoreHandles = true;
704+
}
705+
706+
if (window["godsvgFileHandles"] == undefined) {
707+
window.godsvgFileHandles = {}
708+
}
709+
710+
if (!ignoreHandles && id in window.godsvgFileHandles) {
711+
writeFile(window.godsvgFileHandles[id], buffer);
712+
} else {
713+
window.showSaveFilePicker(options).then((fileHandle) => {
714+
if (!ignoreHandles) window.godsvgFileHandles[id] = fileHandle;
715+
writeFile(fileHandle, buffer);
716+
}).catch((err) => {
717+
if ("SecurityError" in err) {
718+
/*
719+
TODO: Figure out what to do if the user saves without pressing something first.
720+
Popping up a file save dialog without user input is disallowed due to
721+
secure context mumbo jumbo.
722+
*/
723+
console.error("Failed to save file due to the secure web context.");
724+
} else {
725+
console.error(err);
726+
}
727+
});
728+
}
729+
}
730+
""")
731+
732+
# Options
733+
var picker_options := {
734+
"suggestedName": file_name + "." + format,
735+
"startIn": "pictures",
736+
"types": [
737+
{
738+
"description": "Vector Image" if format == "svg" else "Image",
739+
"accept": {
740+
format_type: ["." + format],
741+
},
742+
},
743+
],
744+
"excludeAcceptAllOption": true,
745+
"multiple": false,
746+
}
747+
JavaScriptBridge.eval("""
748+
window.godsvgSavePickerOptions = %s;
749+
""" % JSON.stringify(picker_options))
750+
751+
# Buffer
752+
var array_buff = JavaScriptBridge.create_object("ArrayBuffer", buffer.size())
753+
var buff = JavaScriptBridge.create_object("Uint8Array", array_buff)
754+
for i in len(buffer):
755+
buff[i] = buffer[i]
756+
757+
# Saving
758+
# (godsvgCurrentlySaving is required in order to not accidentally save to the wrong tab)
759+
_file_save_callback = JavaScriptBridge.create_callback(_web_on_file_saved)
760+
window.godsvgCurrentlySaving = active_tab.id
761+
window.godsvgSaveFile(active_tab.id, buff, _file_save_callback, ignore_handles, quick_export, window.godsvgSavePickerOptions)

0 commit comments

Comments
 (0)