download system

This commit is contained in:
Face
2025-09-07 13:37:47 +03:00
parent 57674eb2b5
commit b4639e80bc
28 changed files with 1315 additions and 2 deletions

View File

@@ -0,0 +1,98 @@
class_name DownloadDialog
extends PopupPanel
signal download_confirmed(download_data: Dictionary, save_path: String)
signal download_cancelled(download_data: Dictionary)
@onready var ok_button: Button = $VBox/HBoxContainer/OkButton
@onready var cancel_button: Button = $VBox/HBoxContainer/CancelButton
@onready var file_dialog: FileDialog = $FileDialog
@onready var filename_label: Label = $VBox/FilenameLabel
@onready var url_label: Label = $VBox/URLLabel
var download_data: Dictionary = {}
func show_download_dialog(data: Dictionary):
download_data = data
var filename = data.get("filename", "download")
var url = data.get("url", "")
filename_label.text = "File: " + filename
var current_site = data.get("current_site", "")
if current_site != "":
url_label.text = "From: " + current_site
else:
url_label.text = "From: " + URLUtils.extract_domain(url)
popup()
_animate_entrance()
ok_button.grab_focus()
func _animate_entrance():
if not is_inside_tree():
return
var original_size = Vector2(size)
var small_size = original_size * 0.8
var size_difference = original_size - small_size
var original_pos = position
size = Vector2i(small_size)
position = original_pos + Vector2i(size_difference * 0.5)
var tween = create_tween()
if tween:
tween.set_parallel(true)
var size_property = tween.tween_property(self, "size", Vector2i(original_size), 0.2)
var pos_property = tween.tween_property(self, "position", original_pos, 0.2)
if size_property:
size_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
if pos_property:
pos_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
func _on_download_confirmed():
file_dialog.current_file = download_data.get("filename", "download")
file_dialog.current_dir = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
file_dialog.show()
func _animate_exit():
if not is_inside_tree():
queue_free()
return
var current_size = Vector2(size)
var small_size = current_size * 0.8
var size_difference = current_size - small_size
var current_pos = position
var target_pos = current_pos + Vector2i(size_difference * 0.5)
var tween = create_tween()
if tween:
tween.set_parallel(true)
var size_property = tween.tween_property(self, "size", Vector2i(small_size), 0.15)
var pos_property = tween.tween_property(self, "position", target_pos, 0.15)
if size_property:
size_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK)
if pos_property:
pos_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK)
await tween.finished
queue_free()
func _on_save_location_selected(path: String):
download_confirmed.emit(download_data, path)
_animate_exit()
func _on_file_dialog_cancelled():
download_cancelled.emit(download_data)
_animate_exit()
func _on_download_cancelled():
download_cancelled.emit(download_data)
_animate_exit()

View File

@@ -0,0 +1 @@
uid://be6fpnhb4p57v

View File

@@ -0,0 +1,70 @@
class_name DownloadEntry
extends PanelContainer
@onready var time_label: RichTextLabel = $DownloadEntry/TimeLabel
@onready var filename_label: RichTextLabel = $DownloadEntry/FileNameLabel
@onready var domain_label: RichTextLabel = $DownloadEntry/DomainLabel
@onready var size_label: RichTextLabel = $DownloadEntry/SizeLabel
@onready var link_button: Button = $DownloadEntry/LinkButton
@onready var file_button: Button = $DownloadEntry/FileButton
var download_data: Dictionary = {}
func setup_download_entry(data: Dictionary):
download_data = data
var filename = data.get("filename", "Unknown file")
var url = data.get("url", "")
var current_site = data.get("current_site", "")
var file_size = data.get("size", 0)
var timestamp = data.get("timestamp", Time.get_unix_time_from_system())
filename_label.text = filename
current_site = URLUtils.extract_domain(url) if url != "" else "Unknown source"
domain_label.text = current_site
var size_text = NetworkRequest.format_bytes(file_size)
size_label.text = size_text
var time_text = _format_time(timestamp)
time_label.text = time_text
func _format_time(unix_timestamp: float) -> String:
var datetime = Time.get_datetime_dict_from_unix_time(int(unix_timestamp))
# Format as "3:45PM"
var hour = datetime.hour
var minute = datetime.minute
var am_pm = "AM"
if hour == 0:
hour = 12
elif hour > 12:
hour -= 12
am_pm = "PM"
elif hour == 12:
am_pm = "PM"
return "%d:%02d%s" % [hour, minute, am_pm]
func get_filename() -> String:
return download_data.get("filename", "")
func get_domain() -> String:
var url = download_data.get("url", "")
return URLUtils.extract_domain(url) if url != "" else ""
func get_download_data() -> Dictionary:
return download_data
func _on_link_button_pressed() -> void:
DisplayServer.clipboard_set(download_data.get("url", ""))
func _on_file_button_pressed() -> void:
var file_path = download_data.get("file_path", "")
if file_path != "" and FileAccess.file_exists(file_path):
var file_dir = file_path.get_base_dir()
OS.shell_show_in_file_manager(file_dir)
else:
print("File not found: ", file_path)

