Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Library): add interface and menus to select install location #419

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 15 additions & 7 deletions core/global/install_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -89,12 +89,16 @@ func _process_install_queue() -> void:
# Install the given application using the given provider
install_started.emit(req)
var result: Array
if req._type == REQUEST_TYPE.INSTALL:
req.provider.install(req.item)
result = await req.provider.install_completed
else:
req.provider.update(req.item)
result = await req.provider.update_completed
match req._type:
REQUEST_TYPE.INSTALL:
req.provider.install_to(req.item, req.location, req.options)
result = await req.provider.install_completed
REQUEST_TYPE.UPDATE:
req.provider.update(req.item)
result = await req.provider.update_completed
_:
logger.warn("Unknown request type:", req._type)
result = [req.item, false]
req.success = result[1]
logger.info("Install of '" + req.item.name + "' completed with success: " + str(req.success))

Expand All @@ -116,10 +120,14 @@ class Request extends RefCounted:
signal completed(success: bool)
var provider: Library
var item: LibraryLaunchItem
var location: Library.InstallLocation
var options: Dictionary
var progress: float
var success: bool
var _type: REQUEST_TYPE

func _init(library_provider: Library, launch_item: LibraryLaunchItem) -> void:
func _init(library_provider: Library, launch_item: LibraryLaunchItem, to_location: Library.InstallLocation = null, opts: Dictionary = {}) -> void:
provider = library_provider
item = launch_item
location = to_location
options = opts
96 changes: 82 additions & 14 deletions core/systems/library/library.gd
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ signal install_progressed(item: LibraryLaunchItem, percent_completed: float)
signal launch_item_added(item: LibraryLaunchItem)
## Should be emitted when a library item was removed from the library
signal launch_item_removed(item: LibraryLaunchItem)
## Should be emitted when a library item is moved to a different install location
signal move_completed(item: LibraryLaunchItem, success: bool)
## Should be emitted when a library item move is progressing
signal move_progressed(item: LibraryLaunchItem, percent_completed: float)

var LibraryManager := load("res://core/global/library_manager.tres") as LibraryManager
var LibraryManager := load("res://core/global/library_manager.tres") as LibraryManager # TODO: Remove this
var library_manager := load("res://core/global/library_manager.tres") as LibraryManager

## Unique identifier for the library
@export var library_id: String
Expand All @@ -45,7 +50,7 @@ var LibraryManager := load("res://core/global/library_manager.tres") as LibraryM

func _init() -> void:
add_to_group("library")
ready.connect(LibraryManager.register_library.bind(self))
ready.connect(library_manager.register_library.bind(self))


# Called when the node enters the scene tree for the first time.
Expand All @@ -71,36 +76,99 @@ func get_library_launch_items() -> Array[LibraryLaunchItem]:
return []


## Returns an array of available install locations for this library provider.
## This method should be overridden in the child class.
## Example:
## [codeblock]
## func get_available_install_locations() -> Array[InstallLocation]:
## var location := InstallLocation.new()
## location.name = "/"
## return [location]
## [/codeblock]
func get_available_install_locations(item: LibraryLaunchItem = null) -> Array[InstallLocation]:
return []


## Returns an array of install options for the given [LibraryLaunchItem].
## Install options are arbitrary and are provider-specific. They allow the user
## to select things like the language of a game to install, etc.
## Example:
## [codeblock]
## func get_install_options(item: LibraryLaunchItem) -> Array[InstallOption]:
## var option := InstallOption.new()
## option.id = "lang"
## option.name = "Language"
## option.description = "Language of the game to install"
## option.values = ["english", "spanish"]
## option.value_type = TYPE_STRING
## return [option]
## [/codeblock]
func get_install_options(item: LibraryLaunchItem) -> Array[InstallOption]:
return []


## This method should be overridden if the library requires executing callbacks
## at certain points in an app's lifecycle, such as when an app is starting or
## stopping.
func get_app_lifecycle_hooks() -> Array[AppLifecycleHook]:
var hooks: Array[AppLifecycleHook]
return hooks


## [DEPRECATED]
## Installs the given library item. This method should be overriden in the
## child class, if it supports it.
func install(item: LibraryLaunchItem) -> void:
pass


## Installs the given library item to the given location. This method should be
## overridden in the child class, if it supports it.
func install_to(item: LibraryLaunchItem, location: InstallLocation = null, options: Dictionary = {}) -> void:
install(item)


## Updates the given library item. This method should be overriden in the
## child class, if it supports it.
func update(item: LibraryLaunchItem) -> void:
pass


