Files
2026-03-18 14:16:45 +01:00

284 lines
12 KiB
Python

import os
import json
import lvgl as lv
from mpos import Activity, App, Intent, DownloadManager, SettingActivity, SharedPreferences, TaskManager
from app_detail import AppDetail
class AppStore(Activity):
PACKAGE = "com.micropythonos.appstore"
_GITHUB_PROD_BASE_URL = "https://apps.micropythonos.com"
_GITHUB_LIST = "/app_index.json"
_BADGEHUB_TEST_BASE_URL = "https://badgehub.p1m.nl/api/v3"
_BADGEHUB_PROD_BASE_URL = "https://badgehub.eu/api/v3"
_BADGEHUB_LIST = "project-summaries?badge=fri3d_2024"
_BADGEHUB_DETAILS = "projects"
_BACKEND_API_GITHUB = "github"
_BACKEND_API_BADGEHUB = "badgehub"
_ICON_SIZE = 64
# Hardcoded list for now:
backends = [
("Apps.MicroPythonOS.com on GitHub", _BACKEND_API_GITHUB, _GITHUB_PROD_BASE_URL, _GITHUB_LIST, None),
("BadgeHub.eu (beta)", _BACKEND_API_BADGEHUB, _BADGEHUB_PROD_BASE_URL, _BADGEHUB_LIST, _BADGEHUB_DETAILS),
("BadgeHub.p1m.nl Testing (unstable)", _BACKEND_API_BADGEHUB, _BADGEHUB_TEST_BASE_URL, _BADGEHUB_LIST, _BADGEHUB_DETAILS),
]
apps = []
can_check_network = True
# Widgets:
main_screen = None
app_list = None
update_button = None
install_button = None
install_label = None
please_wait_label = None
progress_bar = None
settings_button = None
def onCreate(self):
self.prefs = SharedPreferences(self.PACKAGE)
self._DEFAULT_BACKEND = AppStore.get_backend_pref_string(0)
self.main_screen = lv.obj()
self.please_wait_label = lv.label(self.main_screen)
self.please_wait_label.set_text("Downloading app index...")
self.please_wait_label.center()
self.settings_button = lv.button(self.main_screen)
settings_margin = 15
settings_size = self._ICON_SIZE - settings_margin
self.settings_button.set_size(settings_size, settings_size)
self.settings_button.align(lv.ALIGN.TOP_RIGHT, -settings_margin, 10)
self.settings_button.add_event_cb(self.settings_button_tap,lv.EVENT.CLICKED,None)
settings_label = lv.label(self.settings_button)
settings_label.set_text(lv.SYMBOL.SETTINGS)
settings_label.set_style_text_font(lv.font_montserrat_24, lv.PART.MAIN)
settings_label.center()
self.setContentView(self.main_screen)
def onResume(self, screen):
super().onResume(screen) # super handles self._has_foreground
if not len(self.apps):
self.refresh_list()
def refresh_list(self):
try:
import network
if not network.WLAN(network.STA_IF).isconnected():
self.please_wait_label.remove_flag(lv.obj.FLAG.HIDDEN) # make sure it's visible
self.please_wait_label.set_text("Error: WiFi is not connected.")
except Exception as e:
print("Warning: can't check network state, assuming we're online...")
TaskManager.create_task(self.download_app_index(self.get_backend_list_url_from_settings()))
def settings_button_tap(self, event):
intent = Intent(activity_class=SettingActivity)
intent.putExtra("prefs", self.prefs)
intent.putExtra("setting", {"title": "AppStore Backend",
"key": "backend",
"ui": "radiobuttons",
"default_value": self._DEFAULT_BACKEND,
"ui_options": [(backend[0], AppStore.get_backend_pref_string(index)) for index, backend in enumerate(AppStore.backends)],
"changed_callback": self.backend_changed})
self.startActivity(intent)
def backend_changed(self, new_value):
print(f"backend changed to {new_value}, refreshing...")
self.refresh_list()
async def download_app_index(self, json_url):
try:
response = await DownloadManager.download_url(json_url)
except Exception as e:
print(f"Failed to download app index: {e}")
if DownloadManager.is_network_error(e):
self.please_wait_label.set_text(f"Network error - check your WiFi connection\nand try again.")
else:
self.please_wait_label.set_text(f"Could not download app index from\n{json_url}\nError: {e}")
return
print(f"Got response text: {response[0:20]}")
try:
parsed = json.loads(response)
#print(f"parsed json: {parsed}")
self.apps.clear()
for app in parsed:
try:
backend_type = self.get_backend_type_from_settings()
if backend_type == self._BACKEND_API_BADGEHUB:
self.apps.append(AppStore.badgehub_app_to_mpos_app(app))
else:
self.apps.append(App(app["name"], app["publisher"], app["short_description"], app["long_description"], app["icon_url"], app["download_url"], app["fullname"], app["version"], app["category"], app["activities"]))
except Exception as e:
print(f"Warning: could not add app from {json_url} to apps list: {e}")
except Exception as e:
self.please_wait_label.set_text(f"ERROR: could not parse reponse.text JSON: {e}")
return
self.please_wait_label.set_text(f"Download successful, building list...")
await TaskManager.sleep(0.1) # give the UI time to display the app list before starting to download
print("Remove duplicates based on app.name")
seen = set()
self.apps = [app for app in self.apps if not (app.fullname in seen or seen.add(app.fullname))]
print("Sort apps by app.name")
self.apps.sort(key=lambda x: x.name.lower()) # Use .lower() for case-insensitive sorting
print("Creating apps list...")
self.create_apps_list()
await TaskManager.sleep(0.1) # give the UI time to display the app list before starting to download
print("awaiting self.download_icons()")
await self.download_icons()
def create_apps_list(self):
print("create_apps_list")
print("Hiding please wait label...")
self.please_wait_label.add_flag(lv.obj.FLAG.HIDDEN)
print("Emptying focus group")
# removing objects or even cleaning the screen doesn't seem to empty the focus group
focusgroup = lv.group_get_default()
if focusgroup:
focusgroup.remove_all_objs()
focusgroup.add_obj(self.settings_button)
self.apps_list = lv.list(self.main_screen)
self._apply_default_styles(self.apps_list)
self.apps_list.set_size(lv.pct(100), lv.pct(100))
self._icon_widgets = {} # Clear old icons
print("create_apps_list iterating")
for app in self.apps:
print(app)
item = self.apps_list.add_button(None, "")
item.set_style_pad_all(0, lv.PART.MAIN)
item.set_size(lv.pct(100), lv.SIZE_CONTENT)
self._add_click_handler(item, self.show_app_detail, app)
cont = lv.obj(item)
cont.set_style_pad_all(0, lv.PART.MAIN)
cont.set_flex_flow(lv.FLEX_FLOW.ROW)
cont.set_size(lv.pct(100), lv.SIZE_CONTENT)
cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF)
self._apply_default_styles(cont)
self._add_click_handler(cont, self.show_app_detail, app)
icon_spacer = lv.image(cont)
icon_spacer.set_size(self._ICON_SIZE, self._ICON_SIZE)
icon_spacer.set_src(lv.SYMBOL.REFRESH)
self._add_click_handler(icon_spacer, self.show_app_detail, app)
app.image_icon_widget = icon_spacer # save it so it can be later set to the actual image
label_cont = lv.obj(cont)
self._apply_default_styles(label_cont)
label_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN)
label_cont.set_style_pad_ver(10, lv.PART.MAIN) # Add vertical padding for spacing
label_cont.set_size(lv.pct(75), lv.SIZE_CONTENT)
self._add_click_handler(label_cont, self.show_app_detail, app)
name_label = lv.label(label_cont)
name_label.set_text(app.name)
name_label.set_style_text_font(lv.font_montserrat_16, lv.PART.MAIN)
self._add_click_handler(name_label, self.show_app_detail, app)
desc_label = lv.label(label_cont)
desc_label.set_text(app.short_description)
desc_label.set_style_text_font(lv.font_montserrat_12, lv.PART.MAIN)
self._add_click_handler(desc_label, self.show_app_detail, app)
print("create_apps_list done")
# Settings button needs to float in foreground:
self.settings_button.move_to_index(-1)
async def download_icons(self):
print("Downloading icons...")
for app in self.apps:
if not self.has_foreground():
print(f"App is stopping, aborting icon downloads.") # maybe this can continue? but then update_ui_if_foreground is needed
break
if not app.icon_data:
try:
app.icon_data = await TaskManager.wait_for(DownloadManager.download_url(app.icon_url), 5) # max 5 seconds per icon
except Exception as e:
print(f"Download of {app.icon_url} got exception: {e}")
continue
if app.icon_data:
print("download_icons has icon_data, showing it...")
image_icon_widget = None
try:
image_icon_widget = app.image_icon_widget
except Exception as e:
print(f"app.image_icon_widget got exception {e}")
if image_icon_widget:
image_dsc = lv.image_dsc_t({
'data_size': len(app.icon_data),
'data': app.icon_data
})
image_icon_widget.set_src(image_dsc) # use some kind of new update_ui_if_foreground() ?
print("Finished downloading icons.")
def show_app_detail(self, app):
intent = Intent(activity_class=AppDetail)
intent.putExtra("app", app)
intent.putExtra("appstore", self)
self.startActivity(intent)
def _get_backend_config(self):
"""Get backend configuration tuple (type, list_url, details_url)"""
pref_string = self.prefs.get_string("backend", self._DEFAULT_BACKEND)
return AppStore.backend_pref_string_to_backend(pref_string)
def get_backend_type_from_settings(self):
return self._get_backend_config()[0]
def get_backend_list_url_from_settings(self):
return self._get_backend_config()[1]
def get_backend_details_url_from_settings(self):
return self._get_backend_config()[2]
@staticmethod
def badgehub_app_to_mpos_app(bhapp):
name = bhapp.get("name")
print(f"Got app name: {name}")
short_description = bhapp.get("description")
fullname = bhapp.get("slug")
# Safely extract nested icon URL
icon_url = None
try:
icon_url = bhapp.get("icon_map", {}).get("64x64", {}).get("url")
except Exception:
print("Could not find icon_map 64x64 url")
# Safely extract first category
category = None
try:
category = bhapp.get("categories", [None])[0]
except Exception:
print("Could not parse category")
return App(name, None, short_description, None, icon_url, None, fullname, None, category, None)
@staticmethod
def get_backend_pref_string(index):
backend_info = AppStore.backends[index]
if backend_info:
api = backend_info[1]
base_url = backend_info[2]
list_suffix = backend_info[3]
details_suffix = backend_info[4]
toreturn = api + "," + base_url + "/" + list_suffix
if api == AppStore._BACKEND_API_BADGEHUB:
toreturn += "," + base_url + "/" + details_suffix
return toreturn
@staticmethod
def backend_pref_string_to_backend(string):
return string.split(",")
@staticmethod
def _apply_default_styles(widget, border=0, radius=0, pad=0):
"""Apply common default styles to reduce repetition"""
widget.set_style_border_width(border, lv.PART.MAIN)
widget.set_style_radius(radius, lv.PART.MAIN)
widget.set_style_pad_all(pad, lv.PART.MAIN)
@staticmethod
def _add_click_handler(widget, callback, app):
"""Register click handler to avoid repetition"""
widget.add_event_cb(lambda e, a=app: callback(a), lv.EVENT.CLICKED, None)