View File

@@ -0,0 +1 @@
uid://c1qh0fqvvh8tq

View File

@@ -0,0 +1,211 @@
class_name DownloadManager
extends Node
const DOWNLOAD_DIALOG = preload("res://Scenes/UI/DownloadDialog.tscn")
const DOWNLOAD_PROGRESS = preload("res://Scenes/UI/DownloadProgress.tscn")
const DOWNLOADS_HISTORY = preload("res://Scenes/BrowserMenus/downloads.tscn")
var active_downloads: Dictionary = {}
var download_progress_container: VBoxContainer = null
var downloads_history_ui: DownloadsStore = null
var main_node: Main = null
func _init(main_reference: Main):
main_node = main_reference
func _ensure_download_progress_container():
if not download_progress_container:
download_progress_container = VBoxContainer.new()
download_progress_container.name = "DownloadProgressContainer"
download_progress_container.size_flags_horizontal = Control.SIZE_SHRINK_END
download_progress_container.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
var anchor_container = Control.new()
anchor_container.name = "DownloadAnchor"
anchor_container.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT)
anchor_container.position = Vector2(0, 130)
anchor_container.offset_left = 381 # 376 + 5px padding
anchor_container.add_child(download_progress_container)
main_node.add_child(anchor_container)
func handle_download_request(download_data: Dictionary):
print("Download requested: ", download_data)
var active_tab = main_node.get_active_tab()
if active_tab and active_tab.current_url:
download_data["current_site"] = URLUtils.extract_domain(active_tab.current_url)
else:
download_data["current_site"] = "Unknown site"
var dialog = DOWNLOAD_DIALOG.instantiate()
main_node.add_child(dialog)
dialog.download_confirmed.connect(_on_download_confirmed)
dialog.download_cancelled.connect(_on_download_cancelled)
dialog.show_download_dialog(download_data)
func _on_download_confirmed(download_data: Dictionary, save_path: String):
var download_id = download_data.get("id", "")
var url = download_data.get("url", "")
print(download_id, url)
if download_id.is_empty() or url.is_empty():
push_error("Invalid download data")
return
_start_download(download_id, url, save_path, download_data)
func _on_download_cancelled(download_data: Dictionary):
print("Download cancelled: ", download_data.get("filename", "Unknown"))
func _start_download(download_id: String, url: String, save_path: String, download_data: Dictionary):
_ensure_download_progress_container()
var progress_ui = DOWNLOAD_PROGRESS.instantiate()
download_progress_container.add_child(progress_ui)
progress_ui.setup_download(download_id, download_data)
progress_ui.download_cancelled.connect(_on_download_progress_cancelled)
var http_request = HTTPRequest.new()
http_request.name = "DownloadRequest_" + download_id
main_node.add_child(http_request)
active_downloads[download_id] = {
"http_request": http_request,
"save_path": save_path,
"progress_ui": progress_ui,
"start_time": Time.get_ticks_msec() / 1000.0,
"total_bytes": 0,
"downloaded_bytes": 0,
"url": download_data.get("url", ""),
"filename": download_data.get("filename", ""),
"current_site": download_data.get("current_site", "")
}
http_request.set_download_file(save_path)
http_request.request_completed.connect(func(result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
_on_download_completed(download_id, result, response_code, headers, body)
)
var headers = ["User-Agent: GURT Browser 1.0"]
var request_error = http_request.request(url, headers)
if request_error != OK:
var error_msg = "Failed to start download: " + str(request_error)
print(error_msg)
if progress_ui:
progress_ui.set_error(error_msg)
http_request.queue_free()
active_downloads.erase(download_id)
return
var timer = Timer.new()
timer.name = "ProgressTimer_" + download_id
timer.wait_time = 0.5
timer.timeout.connect(func(): _update_download_progress(download_id))
main_node.add_child(timer)
timer.start()
func _update_download_progress(download_id: String):
if not active_downloads.has(download_id):
return
var download_info = active_downloads[download_id]
var http_request = download_info.http_request
var progress_ui = download_info.progress_ui
if http_request and progress_ui:
var downloaded = http_request.get_downloaded_bytes()
var total = http_request.get_body_size()
download_info.downloaded_bytes = downloaded
download_info.total_bytes = total
var progress_percent = 0.0
if total > 0:
progress_percent = (float(downloaded) / float(total)) * 100.0
progress_ui.update_progress(progress_percent, downloaded, total)
func _on_download_completed(download_id: String, result: int, response_code: int, headers: PackedStringArray, body: PackedByteArray):
if not active_downloads.has(download_id):
return
var download_info = active_downloads[download_id]
var progress_ui = download_info.progress_ui
var save_path = download_info.save_path
var timer = main_node.get_node_or_null("ProgressTimer_" + download_id)
if timer:
timer.queue_free()
if response_code >= 200 and response_code < 300 and result == HTTPRequest.RESULT_SUCCESS:
var file = FileAccess.open(save_path, FileAccess.READ)
if file:
var file_size = file.get_length()
file.close()
if progress_ui:
progress_ui.set_completed(save_path)
_add_to_download_history(download_info, file_size, save_path)
print("Download completed: ", save_path)
else:
if progress_ui:
progress_ui.set_error("Downloaded file not found")
print("Downloaded file not found: ", save_path)
else:
var error_msg = "HTTP " + str(response_code) if response_code >= 400 else "Request failed (" + str(result) + ")"
if progress_ui:
progress_ui.set_error(error_msg)
print("Download failed: ", error_msg)
if FileAccess.file_exists(save_path):
DirAccess.remove_absolute(save_path)
download_info.http_request.queue_free()
active_downloads.erase(download_id)
func _on_download_progress_cancelled(download_id: String):
if not active_downloads.has(download_id):
return
var download_info = active_downloads[download_id]
if download_info.http_request:
download_info.http_request.cancel_request()
download_info.http_request.queue_free()
var timer = main_node.get_node_or_null("ProgressTimer_" + download_id)
if timer:
timer.queue_free()
active_downloads.erase(download_id)
print("Download cancelled: ", download_id)
func show_downloads_history():
_ensure_downloads_history_ui()
downloads_history_ui.popup_centered_ratio(0.8)
func _add_to_download_history(download_info: Dictionary, file_size: int, file_path: String):
_ensure_downloads_history_ui()
var history_data = {
"url": download_info.url,
"filename": download_info.filename,
"size": file_size,
"timestamp": Time.get_unix_time_from_system(),
"file_path": file_path,
"current_site": download_info.get("current_site", "")
}
downloads_history_ui.add_download_entry(history_data)
func _ensure_downloads_history_ui():
if not downloads_history_ui:
downloads_history_ui = DOWNLOADS_HISTORY.instantiate()
downloads_history_ui.visible = false
main_node.add_child(downloads_history_ui)

View File

@@ -0,0 +1 @@
uid://omjynclhfe8u

View File

@@ -0,0 +1,129 @@
class_name DownloadProgress
extends PanelContainer
signal download_cancelled(download_id: String)
@onready var filename_label: Label = $HBox/VBox/FilenameLabel
@onready var progress_bar: ProgressBar = $HBox/VBox/ProgressBar
@onready var status_label: Label = $HBox/VBox/StatusLabel
@onready var cancel_button: Button = $HBox/CancelButton
var download_id: String = ""
var download_data: Dictionary = {}
var start_time: float = 0.0
func _ready():
progress_bar.value = 0
status_label.text = "Starting download..."
func setup_download(id: String, data: Dictionary):
download_id = id
download_data = data
start_time = Time.get_ticks_msec() / 1000.0
var filename = data.get("filename", "Unknown file")
filename_label.text = filename
status_label.text = "Starting download..."
progress_bar.value = 0
_animate_entrance()
func _animate_entrance():
if not is_inside_tree():
return
var download_container = get_parent()
var anchor_container = download_container.get_parent() if download_container else null
if anchor_container and anchor_container.name == "DownloadAnchor" and download_container.get_child_count() == 1:
var tween = create_tween()
if tween:
var tween_property = tween.tween_property(anchor_container, "offset_left", -381, 0.3)
if tween_property:
tween_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
else:
call_deferred("_animate_individual_entrance")
func _animate_individual_entrance():
if not is_inside_tree():
return
var original_x = position.x
position.x += 400 # Move off-screen to the right
var tween = create_tween()
if tween:
var slide_property = tween.tween_property(self, "position:x", original_x, 0.3)
if slide_property:
slide_property.set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK)
func update_progress(progress_percent: float, bytes_downloaded: int = 0, total_bytes: int = 0):
progress_bar.value = progress_percent
var elapsed_time = (Time.get_ticks_msec() / 1000.0) - start_time
var status_text = ""
if total_bytes > 0:
var speed_bps = bytes_downloaded / elapsed_time if elapsed_time > 0 else 0
var remaining_bytes = total_bytes - bytes_downloaded
var eta_seconds = remaining_bytes / speed_bps if speed_bps > 0 else 0
status_text = NetworkRequest.format_bytes(bytes_downloaded) + " / " + NetworkRequest.format_bytes(total_bytes)
if speed_bps > 0:
status_text += " (" + NetworkRequest.format_bytes(int(speed_bps)) + "/s)"
if eta_seconds > 0 and eta_seconds < 3600:
status_text += " - %d seconds left" % int(eta_seconds)
else:
status_text = "%.0f%% complete" % progress_percent
status_label.text = status_text
func set_completed(file_path: String):
progress_bar.value = 100
status_label.text = "Download complete: " + file_path.get_file()
cancel_button.text = ""
cancel_button.disabled = true
await get_tree().create_timer(2.0).timeout
_animate_exit()
func set_error(error_message: String):
status_label.text = "Error: " + error_message
cancel_button.text = ""
progress_bar.modulate = Color.RED
await get_tree().create_timer(4.0).timeout
_animate_exit()
func _animate_exit():
if not is_inside_tree():
queue_free()
return
var download_container = get_parent()
var anchor_container = download_container.get_parent() if download_container else null
var is_last_download = download_container and download_container.get_child_count() == 1
var tween = create_tween()
if tween:
var slide_property = tween.tween_property(self, "position:x", position.x + 400, 0.25)
if slide_property:
slide_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK)
await tween.finished
if is_last_download and anchor_container and anchor_container.name == "DownloadAnchor":
var container_tween = create_tween()
if container_tween:
var container_property = container_tween.tween_property(anchor_container, "offset_left", 381, 0.25)
if container_property:
container_property.set_ease(Tween.EASE_IN).set_trans(Tween.TRANS_BACK)
await container_tween.finished
queue_free()
func _on_cancel_pressed():
download_cancelled.emit(download_id)
_animate_exit()

