OverviewFilesCommitsIssuesDocsReleasesPeers

godot-keet / scripts / media_viewer.gd

398 lines · gd ref:
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)