mirror of
https://github.com/Cuteminded/jellyframe.git
synced 2026-10-23 05:00:37 +02:00
Replaces custom SVG UI icons with Material Symbols assets across player controls, details, and sidebar navigation, including updated import metadata and icon mappings for library types and skip controls. Adds in-player control labels that appear on hover/focus (including timeline and pause/play state changes), plus a new `test_player_tooltips` coverage path wired into Linux and Windows CI. Updates README usage/licensing notes and export presets to bundle Material icon attribution/license files.
189 lines
7.5 KiB
GDScript
189 lines
7.5 KiB
GDScript
class_name DetailsView
|
|
extends VBoxContainer
|
|
|
|
signal play_requested(item: Dictionary)
|
|
signal back_requested
|
|
|
|
var item: Dictionary
|
|
var api: JellyfinClient
|
|
var initial_season: Dictionary = {}
|
|
var artwork: ArtworkLoader
|
|
var scroll: ScrollContainer
|
|
var body: VBoxContainer
|
|
var series_browser: SeriesBrowser
|
|
var play_button: Button
|
|
var back_button: Button
|
|
var title_label: Label
|
|
var overview_label: Label
|
|
var poster: TextureRect
|
|
var backdrop: TextureRect
|
|
var cast_carousel: CastCarousel
|
|
|
|
func _ready() -> void:
|
|
add_theme_constant_override("separation", 18)
|
|
if artwork == null:
|
|
artwork = ArtworkLoader.new()
|
|
artwork.api = api
|
|
add_child(artwork)
|
|
var navigation := HBoxContainer.new()
|
|
add_child(navigation)
|
|
back_button = Widgets.button("Back to library", func(): back_requested.emit(), navigation)
|
|
scroll = ScrollContainer.new()
|
|
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
|
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
|
scroll.follow_focus = true
|
|
add_child(scroll)
|
|
body = VBoxContainer.new()
|
|
body.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
body.add_theme_constant_override("separation", 28)
|
|
scroll.add_child(body)
|
|
_build_header()
|
|
_build_information()
|
|
if item.get("Type", "") == "Series":
|
|
series_browser = SeriesBrowser.new()
|
|
series_browser.api = api
|
|
series_browser.artwork = artwork
|
|
series_browser.series_id = str(item.Id)
|
|
series_browser.selected_season = initial_season
|
|
series_browser.play_requested.connect(func(episode: Dictionary): play_requested.emit(episode))
|
|
series_browser.section_changed.connect(_scroll_to_series)
|
|
body.add_child(series_browser)
|
|
_build_people()
|
|
|
|
func _build_header() -> void:
|
|
var hero := PanelContainer.new()
|
|
hero.add_theme_stylebox_override("panel", Widgets.surface(Color("162133"), 18))
|
|
body.add_child(hero)
|
|
backdrop = _image(hero)
|
|
artwork.fetch(backdrop, item, true)
|
|
var shade := _image(hero)
|
|
var gradient := Gradient.new()
|
|
gradient.colors = PackedColorArray([Color(0.04, 0.065, 0.10, 0.97), Color(0.04, 0.065, 0.10, 0.82)])
|
|
var texture := GradientTexture2D.new()
|
|
texture.gradient = gradient
|
|
shade.texture = texture
|
|
var margin := MarginContainer.new()
|
|
for side in ["left", "right", "top", "bottom"]:
|
|
margin.add_theme_constant_override("margin_" + side, 24)
|
|
hero.add_child(margin)
|
|
var row := HBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 28)
|
|
margin.add_child(row)
|
|
var frame := PanelContainer.new()
|
|
frame.custom_minimum_size = Vector2(230, 345)
|
|
frame.size_flags_vertical = Control.SIZE_SHRINK_BEGIN
|
|
var frame_style := Widgets.surface(Color("25334a"), 12)
|
|
frame_style.set_border_width_all(1)
|
|
frame_style.border_color = Color(1, 1, 1, 0.15)
|
|
frame.add_theme_stylebox_override("panel", frame_style)
|
|
row.add_child(frame)
|
|
frame.visible = size.x >= 760
|
|
resized.connect(func(): frame.visible = size.x >= 760)
|
|
var fallback := Widgets.label("No poster available", frame, 20)
|
|
fallback.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
|
fallback.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
|
poster = _image(frame)
|
|
poster.accessibility_name = "Poster for " + str(item.get("Name", "Untitled"))
|
|
artwork.fetch(poster, item)
|
|
var summary := VBoxContainer.new()
|
|
summary.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
summary.add_theme_constant_override("separation", 16)
|
|
row.add_child(summary)
|
|
_muted("TV series" if item.get("Type", "") == "Series" else "Movie", summary, 20)
|
|
title_label = _wrapped(DetailsMetadata.title(item), summary, 42)
|
|
var facts := HFlowContainer.new()
|
|
facts.add_theme_constant_override("h_separation", 10)
|
|
facts.add_theme_constant_override("v_separation", 10)
|
|
summary.add_child(facts)
|
|
for fact in DetailsMetadata.facts(item):
|
|
var badge := PanelContainer.new()
|
|
badge.add_theme_stylebox_override("panel", Widgets.surface(Color("202a3d"), 8, 10))
|
|
facts.add_child(badge)
|
|
Widgets.label(fact, badge, 20)
|
|
var genres: Array = item.get("Genres", []) if item.get("Genres") else []
|
|
if not genres.is_empty(): _muted(" / ".join(genres), summary)
|
|
var taglines: Array = item.get("Taglines", []) if item.get("Taglines") else []
|
|
if not taglines.is_empty(): _wrapped(str(taglines[0]), summary, 24)
|
|
_build_playback(summary)
|
|
|
|
func _build_playback(parent: VBoxContainer) -> void:
|
|
if item.get("Type", "") == "Series":
|
|
return
|
|
var user: Dictionary = item.get("UserData", {}) if item.get("UserData") else {}
|
|
var position := int(user.get("PlaybackPositionTicks", 0))
|
|
var actions := HBoxContainer.new()
|
|
parent.add_child(actions)
|
|
play_button = Widgets.button("Resume" if position > 0 else "Play movie", func(): play_requested.emit(item), actions)
|
|
play_button.custom_minimum_size = Vector2(200, 64)
|
|
play_button.icon = preload("res://assets/icons/material/play_arrow.svg")
|
|
play_button.add_theme_constant_override("icon_max_width", 28)
|
|
play_button.add_theme_constant_override("h_separation", 12)
|
|
for state in ["icon_normal_color", "icon_hover_color", "icon_pressed_color", "icon_focus_color"]:
|
|
play_button.add_theme_color_override(state, Color("071c20"))
|
|
Widgets.accent(play_button)
|
|
var focus := Widgets.surface(Color("6de3cf"), 12, 16)
|
|
focus.set_border_width_all(3)
|
|
focus.border_color = Color("071c20")
|
|
play_button.add_theme_stylebox_override("focus", focus)
|
|
if position > 0:
|
|
_muted("Continue from " + DetailsMetadata.duration(position), parent, 20)
|
|
|
|
func _build_information() -> void:
|
|
var section := VBoxContainer.new()
|
|
section.add_theme_constant_override("separation", 14)
|
|
body.add_child(section)
|
|
Widgets.label("Overview", section, 28)
|
|
overview_label = _wrapped(str(item.get("Overview") if item.get("Overview") else "No description available."), section)
|
|
overview_label.add_theme_constant_override("line_spacing", 7)
|
|
var media := DetailsMetadata.streams(item)
|
|
var studios: Array = item.get("Studios", []) if item.get("Studios") else []
|
|
var studio_names: Array[String] = []
|
|
for studio in studios:
|
|
if studio.get("Name"): studio_names.append(str(studio.Name))
|
|
if not studio_names.is_empty(): media["Studios"] = ", ".join(studio_names)
|
|
if media.is_empty(): return
|
|
var info := VBoxContainer.new()
|
|
info.add_theme_constant_override("separation", 12)
|
|
body.add_child(info)
|
|
for key in media:
|
|
var row := HBoxContainer.new()
|
|
row.add_theme_constant_override("separation", 18)
|
|
info.add_child(row)
|
|
var label := _muted(str(key), row, 20)
|
|
label.custom_minimum_size.x = 90
|
|
label.size_flags_horizontal = Control.SIZE_SHRINK_BEGIN
|
|
_wrapped(str(media[key]), row, 20)
|
|
|
|
func _build_people() -> void:
|
|
var people: Array = item.get("People", []) if item.get("People") else []
|
|
if people.is_empty(): return
|
|
cast_carousel = CastCarousel.new()
|
|
cast_carousel.people = people
|
|
cast_carousel.artwork = artwork
|
|
body.add_child(cast_carousel)
|
|
|
|
func _image(parent: Node) -> TextureRect:
|
|
var image := TextureRect.new()
|
|
image.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
|
image.stretch_mode = TextureRect.STRETCH_KEEP_ASPECT_COVERED
|
|
image.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
parent.add_child(image)
|
|
return image
|
|
|
|
func _scroll_to_series() -> void:
|
|
await get_tree().process_frame
|
|
await get_tree().process_frame
|
|
if is_inside_tree(): scroll.scroll_vertical = int(series_browser.position.y)
|
|
|
|
func _wrapped(text: String, parent: Node, font_size: int = 22) -> Label:
|
|
var label := Widgets.label(text, parent, font_size)
|
|
label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
|
label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
|
|
label.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
|
return label
|
|
|
|
func _muted(text: String, parent: Node, font_size: int = 22) -> Label:
|
|
var label := _wrapped(text, parent, font_size)
|
|
label.add_theme_color_override("font_color", Color("b0bbce"))
|
|
return label
|