## Should return true if the given library item has an update available
func has_update(item: LibraryLaunchItem) -> bool:
return false


## Uninstalls the given library item. This method should be overriden in the
## child class if it supports it.
func uninstall(item: LibraryLaunchItem) -> void:
pass


## Should return true if the given library item has an update available
func has_update(item: LibraryLaunchItem) -> bool:
return false


## This method should be overridden if the library requires executing callbacks
## at certain points in an app's lifecycle, such as when an app is starting or
## stopping.
func get_app_lifecycle_hooks() -> Array[AppLifecycleHook]:
var hooks: Array[AppLifecycleHook]
return hooks
## Move the given library item to the given install location. This method should
## be overriden in the child class if it supports it.
func move(item: LibraryLaunchItem, to_location: InstallLocation) -> void:
pass


func _exit_tree() -> void:
LibraryManager.unregister_library(self)
library_manager.unregister_library(self)


## InstallLocation defines a place where a [LibraryLaunchItem] can be installed.
class InstallLocation extends RefCounted:
var id: String
var name: String
var description: String
var icon: Texture2D
var total_space_mb: int
var free_space_mb: int


## InstallOption defines an arbitrary install option for a [LibraryLaunchitem].
class InstallOption extends RefCounted:
var id: String
var name: String
var description: String
var values: Array[Variant]
var value_type: int
82 changes: 61 additions & 21 deletions core/ui/card_ui/launch/game_launch_menu.gd
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,17 @@ var logger := Log.get_logger("GameLaunchMenu")

@export var launch_item: LibraryLaunchItem

@onready var banner: TextureRect = $%BannerTexture
@onready var logo: TextureRect = $%LogoTexture
@onready var launch_button := $%LaunchButton
@onready var loading: Control = $%LoadingAnimation
@onready var player := $%AnimationPlayer
@onready var progress_container := $%ProgressContainer
@onready var progress_bar: ProgressBar = $%ProgressBar
@onready var delete_container := $%DeleteContainer
@onready var delete_button := $%DeleteButton
@onready var banner := $%BannerTexture as TextureRect
@onready var logo := $%LogoTexture as TextureRect
@onready var launch_button := $%LaunchButton as CardButton
@onready var loading := $%LoadingAnimation as Control
@onready var player := $%AnimationPlayer as AnimationPlayer
@onready var progress_container := $%ProgressContainer as MarginContainer
@onready var progress_bar := $%ProgressBar as ProgressBar
@onready var delete_container := $%DeleteContainer as MarginContainer
@onready var delete_button := $%DeleteButton as CardIconButton
@onready var location_menu := $%InstallLocationDialog as InstallLocationDialog
@onready var options_menu := $%InstallOptionsDialog as InstallOptionDialog


# Called when the node enters the scene tree for the first time.
Expand Down Expand Up @@ -55,7 +57,11 @@ func _process(_delta: float) -> void:
func _on_state_entered(_from: State) -> void:
# Fade in the banner texture
player.play("fade_in")


# Ensure dialogs are hidden
location_menu.visible = false
options_menu.visible = false

# Focus the first entry on state change
launch_button.grab_focus.call_deferred()

Expand Down Expand Up @@ -96,11 +102,11 @@ func _on_state_entered(_from: State) -> void:

# Load the banner for the game
var logo_texture = await (
BoxArtManager . get_boxart_or_placeholder(library_item, BoxArtProvider.LAYOUT.LOGO)
BoxArtManager.get_boxart_or_placeholder(library_item, BoxArtProvider.LAYOUT.LOGO)
)
logo.texture = logo_texture
var banner_texture = await (
BoxArtManager . get_boxart_or_placeholder(library_item, BoxArtProvider.LAYOUT.BANNER)
BoxArtManager.get_boxart_or_placeholder(library_item, BoxArtProvider.LAYOUT.BANNER)
)
banner.texture = banner_texture

Expand All @@ -119,15 +125,15 @@ func _update_launch_button() -> void:
if not launch_item:
return
if launch_item.installed:
launch_button.text = "Play Now"
launch_button.text = tr("Play Now")
else:
launch_button.text = "Install"
launch_button.text = tr("Install")
if LaunchManager.is_running(launch_item.name):
launch_button.text = "Resume"
launch_button.text = tr("Resume")
if InstallManager.is_queued(launch_item):
launch_button.text = "Queued"
launch_button.text = tr("Queued")
if InstallManager.is_installing(launch_item):
launch_button.text = "Installing"
launch_button.text = tr("Installing")


