diff --git a/index.json b/index.json index 7f7e48f..6c0ba14 100644 --- a/index.json +++ b/index.json @@ -1,19 +1,7 @@ { - "plugin_categories": [ - { - "display_name": "Utilities", - "meta": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/utilities.json", - "base_download_url": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/utilities" - }, - { - "display_name": "Minigames", - "meta": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/minigames.json", - "base_download_url": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/minigames" - }, - { - "display_name": "Maps", - "meta": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/maps.json", - "base_download_url": "https://github.com/bombsquad-community/mod-manager/raw/main/plugins/maps" - } + "categories": [ + "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/utilities.json", + "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/minigames.json", + "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/maps.json" ] } diff --git a/plugin_manager.py b/plugin_manager.py index b8e09c5..38d69c2 100644 --- a/plugin_manager.py +++ b/plugin_manager.py @@ -15,7 +15,7 @@ from typing import Union, Optional _env = _ba.env() _uiscale = ba.app.ui.uiscale -INDEX_META = "https://raw.githubusercontent.com/bombsquad-community/mod-manager/main/index.json" +INDEX_META = "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/index.json" HEADERS = { "User-Agent": _env["user_agent_string"], } @@ -83,49 +83,77 @@ def play_sound(): class Category: - def __init__(self, name, base_download_url, meta_url): - self.name = name - self.base_download_url = base_download_url + def __init__(self, meta_url): self.meta_url = meta_url self.request_headers = HEADERS - self._plugins = _CACHE.get("categories", {}).get(self.name) + self._metadata = _CACHE.get("categories", {}).get(meta_url, {}).get("metadata") + self._plugins = _CACHE.get("categories", {}).get(meta_url, {}).get("plugins") - async def get_plugins(self): - if self._plugins is None: + async def fetch_metadata(self): + if self._metadata is None: request = urllib.request.Request( - self.meta_url, + self.meta_url.format(content_type="raw"), headers=self.request_headers, ) response = await async_send_network_request(request) - plugins_info = json.loads(response.read()) - self._plugins = ([Plugin(plugin_info, self.base_download_url) - for plugin_info in plugins_info.items()]) - self.set_category_plugins_global_cache(self._plugins) + self._metadata = json.loads(response.read()) + self.set_category_global_cache("metadata", self._metadata) + return self + + async def get_name(self): + if self._metadata is None: + await self.fetch_metadata() + return self._metadata["name"] + + async def get_description(self): + if self._metadata is None: + await self.fetch_metadata() + return self._metadata["description"] + + async def get_plugins_base_url(self): + if self._metadata is None: + await self.fetch_metadata() + return self._metadata["plugins_base_url"] + + async def get_plugins(self): + if self._plugins is None: + if self._metadata is None: + await self.fetch_metadata() + self._plugins = ([ + Plugin(plugin_info, f"{await self.get_plugins_base_url()}/{plugin_info[0]}.py") + for plugin_info in self._metadata["plugins"].items() + ]) + self.set_category_global_cache("plugins", self._plugins) return self._plugins - def set_category_plugins_global_cache(self, plugins): + def set_category_global_cache(self, key, value): if "categories" not in _CACHE: _CACHE["categories"] = {} - _CACHE["categories"][self.name] = plugins + if self.meta_url not in _CACHE["categories"]: + _CACHE["categories"][self.meta_url] = {} + _CACHE["categories"][self.meta_url][key] = value - def unset_category_plugins_global_cache(self): + def unset_category_global_cache(self): try: - del _CACHE["categories"][self.name] + del _CACHE["categories"][self.meta_url] except KeyError: pass - def refresh(self): - self._plugins.clear() - self.unset_category_plugins_global_cache() + async def refresh(self): + self.cleanup() + await self.get_plugins() - async def cleanup(self): - self.refresh() - return await self.get_plugins() + def cleanup(self): + self._metadata = None + self._plugins.clear() + self.unset_category_global_cache() class CategoryAll(Category): def __init__(self, plugins={}): - super().__init__(name="All", base_download_url=None, meta_url=None) + super().__init__(meta_url=None) + self._name = "All" + self._description = "All plugins" self._plugins = plugins @@ -142,7 +170,7 @@ class PluginLocal: self._content = None self._api_version = None self._entry_points = [] - self._contains_minigames = None + self._has_minigames = None @property def is_installed(self): @@ -211,10 +239,10 @@ class PluginLocal: return self._entry_points async def has_minigames(self): - if self._contains_minigames is None: + if self._has_minigames is None: content = await self.get_content() - self._contains_minigames = REGEXP["minigames"].search(content) is not None - return self._contains_minigames + self._has_minigames = REGEXP["minigames"].search(content) is not None + return self._has_minigames async def is_enabled(self): """ @@ -288,13 +316,14 @@ class PluginVersion: class Plugin: - def __init__(self, plugin, base_download_url): + def __init__(self, plugin, url): """ Initialize a plugin from network repository. """ self.name, self.info = plugin self.install_path = os.path.join(PLUGIN_DIRECTORY, f"{self.name}.py") - self.download_url = f"{base_download_url}/{self.name}.py" + self.download_url = url.format(content_type="raw") + self.view_url = url.format(content_type="blob") self._local_plugin = None def __repr__(self): @@ -475,7 +504,7 @@ class PluginWindow(popup.PopupWindow): size=(40, 40), button_type="square", label="", - on_activate_call=self.open_plugin_page) + on_activate_call=lambda: ba.open_url(self.plugin.view_url)) ba.imagewidget(parent=self._root_widget, position=(open_pos_x, open_pos_y), size=(40, 40), @@ -489,9 +518,6 @@ class PluginWindow(popup.PopupWindow): play_sound() ba.containerwidget(edit=self._root_widget, transition='out_scale') - def open_plugin_page(self) -> None: - ba.open_url(f"https://github.com/bombsquad-community/plugin-manager/tree/main/plugins/utilities/{self.plugin.name}.py") - def button(fn): async def asyncio_handler(fn, self, *args, **kwargs): await fn(self, *args, **kwargs) @@ -538,26 +564,47 @@ class PluginManager: def __init__(self): self.request_headers = HEADERS self._index = _CACHE.get("index", {}) + self.categories = {} - async def get_index(self): + async def setup_index(self): global _INDEX if not self._index: request = urllib.request.Request( - INDEX_META, + INDEX_META.format(content_type="raw"), headers=self.request_headers, ) response = await async_send_network_request(request) self._index = json.loads(response.read()) self.set_index_global_cache(self._index) - return self._index + await self._setup_plugin_categories(self._index) + + async def _setup_plugin_categories(self, plugin_index): + # A hack to have the "All" category show at the top. + self.categories["All"] = None + + requests = [] + for plugin_category_url in plugin_index["categories"]: + category = Category(plugin_category_url) + request = category.fetch_metadata() + requests.append(request) + categories = await asyncio.gather(*requests) + + all_plugins = [] + for category in categories: + self.categories[await category.get_name()] = category + all_plugins.extend(await category.get_plugins()) + self.categories["All"] = CategoryAll(plugins=all_plugins) def cleanup(self): + for category in self.categories.values(): + category.cleanup() + self.categories.clear() self._index.clear() self.unset_index_global_cache() async def refresh(self): self.cleanup() - return await self.get_index() + await self.setup_index() def set_index_global_cache(self, index): _CACHE["index"] = index @@ -572,16 +619,15 @@ class PluginManager: pass -class PluginManagerWindow(ba.Window, PluginManager): +class PluginManagerWindow(ba.Window): def __init__(self, transition: str = "in_right", origin_widget: ba.Widget = None): - PluginManager.__init__(self) - self.categories = {} + self.plugin_manager = PluginManager() self.category_selection_button = None self.selected_category = None self.plugins_in_current_view = {} loop = asyncio.get_event_loop() - loop.create_task(self.plugin_index()) + loop.create_task(self.draw_index()) self._width = (490 if _uiscale is ba.UIScale.MEDIUM else 570) self._height = (500 if _uiscale is ba.UIScale.SMALL @@ -634,11 +680,11 @@ class PluginManagerWindow(ba.Window, PluginManager): v_align="center", maxwidth=270, ) - + loading_pos_y = self._height - (235 if _uiscale is ba.UIScale.SMALL else 220 if _uiscale is ba.UIScale.MEDIUM else 250) - - self._loading_text = ba.textwidget( + + self._plugin_manager_status_text = ba.textwidget( parent=self._root_widget, position=(-5, loading_pos_y), size=(self._width, 25), @@ -738,43 +784,24 @@ class PluginManagerWindow(ba.Window, PluginManager): ba.app.ui.set_main_menu_window( AllSettingsWindow(transition='in_left').get_root_widget()) - async def setup_plugin_categories(self, plugin_index): - self.categories["All"] = None - requests = [] - for plugin_category in plugin_index["plugin_categories"]: - category = Category( - plugin_category["display_name"], - plugin_category["base_download_url"], - plugin_category["meta"], - ) - self.categories[plugin_category["display_name"]] = category - request = category.get_plugins() - requests.append(request) - categories = await asyncio.gather(*requests) - - all_plugins = [] - for plugins in categories: - all_plugins.extend(plugins) - self.categories["All"] = CategoryAll(plugins=all_plugins) - - async def plugin_index(self): + async def draw_index(self): try: - index = await super().get_index() await self.draw_search_bar() - await self.setup_plugin_categories(index) - await self.select_category("All") + await self.draw_category_selection_button(post_label="All") await self.draw_refresh_icon() await self.draw_settings_icon() + await self.plugin_manager.setup_index() + ba.textwidget(edit=self._plugin_manager_status_text, + text="") + await self.select_category("All") except RuntimeError: # User probably went back before the PluginManagerWindow could finish loading. pass except urllib.error.URLError: - ba.textwidget(edit=self._loading_text, + ba.textwidget(edit=self._plugin_manager_status_text, text="Make sure you are connected\n to the Internet and try again.") - else: - self._loading_text.delete() - async def draw_category_selection_button(self, label=None): + async def draw_category_selection_button(self, post_label): # v = (self._height - 75) if _uiscale is ba.UIScale.SMALL else (self._height - 105) # v = 395 # h = 440 @@ -793,16 +820,14 @@ class PluginManagerWindow(ba.Window, PluginManager): b_textcolor = (0.75, 0.7, 0.8) b_color = (0.6, 0.53, 0.63) - if label is None: - label = self.selected_category - label = f"Category: {label}" + label = f"Category: {post_label}" if self.category_selection_button is None: self.category_selection_button = ba.buttonwidget(parent=self._root_widget, position=(category_pos_x, category_pos_y), size=b_size, - on_activate_call=self.show_categories, + on_activate_call=lambda: self.show_categories(), label=label, button_type="square", color=b_color, @@ -875,7 +900,7 @@ class PluginManagerWindow(ba.Window, PluginManager): texture=ba.gettexture("replayIcon"), draw_controller=controller_button) - async def draw_plugin_names(self): + async def draw_plugin_names(self, category): # v = (self._height - 75) if _uiscale is ba.UIScale.SMALL else (self._height - 105) # h = 440 # next 2 lines belong in 1 line @@ -889,7 +914,7 @@ class PluginManagerWindow(ba.Window, PluginManager): for plugin in self._columnwidget.get_children(): plugin.delete() - plugins = await self.categories[self.selected_category].get_plugins() + plugins = await self.plugin_manager.categories[category].get_plugins() plugin_names_to_draw = tuple(self.draw_plugin_name(plugin) for plugin in plugins) await asyncio.gather(*plugin_names_to_draw) @@ -939,15 +964,15 @@ class PluginManagerWindow(ba.Window, PluginManager): position=(200, 0), scale=(2.3 if _uiscale is ba.UIScale.SMALL else 1.65 if _uiscale is ba.UIScale.MEDIUM else 1.23), - choices=self.categories.keys(), + choices=self.plugin_manager.categories.keys(), current_choice=self.selected_category, delegate=self) async def select_category(self, category): self.selected_category = category self.plugins_in_current_view.clear() - await self.draw_category_selection_button(label=category) - await self.draw_plugin_names() + await self.draw_category_selection_button(post_label=category) + await self.draw_plugin_names(category) def popup_menu_selected_choice(self, window, choice): loop = asyncio.get_event_loop() @@ -957,19 +982,23 @@ class PluginManagerWindow(ba.Window, PluginManager): pass def cleanup(self): - super().cleanup() - self.categories.clear() + self.plugin_manager.cleanup() for plugin in self._columnwidget.get_children(): plugin.delete() self.plugins_in_current_view.clear() async def _refresh(self): - index = await super().refresh() - await self.setup_plugin_categories(index) + await self.plugin_manager.refresh() + await self.plugin_manager.setup_index() + ba.textwidget(edit=self._plugin_manager_status_text, + text="") await self.select_category(self.selected_category) def refresh(self): play_sound() + self.cleanup() + ba.textwidget(edit=self._plugin_manager_status_text, + text="Refreshing...") loop = asyncio.get_event_loop() loop.create_task(self._refresh()) diff --git a/plugins/maps.json b/plugins/maps.json index 0967ef4..eef16ec 100644 --- a/plugins/maps.json +++ b/plugins/maps.json @@ -1 +1,6 @@ -{} +{ + "name": "Maps", + "description": "Maps", + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/maps", + "plugins": {} +} diff --git a/plugins/minigames.json b/plugins/minigames.json index a8b6a70..f3d8891 100644 --- a/plugins/minigames.json +++ b/plugins/minigames.json @@ -1,21 +1,26 @@ { - "alliance_elimination": { - "description": "Fight in groups of duo, trio, or more. Last remaining alive wins.", - "external_url": "", - "authors": [ - { - "name": "Rikko", - "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" - } - ], - "versions": { - "1.1.0": { - "api_version": 7, - "commit_sha": "cbdb3ead", - "dependencies": [], - "released_on": "08-08-2022", - "md5sum": "11dbb3c7e37e97bda028ea1251529ea0" + "name": "Minigames", + "description": "Minigames", + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/minigames", + "plugins": { + "alliance_elimination": { + "description": "Fight in groups of duo, trio, or more. Last remaining alive wins.", + "external_url": "", + "authors": [ + { + "name": "Rikko", + "email": "rikkolovescats@proton.me", + "discord": "Rikko#7383" + } + ], + "versions": { + "1.1.0": { + "api_version": 7, + "commit_sha": "cbdb3ead", + "dependencies": [], + "released_on": "08-08-2022", + "md5sum": "11dbb3c7e37e97bda028ea1251529ea0" + } } } } diff --git a/plugins/utilities.json b/plugins/utilities.json index f1b6f77..9a16b40 100644 --- a/plugins/utilities.json +++ b/plugins/utilities.json @@ -1,46 +1,51 @@ { - "colorscheme": { - "description": "Create custom UI colorschemes!", - "external_url": "https://www.youtube.com/watch?v=qatwWrBAvjc", - "authors": [ - { - "name": "Rikko", - "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" - }, - { - "name": "Vishal", - "email": "vishal.u338@gmail.com", - "discord": "𝑽𝑰𝑺𝑯𝑼𝑼𝑼#2921" + "name": "Utilities", + "description": "Utilities", + "plugins_base_url": "https://github.com/bombsquad-community/plugin-manager/{content_type}/main/plugins/utilities", + "plugins": { + "colorscheme": { + "description": "Create custom UI colorschemes!", + "external_url": "https://www.youtube.com/watch?v=qatwWrBAvjc", + "authors": [ + { + "name": "Rikko", + "email": "rikkolovescats@proton.me", + "discord": "Rikko#7383" + }, + { + "name": "Vishal", + "email": "vishal.u338@gmail.com", + "discord": "𝑽𝑰𝑺𝑯𝑼𝑼𝑼#2921" + } + ], + "versions": { + "1.1.0": { + "api_version": 7, + "commit_sha": "13a9d128", + "dependencies": [], + "released_on": "03-06-2022", + "md5sum": "4b6bbb99037ebda4664da7c510b3717c" + } } - ], - "versions": { - "1.1.0": { - "api_version": 7, - "commit_sha": "13a9d128", - "dependencies": [], - "released_on": "03-06-2022", - "md5sum": "4b6bbb99037ebda4664da7c510b3717c" - } - } - }, - "store_event_specials": { - "description": "Exposes event-special store items not normally available for purchase", - "external_url": "", - "authors": [ - { - "name": "Rikko", - "email": "rikkolovescats@proton.me", - "discord": "Rikko#7383" - } - ], - "versions": { - "1.0.0": { - "api_version": 7, - "commit_sha": "2aa6df31", - "dependencies": [], - "released_on": "06-08-2022", - "md5sum": "233dfaa7f0e9394d21454f4ffa7d0205" + }, + "store_event_specials": { + "description": "Exposes event-special store items not normally available for purchase", + "external_url": "", + "authors": [ + { + "name": "Rikko", + "email": "rikkolovescats@proton.me", + "discord": "Rikko#7383" + } + ], + "versions": { + "1.0.0": { + "api_version": 7, + "commit_sha": "2aa6df31", + "dependencies": [], + "released_on": "06-08-2022", + "md5sum": "233dfaa7f0e9394d21454f4ffa7d0205" + } } } }