mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-07-19 12:50:46 -07:00
Launcher: Loading Screen (#6115)
This commit is contained in:
+78
-91
@@ -16,21 +16,20 @@ import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import urllib.parse
|
||||
import webbrowser
|
||||
from collections.abc import Callable, Sequence
|
||||
from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Any
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds.LauncherComponents import Component, Type
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
|
||||
ModuleUpdate.update()
|
||||
|
||||
import settings
|
||||
import Utils
|
||||
from Utils import (env_cleared_lib_path, init_logging, is_frozen, is_linux, is_macos, is_windows, local_path,
|
||||
messagebox, open_filename, user_path)
|
||||
from Utils import env_cleared_lib_path, init_logging, is_linux, is_macos, is_windows, local_path
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
@@ -115,27 +114,8 @@ def update_settings():
|
||||
get_settings().save()
|
||||
|
||||
|
||||
components.extend([
|
||||
# Functions
|
||||
Component("Open host.yaml", func=open_host_yaml,
|
||||
description="Open the host.yaml file to change settings for generation, games, and more."),
|
||||
Component("Open Patch", func=open_patch,
|
||||
description="Open a patch file, downloaded from the room page or provided by the host."),
|
||||
Component("Generate Template Options", func=generate_yamls,
|
||||
description="Generate template YAMLs for currently installed games."),
|
||||
Component("Archipelago Website", func=lambda: webbrowser.open("https://archipelago.gg/"),
|
||||
description="Open archipelago.gg in your browser."),
|
||||
Component("Discord Server", icon="discord", func=lambda: webbrowser.open("https://discord.gg/8Z65BR2"),
|
||||
description="Join the Discord server to play public multiworlds, report issues, or just chat!"),
|
||||
Component("Unrated/18+ Discord Server", icon="discord",
|
||||
func=lambda: webbrowser.open("https://discord.gg/fqvNCCRsu4"),
|
||||
description="Find unrated and 18+ games in the After Dark Discord server."),
|
||||
Component("Browse Files", func=browse_files,
|
||||
description="Open the Archipelago installation folder in your file browser."),
|
||||
])
|
||||
|
||||
|
||||
def handle_uri(path: str) -> tuple[list[Component], Component]:
|
||||
def handle_uri(path: str) -> tuple[list["Component"], "Component"]:
|
||||
from worlds.LauncherComponents import components
|
||||
url = urllib.parse.urlparse(path)
|
||||
queries = urllib.parse.parse_qs(url.query)
|
||||
client_components = []
|
||||
@@ -149,7 +129,7 @@ def handle_uri(path: str) -> tuple[list[Component], Component]:
|
||||
return client_components, text_client_component
|
||||
|
||||
|
||||
def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ...]) -> None:
|
||||
def build_uri_popup(component_list: list["Component"], launch_args: tuple[str, ...]) -> None:
|
||||
from kvui import ButtonsPrompt
|
||||
component_options = {
|
||||
component.display_name: component for component in component_list
|
||||
@@ -161,41 +141,6 @@ def build_uri_popup(component_list: list[Component], launch_args: tuple[str, ...
|
||||
popup.open()
|
||||
|
||||
|
||||
def identify(path: None | str) -> tuple[None | str, None | Component]:
|
||||
if path is None:
|
||||
return None, None
|
||||
for component in components:
|
||||
if component.handles_file(path):
|
||||
return path, component
|
||||
elif path == component.display_name or path == component.script_name:
|
||||
return None, component
|
||||
return None, None
|
||||
|
||||
|
||||
def get_exe(component: str | Component) -> Sequence[str] | None:
|
||||
if isinstance(component, str):
|
||||
name = component
|
||||
component = None
|
||||
if name.startswith("Archipelago"):
|
||||
name = name[11:]
|
||||
if name.endswith(".exe"):
|
||||
name = name[:-4]
|
||||
if name.endswith(".py"):
|
||||
name = name[:-3]
|
||||
if not name:
|
||||
return None
|
||||
for c in components:
|
||||
if c.script_name == name or c.frozen_name == f"Archipelago{name}":
|
||||
component = c
|
||||
break
|
||||
if not component:
|
||||
return None
|
||||
if is_frozen():
|
||||
suffix = ".exe" if is_windows else ""
|
||||
return [local_path(f"{component.frozen_name}{suffix}")] if component.frozen_name else None
|
||||
else:
|
||||
return [sys.executable, local_path(f"{component.script_name}.py")] if component.script_name else None
|
||||
|
||||
|
||||
def launch(exe: Sequence[str], in_terminal: bool = False) -> bool:
|
||||
"""Runs the given command/args in `exe` in a new process.
|
||||
@@ -225,7 +170,7 @@ def launch(exe: Sequence[str], in_terminal: bool = False) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
def create_shortcut(button: Any, component: "Component") -> None:
|
||||
from pyshortcuts import make_shortcut
|
||||
env = os.environ
|
||||
if "APPIMAGE" in env:
|
||||
@@ -244,11 +189,14 @@ def create_shortcut(button: Any, component: Component) -> None:
|
||||
refresh_components: Callable[[], None] | None = None
|
||||
|
||||
|
||||
def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox)
|
||||
def run_gui(launch_components: list["Component"], args: Any) -> None:
|
||||
import threading
|
||||
from kvui import (ThemedApp, MDFloatLayout, MDGridLayout, ScrollBox,
|
||||
MDScreenManager, MDScreen, LoadingScreen, LogtoLoadingScreen)
|
||||
from kivy.properties import ObjectProperty
|
||||
from kivy.core.window import Window
|
||||
from kivy.metrics import dp
|
||||
from kivy.clock import Clock
|
||||
from kivymd.uix.button import MDIconButton, MDButton
|
||||
from kivymd.uix.card import MDCard
|
||||
from kivymd.uix.menu import MDDropdownMenu
|
||||
@@ -258,11 +206,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
from kivy.lang.builder import Builder
|
||||
|
||||
class LauncherCard(MDCard):
|
||||
component: Component | None
|
||||
component: "Component | None"
|
||||
image: str
|
||||
context_button: MDIconButton = ObjectProperty(None)
|
||||
|
||||
def __init__(self, *args, component: Component | None = None, image_path: str = "", **kwargs):
|
||||
def __init__(self, *args, component: "Component | None" = None, image_path: str = "", **kwargs):
|
||||
self.component = component
|
||||
self.image = image_path
|
||||
super().__init__(args, kwargs)
|
||||
@@ -275,7 +223,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
button_layout: ScrollBox = ObjectProperty(None)
|
||||
search_box: MDTextField = ObjectProperty(None)
|
||||
cards: list[LauncherCard]
|
||||
current_filter: Sequence[str | Type] | None
|
||||
current_filter: Sequence[str, "Type"] | None
|
||||
failed_worlds: bool = bool(failed_world_loads)
|
||||
|
||||
def __init__(self, ctx=None, components=None, args=None):
|
||||
@@ -286,6 +234,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
self.launch_components = components
|
||||
self.launch_args = args
|
||||
self.cards = []
|
||||
self.current_filter = ()
|
||||
super().__init__()
|
||||
|
||||
def load_filter(self):
|
||||
from worlds.LauncherComponents import Type
|
||||
self.current_filter = (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||
persistent = Utils.persistent_load()
|
||||
if "launcher" in persistent:
|
||||
@@ -300,7 +253,6 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
else:
|
||||
filters.append(Type[filter])
|
||||
self.current_filter = filters
|
||||
super().__init__()
|
||||
|
||||
def set_favorite(self, caller):
|
||||
if caller.component.display_name in self.favorites:
|
||||
@@ -310,7 +262,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
self.favorites.append(caller.component.display_name)
|
||||
caller.icon = "star"
|
||||
|
||||
def build_card(self, component: Component) -> LauncherCard:
|
||||
def build_card(self, component: "Component") -> LauncherCard:
|
||||
"""
|
||||
Builds a card widget for a given component.
|
||||
|
||||
@@ -318,6 +270,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
|
||||
:return: The created Card Widget.
|
||||
"""
|
||||
from worlds.LauncherComponents import icon_paths
|
||||
button_card = LauncherCard(component=component,
|
||||
image_path=icon_paths[component.icon])
|
||||
|
||||
@@ -336,8 +289,9 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
|
||||
return button_card
|
||||
|
||||
def _refresh_components(self, type_filter: Sequence[str | Type] | None = None) -> None:
|
||||
def _refresh_components(self, type_filter: Sequence["Type"] | None = None) -> None:
|
||||
if not type_filter:
|
||||
from worlds.LauncherComponents import Type
|
||||
type_filter = [Type.CLIENT, Type.ADJUSTER, Type.TOOL, Type.MISC]
|
||||
favorites = "favorites" in type_filter
|
||||
|
||||
@@ -368,6 +322,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
if len(name) == 0:
|
||||
self._refresh_components(self.current_filter)
|
||||
return
|
||||
from worlds.LauncherComponents import Type
|
||||
|
||||
sub_matches = [
|
||||
card for card in self.cards
|
||||
@@ -378,38 +333,70 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
self.button_layout.layout.add_widget(card)
|
||||
|
||||
def build(self):
|
||||
self.set_colors()
|
||||
self.screen_manager = MDScreenManager()
|
||||
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||
self.loading_screen = LoadingScreen(name="loading")
|
||||
self.screen_manager.add_widget(self.loading_screen)
|
||||
self.grid = self.top_screen.ids.grid
|
||||
self.navigation = self.top_screen.ids.navigation
|
||||
self.button_layout = self.top_screen.ids.button_layout
|
||||
self.search_box = self.top_screen.ids.search_box
|
||||
self.set_colors()
|
||||
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||
|
||||
global refresh_components
|
||||
refresh_components = self._refresh_components
|
||||
|
||||
Window.bind(on_drop_file=self._on_drop_file)
|
||||
Window.bind(on_keyboard=self._on_keyboard)
|
||||
|
||||
for component in components:
|
||||
self.cards.append(self.build_card(component))
|
||||
|
||||
self._refresh_components(self.current_filter)
|
||||
|
||||
# Uncomment to re-enable the Kivy console/live editor
|
||||
# Ctrl-E to enable it, make sure numlock/capslock is disabled
|
||||
# from kivy.modules.console import create_console
|
||||
# create_console(Window, self.top_screen)
|
||||
|
||||
return self.top_screen
|
||||
main_screen = MDScreen(name="main")
|
||||
main_screen.add_widget(self.top_screen)
|
||||
self.screen_manager.add_widget(main_screen)
|
||||
|
||||
return self.screen_manager
|
||||
|
||||
def on_start(self):
|
||||
super().on_start()
|
||||
logger = logging.getLogger("Worlds")
|
||||
logger.propagate = False
|
||||
self.loading_handler = LogtoLoadingScreen(self.loading_screen.update_text)
|
||||
logger.addHandler(self.loading_handler)
|
||||
threading.Thread(target=self.do_loading, name="WorldLoading").start()
|
||||
|
||||
if self.launch_components:
|
||||
build_uri_popup(self.launch_components, self.launch_args)
|
||||
self.launch_components = None
|
||||
self.launch_args = None
|
||||
|
||||
def do_loading(self):
|
||||
import importlib
|
||||
import time
|
||||
start = time.perf_counter()
|
||||
assert "worlds" not in sys.modules, "worlds module already loaded."
|
||||
importlib.import_module("worlds")
|
||||
logging.error(f"Worlds module loaded in {time.perf_counter() - start:.2f} seconds")
|
||||
|
||||
global refresh_components
|
||||
logger = logging.getLogger("Worlds")
|
||||
logger.info("User Data")
|
||||
self.load_filter()
|
||||
|
||||
refresh_components = self._refresh_components
|
||||
logger.info("Finalizing startup")
|
||||
Clock.schedule_once(self.finish_loading)
|
||||
|
||||
def finish_loading(self, dt):
|
||||
from worlds.LauncherComponents import components
|
||||
logger = logging.getLogger("Worlds")
|
||||
for component in components:
|
||||
self.cards.append(self.build_card(component))
|
||||
self._refresh_components(self.current_filter)
|
||||
logger.removeHandler(self.loading_handler)
|
||||
self.screen_manager.current = "main"
|
||||
|
||||
@staticmethod
|
||||
def component_action(button):
|
||||
open_text = "Opening in a new window..."
|
||||
@@ -418,6 +405,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
button.component.func()
|
||||
else:
|
||||
# if launch returns False, it started the process in background (not in a new terminal)
|
||||
from worlds.LauncherComponents import get_exe
|
||||
if not launch(get_exe(button.component), button.component.cli) and button.component.cli:
|
||||
open_text = "Running in the background..."
|
||||
|
||||
@@ -459,6 +447,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
|
||||
def _on_drop_file(self, window: Window, filename: bytes, x: int, y: int) -> None:
|
||||
""" When a patch file is dropped into the window, run the associated component. """
|
||||
from worlds.LauncherComponents import identify
|
||||
file, component = identify(filename.decode())
|
||||
if file and component:
|
||||
run_component(component, file)
|
||||
@@ -481,6 +470,7 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
super()._stop(*largs)
|
||||
|
||||
def on_stop(self):
|
||||
from worlds.LauncherComponents import Type
|
||||
Utils.persistent_store("launcher", "favorites", self.favorites)
|
||||
Utils.persistent_store("launcher", "filter", ", ".join(filter.name if isinstance(filter, Type) else filter
|
||||
for filter in self.current_filter))
|
||||
@@ -494,15 +484,11 @@ def run_gui(launch_components: list[Component], args: Any) -> None:
|
||||
refresh_components = None
|
||||
|
||||
|
||||
def run_component(component: Component, *args):
|
||||
if component.func:
|
||||
component.func(*args)
|
||||
if refresh_components:
|
||||
refresh_components()
|
||||
elif component.script_name:
|
||||
subprocess.run([*get_exe(component.script_name), *args])
|
||||
else:
|
||||
logging.warning(f"Component {component} does not appear to be executable.")
|
||||
def run_component(component: "Component", *args):
|
||||
global refresh_components
|
||||
component.run(*args)
|
||||
if refresh_components:
|
||||
refresh_components()
|
||||
|
||||
|
||||
def main(args: argparse.Namespace | dict | None = None):
|
||||
@@ -522,6 +508,7 @@ def main(args: argparse.Namespace | dict | None = None):
|
||||
else:
|
||||
args['launch_components'] = [text_client_component, *components]
|
||||
else:
|
||||
from worlds.LauncherComponents import identify
|
||||
file, component = identify(path)
|
||||
if file:
|
||||
args['file'] = file
|
||||
|
||||
Reference in New Issue
Block a user