func _update_uninstall_button() -> void:
Expand Down Expand Up @@ -162,16 +168,50 @@ func _on_install() -> void:
# Do nothing if we're already installing
if InstallManager.is_queued_or_installing(launch_item):
return
var notify := Notification.new("Installing " + launch_item.name)
NotificationManager.show(notify)

# Create an install request
# Get the library provider for this launch item
var provider := LibraryManager.get_library_by_id(launch_item._provider_id)
var install_req := InstallManager.Request.new(provider, launch_item)

# If multiple install locations are available, ask the user where to
# install.
var location: Library.InstallLocation = null
var locations := await provider.get_available_install_locations(launch_item)
if locations.size() > 0:
# Open the install location menu and wait for the user to select an
# install location.
location_menu.open(launch_button, locations)
var result := await location_menu.choice_selected as Array
var accepted := result[0] as bool
var choice := result[1] as Library.InstallLocation
if not accepted:
return
location = choice

# If install options are available for this library item, ask the user
# to select from the options.
var options := {}
var available_options := await provider.get_install_options(launch_item)
if available_options.size() > 0:
# Open the install option menu and wait for the user to select install
# options.
options_menu.open(launch_button, available_options)
var result := await options_menu.choice_selected as Array
var accepted := result[0] as bool
var choices := result[1] as Dictionary
if not accepted:
return
options = choices

# Create an install request
var install_req := InstallManager.Request.new(provider, launch_item, location, options)

# Update the progress bar with install progress of the request
progress_bar.value = 0

# Display a notification
var notify := Notification.new("Installing " + launch_item.name)
NotificationManager.show(notify)

# Start the install
InstallManager.install(install_req)

Expand Down
14 changes: 13 additions & 1 deletion core/ui/card_ui/launch/game_launch_menu.tscn
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[gd_scene load_steps=22 format=3 uid="uid://bcdk1lj6enq3l"]
[gd_scene load_steps=24 format=3 uid="uid://bcdk1lj6enq3l"]

[ext_resource type="Script" path="res://core/ui/card_ui/launch/game_launch_menu.gd" id="1_u3ehs"]
[ext_resource type="Texture2D" uid="uid://d1mksukdkqorr" path="res://assets/images/placeholder-grid-banner.png" id="2_oae7b"]
Expand All @@ -15,7 +15,9 @@
[ext_resource type="PackedScene" uid="uid://cr83fmlociwko" path="res://core/ui/components/card_icon_button.tscn" id="15_f3ktw"]
[ext_resource type="PackedScene" uid="uid://b76dvfuouhlwd" path="res://core/systems/state/state_updater.tscn" id="15_lat8h"]
[ext_resource type="Resource" uid="uid://cx8u1y5j7vyss" path="res://assets/state/states/gamepad_settings.tres" id="17_7ydn0"]
[ext_resource type="PackedScene" uid="uid://b4u8djfdc4kea" path="res://core/ui/components/install_location_dialog.tscn" id="18_j25yi"]
[ext_resource type="Resource" uid="uid://3vw3bk76d88w" path="res://assets/state/states/game_settings.tres" id="19_b21vy"]
[ext_resource type="PackedScene" uid="uid://18axsy5my1x6" path="res://core/ui/components/install_options_dialog.tscn" id="19_k020t"]
[ext_resource type="Texture2D" uid="uid://dj1ohb74chydb" path="res://assets/ui/icons/round-delete-forever.svg" id="21_agq5k"]

[sub_resource type="Animation" id="Animation_ou6f5"]
Expand Down Expand Up @@ -252,3 +254,13 @@ size_flags_vertical = 4
theme_override_styles/fill = SubResource("StyleBoxFlat_7fb8y")
value = 50.0
rounded = true

[node name="InstallLocationDialog" parent="." instance=ExtResource("18_j25yi")]
unique_name_in_owner = true
visible = false
layout_mode = 1

[node name="InstallOptionsDialog" parent="." instance=ExtResource("19_k020t")]
unique_name_in_owner = true
visible = false
layout_mode = 1
3 changes: 1 addition & 2 deletions core/ui/components/dropdown.tscn
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ autowrap_mode = 3
[node name="OptionButton" type="OptionButton" parent="."]
unique_name_in_owner = true
layout_mode = 2
item_count = 1
fit_to_longest_item = false
item_count = 1
popup/item_0/text = "None"
popup/item_0/id = 0
Loading
Loading