extends Window
## Modal media viewer: full-size image preview, room file gallery, download management.
@onready var file_name_label: Label = %FileNameLabel
@onready var download_btn: Button = %DownloadBtn
@onready var close_btn: Button = %CloseBtn
@onready var tabs: TabContainer = %TabContainer
@onready var image_display: TextureRect = %ImageDisplay
@onready var video_player: Control = %VideoPlayer # VideoPlayback (GoZen GDExtension)
@onready var gallery_grid: GridContainer = %GalleryGrid
@onready var downloads_list: VBoxContainer = %DownloadsList
@onready var status_label: Label = %StatusLabel
var _room_id: String = ""
var _file_id: String = ""
var _file_name: String = ""
var _video_link: String = "" # Full video URL for download+play
var _texture_cache: KeetTextureCache = KeetTextureCache.new()
var _gallery_files: Array = []
const GALLERY_THUMB_SIZE: int = 80
const IMAGE_TYPES: Array[String] = ["image/webp", "image/png", "image/jpeg", "image/jpg", "image/gif"]
const VIDEO_TYPES: Array[String] = ["video/mp4", "video/webm", "video/ogg", "video/quicktime", "video/x-matroska"]
func _ready() -> void:
close_btn.pressed.connect(_on_close)
close_requested.connect(_on_close)
download_btn.pressed.connect(_on_download)
KeetService.file_mgr.file_preview_loaded.connect(_on_preview_loaded)
KeetService.file_mgr.files_loaded.connect(_on_files_loaded)
KeetService.file_mgr.download_started.connect(_on_download_started)
KeetService.file_mgr.download_progress.connect(_on_download_progress)
KeetService.file_mgr.file_update.connect(_on_file_update)
## Show a specific file in the Image tab.
func show_file(room_id: String, file_id: String) -> void:
# Stop any previous video playback
if video_player and video_player.is_open():
video_player.close()
_room_id = room_id
_file_id = file_id
_file_name = ""
_video_link = ""
tabs.current_tab = 0 # Image tab
file_name_label.text = "Loading..."
status_label.text = ""
image_display.texture = null
download_btn.text = "Download"
download_btn.disabled = false
popup_centered()
# Request medium/large preview
KeetService.file_mgr.get_preview(room_id, file_id, "medium")
## Show the gallery of all files in a room.
func show_gallery(room_id: String) -> void:
_room_id = room_id
_file_id = ""
tabs.current_tab = 2 # Gallery tab
file_name_label.text = "Room Files"
status_label.text = "Loading files..."
# Clear existing gallery
for child: Node in gallery_grid.get_children():
child.queue_free()
popup_centered()
KeetService.file_mgr.list_files(room_id)
func _on_close() -> void:
if video_player and video_player.is_open():
video_player.close()
hide()
func _on_download() -> void:
if _room_id == "" or _file_id == "":
return
# Video: toggle play/pause if already playing, otherwise start inline
if _video_link != "":
if video_player and video_player.is_open():
if video_player.is_playing:
video_player.pause()
download_btn.text = "▶ Resume"
status_label.text = "Paused"
else:
video_player.play()
download_btn.text = "⏸ Pause"
status_label.text = ""
return
_play_video_inline(_video_link)
return
status_label.text = "Starting download..."
KeetService.file_mgr.start_download(_room_id, _file_id)
func _on_preview_loaded(room_id: String, file_id: String, data: Dictionary) -> void:
if room_id != _room_id:
return
print("[media_viewer] preview data keys: %s" % str(data.keys()))
print("[media_viewer] preview data: %s" % str(data).substr(0, 300))
var name_str: String = str(data.get("name", "file"))
# Handle null from JSON — data.get returns null variant, str(null) = "<null>"
var raw_link: Variant = data.get("httpLink", null)
var http_link: String = "" if raw_link == null else str(raw_link)
# Get fullHttpLink separately (actual file URL, not preview thumbnail)
var raw_full_link: Variant = data.get("fullHttpLink", null)
var full_http_link: String = "" if raw_full_link == null else str(raw_full_link)
# Fall back to fullHttpLink if no preview link available
if http_link == "":
http_link = full_http_link
# Update display for the file being viewed
if file_id == _file_id:
file_name_label.text = name_str
_file_name = name_str
var raw_type: Variant = data.get("type", null)
var file_type: String = "" if raw_type == null else str(raw_type)
var is_video: bool = _is_video_type(file_type, name_str)
if is_video:
# Store the full video link for the play button
_video_link = full_http_link if full_http_link != "" else http_link
download_btn.text = "▶ Play"
if http_link != "":
status_label.text = "Click ▶ Play to watch"
_fetch_image_via_proxy(http_link, file_id)
else:
status_label.text = "Click ▶ Play to download and watch"
elif http_link != "":
_video_link = ""
status_label.text = "Fetching preview..."
_fetch_image_via_proxy(http_link, file_id)
else:
_video_link = ""
status_label.text = "No preview available"
var keys: Variant = data.get("_entryKeys", null)
if keys is Array:
status_label.text = "No preview (keys: %s)" % str(keys)
func _on_files_loaded(room_id: String, files: Array) -> void:
if room_id != _room_id:
return
_gallery_files = files
status_label.text = "%d files" % files.size()
# Clear and rebuild gallery grid
for child: Node in gallery_grid.get_children():
child.queue_free()
for file_entry: Variant in files:
if file_entry is Dictionary:
_add_gallery_item(file_entry)
func _add_gallery_item(file_entry: Dictionary) -> void:
var file_id: String = str(file_entry.get("id", file_entry.get("fileId", "")))
var file_name: String = str(file_entry.get("name", file_entry.get("fileName", "file")))
var file_type: String = str(file_entry.get("mimeType", file_entry.get("type", "")))
var is_image: bool = false
for img_type: String in IMAGE_TYPES:
if file_type.begins_with(img_type) or file_name.get_extension().to_lower() in ["png", "jpg", "jpeg", "webp", "gif"]:
is_image = true
break
var is_video: bool = _is_video_type(file_type, file_name)
var item := Button.new()
item.custom_minimum_size = Vector2(GALLERY_THUMB_SIZE + 20, GALLERY_THUMB_SIZE + 30)
item.clip_text = true
if is_image:
# Create a VBox with icon placeholder + name
var vbox := VBoxContainer.new()
vbox.size_flags_horizontal = Control.SIZE_EXPAND_FILL
var tex_rect := TextureRect.new()
tex_rect.custom_minimum_size = Vector2(GALLERY_THUMB_SIZE, GALLERY_THUMB_SIZE)
tex_rect.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_CENTERED
tex_rect.name = "Thumb_%s" % file_id.substr(0, 8)
vbox.add_child(tex_rect)
var label := Label.new()
label.text = file_name
label.clip_text = true
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
vbox.add_child(label)
item.add_child(vbox)
# Request preview httpLink for gallery thumb
KeetService.file_mgr.get_preview(_room_id, file_id, "small", func(data: Dictionary):
var link: String = str(data.get("httpLink", ""))
if link != "" and is_instance_valid(tex_rect):
_fetch_image_for_widget(link, file_id, tex_rect)
)
elif is_video:
item.text = "▶ " + file_name
else:
item.text = file_name
item.pressed.connect(func():
_file_id = file_id
tabs.current_tab = 0
file_name_label.text = file_name
image_display.texture = null
status_label.text = "Loading..."
KeetService.file_mgr.get_preview(_room_id, file_id, "medium")
)
gallery_grid.add_child(item)
func _on_download_started(room_id: String, file_id: String) -> void:
if room_id != _room_id:
return
if file_id == _file_id:
status_label.text = "Download started"
_add_download_row(file_id, "downloading...")
func _on_download_progress(room_id: String, file_id: String, progress: Dictionary) -> void:
if room_id != _room_id:
return
var pct: float = float(progress.get("progress", 0)) * 100.0
var done: bool = progress.get("done", false)
if file_id == _file_id:
if done:
status_label.text = "Download complete"
else:
status_label.text = "Downloading: %.0f%%" % pct
_update_download_row(file_id, pct, done)
func _on_file_update(room_id: String, data: Dictionary) -> void:
if room_id != _room_id:
return
# Refresh gallery on file changes
if tabs.current_tab == 2:
KeetService.file_mgr.list_files(room_id)
func _add_download_row(file_id: String, status: String) -> void:
# Check if row already exists
for child: Node in downloads_list.get_children():
if child.name == "dl_%s" % file_id.substr(0, 12):
return
var row := HBoxContainer.new()
row.name = "dl_%s" % file_id.substr(0, 12)
var label := Label.new()
label.text = file_id.substr(0, 12)
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
row.add_child(label)
var progress_label := Label.new()
progress_label.name = "Progress"
progress_label.text = status
row.add_child(progress_label)
downloads_list.add_child(row)
func _update_download_row(file_id: String, pct: float, done: bool) -> void:
var row_name: String = "dl_%s" % file_id.substr(0, 12)
for child: Node in downloads_list.get_children():
if child.name == row_name:
var progress_label: Label = child.get_node_or_null("Progress")
if progress_label:
if done:
progress_label.text = "complete"
else:
progress_label.text = "%.0f%%" % pct
return
func _is_video_type(file_type: String, file_name: String) -> bool:
for vid_type: String in VIDEO_TYPES:
if file_type.begins_with(vid_type):
return true
if file_name.get_extension().to_lower() in ["mp4", "webm", "ogg", "mov", "mkv"]:
return true
return false
## Play video inline using GoZen VideoPlayback (direct streaming from keet-core blob server).
func _play_video_inline(http_link: String) -> void:
tabs.current_tab = 1 # Video tab
status_label.text = "Loading video..."
download_btn.disabled = true
print("[media_viewer] playing video: %s" % http_link.substr(0, 100))
video_player.video_loaded.connect(_on_video_loaded, CONNECT_ONE_SHOT)
video_player.video_ended.connect(_on_video_ended, CONNECT_ONE_SHOT)
video_player.set_video_path(http_link)
# Timeout fallback — if streaming fails, download to /tmp and play locally
get_tree().create_timer(10.0).timeout.connect(func():
if not video_player.is_open():
print("[media_viewer] stream timeout, falling back to download")
_download_and_play_local(http_link, _file_name)
)
func _on_video_loaded() -> void:
status_label.text = ""
download_btn.disabled = false
download_btn.text = "⏸ Pause"
video_player.play()
func _on_video_ended() -> void:
download_btn.text = "▶ Replay"
status_label.text = "Video ended"
## Fallback: download video to /tmp then play locally via GoZen.
func _download_and_play_local(http_link: String, file_name_str: String) -> void:
var safe_name: String = file_name_str.get_file().replace(" ", "_")
if safe_name == "":
safe_name = "video.mp4"
var tmp_path: String = "/tmp/keet-video-%s" % safe_name
status_label.text = "Downloading video..."
var http_req := HTTPRequest.new()
http_req.download_file = tmp_path
add_child(http_req)
http_req.request_completed.connect(func(result: int, response_code: int, _headers: PackedStringArray, _body: PackedByteArray):
http_req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS or (response_code != 200 and response_code != 206):
print("[media_viewer] video download failed: result=%d code=%d" % [result, response_code])
status_label.text = "Video download failed (%d)" % response_code
download_btn.disabled = false
return
print("[media_viewer] video downloaded to %s, playing locally" % tmp_path)
video_player.video_loaded.connect(_on_video_loaded, CONNECT_ONE_SHOT)
video_player.video_ended.connect(_on_video_ended, CONNECT_ONE_SHOT)
video_player.set_video_path(tmp_path)
)
print("[media_viewer] downloading video: %s -> %s" % [http_link.substr(0, 80), tmp_path])
http_req.request(http_link)
## Fetch an image directly from keet-core's blob server and display in the main image view.
func _fetch_image_via_proxy(http_link: String, file_id: String) -> void:
var cached: ImageTexture = _texture_cache.get_cached(file_id)
if cached != null:
image_display.texture = cached
status_label.text = "Preview loaded (cached)"
return
var http_req := HTTPRequest.new()
add_child(http_req)
http_req.request_completed.connect(func(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
http_req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
var body_str: String = body.get_string_from_utf8().substr(0, 200)
print("[media_viewer] fetch error: result=%d code=%d body=%s" % [result, response_code, body_str])
status_label.text = "Preview fetch failed (%d)" % response_code
return
var tex: ImageTexture = _texture_cache.decode_bytes_and_cache(file_id, body)
if tex != null:
image_display.texture = tex
status_label.text = ""
else:
status_label.text = "Failed to decode image"
)
print("[media_viewer] fetching: %s" % http_link.substr(0, 80))
http_req.request(http_link)
## Fetch an image directly from keet-core's blob server and display in a specific TextureRect widget.
func _fetch_image_for_widget(http_link: String, file_id: String, tex_rect: TextureRect) -> void:
var cached: ImageTexture = _texture_cache.get_cached(file_id)
if cached != null and is_instance_valid(tex_rect):
tex_rect.texture = cached
return
var http_req := HTTPRequest.new()
add_child(http_req)
http_req.request_completed.connect(func(result: int, response_code: int, _headers: PackedStringArray, body: PackedByteArray):
http_req.queue_free()
if result != HTTPRequest.RESULT_SUCCESS or response_code != 200:
return
var tex: ImageTexture = _texture_cache.decode_bytes_and_cache(file_id, body)
if tex != null and is_instance_valid(tex_rect):
tex_rect.texture = tex
)
http_req.request(http_link)