View File

@@ -0,0 +1 @@
uid://kj6oda7sey5s

View File

@@ -0,0 +1,76 @@
class_name DownloadsStore
extends PopupPanel
@onready var search_line_edit: LineEdit = $Main/LineEdit
@onready var download_entry_container: VBoxContainer = $Main/PanelContainer2/ScrollContainer/DownloadEntryContainer
const DOWNLOAD_ENTRY = preload("res://Scenes/BrowserMenus/download_entry.tscn")
var save_path = "user://downloads_history.json"
var download_entries: Array[DownloadEntry] = []
func _ready():
search_line_edit.text_changed.connect(_on_search_text_changed)
load_download_history()
func add_download_entry(download_data: Dictionary):
var entry = DOWNLOAD_ENTRY.instantiate()
download_entry_container.add_child(entry)
entry.setup_download_entry(download_data)
download_entries.append(entry)
save_download_history()
func _on_search_text_changed(new_text: String):
var search_term = new_text.to_lower().strip_edges()
for entry in download_entries:
if search_term.is_empty():
entry.visible = true
else:
var filename = entry.get_filename().to_lower()
var domain = entry.get_domain().to_lower()
entry.visible = filename.contains(search_term) or domain.contains(search_term)
func load_download_history():
if not FileAccess.file_exists(save_path):
return
var file = FileAccess.open(save_path, FileAccess.READ)
if not file:
print("Could not open downloads history file for reading")
return
var json_text = file.get_as_text()
file.close()
var json = JSON.new()
var parse_result = json.parse(json_text)
if parse_result != OK:
print("Error parsing downloads history JSON")
return
var downloads_data = json.data
if downloads_data is Array:
for download_data in downloads_data:
add_download_entry(download_data)
func save_download_history():
var file = FileAccess.open(save_path, FileAccess.WRITE)
if not file:
print("Could not open downloads history file for writing")
return
var downloads_data = get_download_data_array()
var json_text = JSON.stringify(downloads_data)
file.store_string(json_text)
file.close()
func get_download_data_array() -> Array[Dictionary]:
var data_array: Array[Dictionary] = []
for entry in download_entries:
data_array.append(entry.get_download_data())
return data_array

View File

@@ -0,0 +1 @@
uid://cp3geyxt0f8tn

View File

@@ -25,6 +25,10 @@ func _input(_event: InputEvent) -> void:
# CTRL+H - History
_on_options_menu_id_pressed(4)
get_viewport().set_input_as_handled()
elif _event.keycode == KEY_J:
# CTRL+J - Downloads
_on_options_menu_id_pressed(5)
get_viewport().set_input_as_handled()
func _on_options_menu_id_pressed(id: int) -> void:
if id == 0: # new tab
@@ -36,6 +40,8 @@ func _on_options_menu_id_pressed(id: int) -> void:
OS.create_process(OS.get_executable_path(), ["--incognito"])
if id == 4: # history
show_history()
if id == 5: # downloads
show_downloads()
if id == 10: # exit
get_tree().quit()
@@ -53,3 +59,6 @@ func show_history() -> void:
func _on_history_closed() -> void:
if history_scene:
history_scene.hide()
func show_downloads() -> void:
main.download_manager.show_downloads_history()