diff --git a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py index 1fac8195..7630eb2f 100644 --- a/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py +++ b/internal_filesystem/apps/com.lightningpiggy.displaywallet/assets/displaywallet.py @@ -1,81 +1,18 @@ import time +from mpos.apps import Activity import mpos.config import mpos.ui from wallet import LNBitsWallet, NWCWallet -# screens: -main_screen = None -settings_screen = None -settings_screen_detail = None -qr_screen = None -qr_scanner_screen = None - -# widgets -receive_qr = None -balance_label = None -payments_label = None - -# variables -wallet = None -receive_qr_data = None - -# Settings screen implementation -class SettingsScreen(): - def __init__(self): +class SettingActivity(Activity): + def __init__(self, setting): + super().__init__() self.prefs = mpos.config.SharedPreferences("com.lightningpiggy.displaywallet") - self.settings = [ - {"title": "Wallet Type", "key": "wallet_type", "value_label": None, "cont": None}, - {"title": "LNBits URL", "key": "lnbits_url", "value_label": None, "cont": None}, - {"title": "LNBits Read Key", "key": "lnbits_readkey", "value_label": None, "cont": None}, - {"title": "Static receive code", "key": "lnbits_static_receive_code", "value_label": None, "cont": None}, - {"title": "NWC URL", "key": "nwc_url", "value_label": None, "cont": None}, - ] - self.keyboard = None - self.textarea = None - self.radio_container = None - self.active_radio_index = 0 # Track active radio button index - self.screen = self.create_ui() - self.update_setting_visibility() # Initialize visibility based on saved wallet_type - - def create_ui(self): - screen = lv.obj() - print("creating ui...") - screen.set_size(lv.pct(100), lv.pct(100)) - screen.set_style_pad_all(10, 0) - screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) - screen.set_style_border_width(0, 0) - - # Create settings entries - for setting in self.settings: - # Container for each setting - setting_cont = lv.obj(screen) - setting_cont.set_width(lv.pct(100)) - setting_cont.set_height(lv.SIZE_CONTENT) - setting_cont.set_style_border_width(1, 0) - setting_cont.set_style_border_side(lv.BORDER_SIDE.BOTTOM, 0) - setting_cont.set_style_pad_all(8, 0) - setting_cont.add_flag(lv.obj.FLAG.CLICKABLE) - setting["cont"] = setting_cont # Store container reference for visibility control - - # Title label (bold, larger) - title = lv.label(setting_cont) - title.set_text(setting["title"]) - title.set_style_text_font(lv.font_montserrat_16, 0) - title.set_pos(0, 0) - - # Value label (smaller, below title) - value = lv.label(setting_cont) - value.set_text(self.prefs.get_string(setting["key"], "Not set")) - value.set_style_text_font(lv.font_montserrat_12, 0) - value.set_style_text_color(lv.color_hex(0x666666), 0) - value.set_pos(0, 20) - setting["value_label"] = value # Store reference for updating - setting_cont.add_event_cb( - lambda e, s=setting: self.open_edit_popup(s), lv.EVENT.CLICKED, None - ) + self.setting = setting + def onCreate(self): # Initialize keyboard (hidden initially) self.keyboard = lv.keyboard(lv.layer_sys()) self.keyboard.set_size(lv.pct(100), lv.pct(40)) @@ -83,7 +20,80 @@ class SettingsScreen(): self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) self.keyboard.add_event_cb(self.keyboard_cb, lv.EVENT.READY, None) self.keyboard.add_event_cb(self.keyboard_cb, lv.EVENT.CANCEL, None) - return screen + + settings_screen_detail = lv.obj() + settings_screen_detail.set_style_pad_all(10, 0) + settings_screen_detail.set_flex_flow(lv.FLEX_FLOW.COLUMN) + + top_cont = lv.obj(settings_screen_detail) + top_cont.set_width(lv.pct(100)) + top_cont.set_height(lv.SIZE_CONTENT) + top_cont.set_style_pad_all(0, 0) + top_cont.set_flex_flow(lv.FLEX_FLOW.ROW) + top_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) + + setting_label = lv.label(top_cont) + setting_label.set_text(self.setting["title"]) + setting_label.align(lv.ALIGN.TOP_LEFT,0,0) + setting_label.set_style_text_font(lv.font_montserrat_22, 0) + + # Camera for text + cambutton = lv.button(top_cont) + cambutton.align(lv.ALIGN.TOP_RIGHT,0,0) + cambuttonlabel = lv.label(cambutton) + cambuttonlabel.set_text("SCAN QR") + cambuttonlabel.center() + cambutton.add_event_cb(self.cambutton_cb, lv.EVENT.CLICKED, None) + + if self.setting["key"] == "wallet_type": + cambutton.add_flag(lv.obj.FLAG.HIDDEN) + # Create container for radio buttons + self.radio_container = lv.obj(settings_screen_detail) + self.radio_container.set_width(lv.pct(100)) + self.radio_container.set_height(lv.SIZE_CONTENT) + self.radio_container.set_flex_flow(lv.FLEX_FLOW.COLUMN) + self.radio_container.add_event_cb(self.radio_event_handler, lv.EVENT.CLICKED, None) + + # Create radio buttons + options = [("LNBits", "lnbits"), ("Nostr Wallet Connect", "nwc")] + current_wallet = self.prefs.get_string("wallet_type", "lnbits") + self.active_radio_index = 0 if current_wallet == "lnbits" else 1 + + for i, (text, _) in enumerate(options): + cb = self.create_radio_button(self.radio_container, text, i) + if i == self.active_radio_index: + cb.add_state(lv.STATE.CHECKED) + else: + # Textarea for other settings + self.textarea = lv.textarea(settings_screen_detail) + self.textarea.set_width(lv.pct(100)) + self.textarea.set_height(lv.SIZE_CONTENT) + self.textarea.set_text(self.prefs.get_string(self.setting["key"], "")) + self.textarea.add_event_cb(self.show_keyboard, lv.EVENT.CLICKED, None) + self.textarea.add_event_cb(self.show_keyboard, lv.EVENT.FOCUSED, None) + self.textarea.add_event_cb(self.hide_keyboard, lv.EVENT.DEFOCUSED, None) + # Button container + btn_cont = lv.obj(settings_screen_detail) + btn_cont.set_width(lv.pct(100)) + btn_cont.set_height(lv.SIZE_CONTENT) + btn_cont.set_style_pad_all(5, 0) + btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW) + btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) + # Save button + save_btn = lv.button(btn_cont) + save_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) + save_label = lv.label(save_btn) + save_label.set_text("Save") + save_label.center() + save_btn.add_event_cb(lambda e, s=self.setting: self.save_setting(s), lv.EVENT.CLICKED, None) + # Cancel button + cancel_btn = lv.button(btn_cont) + cancel_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) + cancel_label = lv.label(cancel_btn) + cancel_label.set_text("Cancel") + cancel_label.center() + cancel_btn.add_event_cb(self.close_popup, lv.EVENT.CLICKED, None) + mpos.ui.setContentView(self, settings_screen_detail) def hide_keyboard(self, event=None): print("hide_keyboard: hiding keyboard") @@ -101,20 +111,6 @@ class SettingsScreen(): print("keyboard_cb: READY or CANCEL or RETURN clicked, hiding keyboard") self.hide_keyboard() - def update_setting_visibility(self): - wallet_type = self.prefs.get_string("wallet_type", "lnbits") - for setting in self.settings: - if setting["key"].startswith("lnbits_"): - if wallet_type != "lnbits": - setting["cont"].add_flag(lv.obj.FLAG.HIDDEN) - else: - setting["cont"].remove_flag(lv.obj.FLAG.HIDDEN) - elif setting["key"].startswith("nwc_"): - if wallet_type != "nwc": - setting["cont"].add_flag(lv.obj.FLAG.HIDDEN) - else: - setting["cont"].remove_flag(lv.obj.FLAG.HIDDEN) - def radio_event_handler(self, event): old_cb = self.radio_container.get_child(self.active_radio_index) old_cb.remove_state(lv.STATE.CHECKED) @@ -149,103 +145,12 @@ class SettingsScreen(): self.textarea.set_text(data) def cambutton_cb(self, event): - global qr_scanner_screen print("cambutton clicked!") import captureqr qr_scanner_screen = captureqr.init(self.gotqr_callback) if qr_scanner_screen: mpos.ui.load_screen(qr_scanner_screen) - def open_edit_popup(self, setting): - global settings_screen_detail - # Close existing msgbox and keyboard if open - if settings_screen_detail: - try: - settings_screen_detail.delete() - except Exception as e: - print(f"Warning: could not delete settings_screen_detail: {e}") - if self.keyboard: - self.keyboard.add_flag(lv.obj.FLAG.HIDDEN) - - # Create msgbox - settings_screen_detail = lv.obj() - settings_screen_detail.set_style_pad_all(10, 0) - settings_screen_detail.set_flex_flow(lv.FLEX_FLOW.COLUMN) - - top_cont = lv.obj(settings_screen_detail) - top_cont.set_width(lv.pct(100)) - top_cont.set_height(lv.SIZE_CONTENT) - top_cont.set_style_pad_all(0, 0) - top_cont.set_flex_flow(lv.FLEX_FLOW.ROW) - top_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) - - setting_label = lv.label(top_cont) - setting_label.set_text(setting["title"]) - setting_label.align(lv.ALIGN.TOP_LEFT,0,0) - setting_label.set_style_text_font(lv.font_montserrat_22, 0) - - # Camera for text - cambutton = lv.button(top_cont) - cambutton.align(lv.ALIGN.TOP_RIGHT,0,0) - cambuttonlabel = lv.label(cambutton) - cambuttonlabel.set_text("SCAN QR") - cambuttonlabel.center() - cambutton.add_event_cb(self.cambutton_cb, lv.EVENT.CLICKED, None) - - if setting["key"] == "wallet_type": - cambutton.add_flag(lv.obj.FLAG.HIDDEN) - # Create container for radio buttons - self.radio_container = lv.obj(settings_screen_detail) - self.radio_container.set_width(lv.pct(100)) - self.radio_container.set_height(lv.SIZE_CONTENT) - self.radio_container.set_flex_flow(lv.FLEX_FLOW.COLUMN) - self.radio_container.add_event_cb(self.radio_event_handler, lv.EVENT.CLICKED, None) - - # Create radio buttons - options = [("LNBits", "lnbits"), ("Nostr Wallet Connect", "nwc")] - current_wallet = self.prefs.get_string("wallet_type", "lnbits") - self.active_radio_index = 0 if current_wallet == "lnbits" else 1 - - for i, (text, _) in enumerate(options): - cb = self.create_radio_button(self.radio_container, text, i) - if i == self.active_radio_index: - cb.add_state(lv.STATE.CHECKED) - else: - # Textarea for other settings - self.textarea = lv.textarea(settings_screen_detail) - self.textarea.set_width(lv.pct(100)) - self.textarea.set_height(lv.SIZE_CONTENT) - self.textarea.set_text(self.prefs.get_string(setting["key"], "")) - self.textarea.add_event_cb(self.show_keyboard, lv.EVENT.CLICKED, None) - self.textarea.add_event_cb(self.show_keyboard, lv.EVENT.FOCUSED, None) - self.textarea.add_event_cb(self.hide_keyboard, lv.EVENT.DEFOCUSED, None) - - # Button container - btn_cont = lv.obj(settings_screen_detail) - btn_cont.set_width(lv.pct(100)) - btn_cont.set_height(lv.SIZE_CONTENT) - btn_cont.set_style_pad_all(5, 0) - btn_cont.set_flex_flow(lv.FLEX_FLOW.ROW) - btn_cont.set_style_flex_main_place(lv.FLEX_ALIGN.SPACE_BETWEEN, 0) - - # Save button - save_btn = lv.button(btn_cont) - save_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) - save_label = lv.label(save_btn) - save_label.set_text("Save") - save_label.center() - save_btn.add_event_cb(lambda e, s=setting: self.save_setting(s), lv.EVENT.CLICKED, None) - - # Cancel button - cancel_btn = lv.button(btn_cont) - cancel_btn.set_size(lv.pct(45), lv.SIZE_CONTENT) - cancel_label = lv.label(cancel_btn) - cancel_label.set_text("Cancel") - cancel_label.center() - cancel_btn.add_event_cb(self.close_popup, lv.EVENT.CLICKED, None) - - mpos.ui.load_screen(settings_screen_detail) - def save_setting(self, setting): if setting["key"] == "wallet_type" and self.radio_container: selected_idx = self.active_radio_index @@ -258,139 +163,199 @@ class SettingsScreen(): editor.put_string(setting["key"], new_value) editor.commit() setting["value_label"].set_text(new_value if new_value else "Not set") - if setting["key"] == "wallet_type": - self.update_setting_visibility() self.close_popup(None) def close_popup(self, event): - global settings_screen_detail - mpos.ui.back_screen() - if settings_screen_detail: - settings_screen_detail.delete() if self.keyboard: self.hide_keyboard() + mpos.ui.back_screen() -def settings_button_tap(event): - global settings_screen, wallet - if not settings_screen: - settings_screen = SettingsScreen().screen - wallet.stop() - mpos.ui.load_screen(settings_screen) +class SettingsActivity(Activity): + def __init__(self): + self.prefs = mpos.config.SharedPreferences("com.lightningpiggy.displaywallet") + self.settings = [ + {"title": "Wallet Type", "key": "wallet_type", "value_label": None, "cont": None}, + {"title": "LNBits URL", "key": "lnbits_url", "value_label": None, "cont": None}, + {"title": "LNBits Read Key", "key": "lnbits_readkey", "value_label": None, "cont": None}, + {"title": "Static receive code", "key": "lnbits_static_receive_code", "value_label": None, "cont": None}, + {"title": "NWC URL", "key": "nwc_url", "value_label": None, "cont": None}, + ] + self.keyboard = None + self.textarea = None + self.radio_container = None + self.active_radio_index = 0 # Track active radio button index -def main_ui_set_defaults(): - global balance_label, payments_label, receive_qr - balance_label.set_text(lv.SYMBOL.REFRESH) - payments_label.set_text(lv.SYMBOL.REFRESH) - receive_qr.update("", len("")) + def onCreate(self): + screen = lv.obj() + print("creating SettingsActivity ui...") + screen.set_size(lv.pct(100), lv.pct(100)) + screen.set_style_pad_all(10, 0) + screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) + screen.set_style_border_width(0, 0) + + # Create settings entries + for setting in self.settings: + # Container for each setting + setting_cont = lv.obj(screen) + setting_cont.set_width(lv.pct(100)) + setting_cont.set_height(lv.SIZE_CONTENT) + setting_cont.set_style_border_width(1, 0) + setting_cont.set_style_border_side(lv.BORDER_SIDE.BOTTOM, 0) + setting_cont.set_style_pad_all(8, 0) + setting_cont.add_flag(lv.obj.FLAG.CLICKABLE) + setting["cont"] = setting_cont # Store container reference for visibility control -def qr_clicked_cb(event): - global qr_screen, big_receive_qr, receive_qr_data - print("QR clicked") - qr_screen = lv.obj() - big_receive_qr = lv.qrcode(qr_screen) - big_receive_qr.set_size(240) # TODO: make this dynamic - big_receive_qr.set_dark_color(lv.color_black()) - big_receive_qr.set_light_color(lv.color_white()) - big_receive_qr.center() - big_receive_qr.set_style_border_color(lv.color_white(), 0) - big_receive_qr.set_style_border_width(3, 0); - big_receive_qr.update(receive_qr_data, len(receive_qr_data)) - mpos.ui.load_screen(qr_screen) + # Title label (bold, larger) + title = lv.label(setting_cont) + title.set_text(setting["title"]) + title.set_style_text_font(lv.font_montserrat_16, 0) + title.set_pos(0, 0) + + # Value label (smaller, below title) + value = lv.label(setting_cont) + value.set_text(self.prefs.get_string(setting["key"], "Not set")) + value.set_style_text_font(lv.font_montserrat_12, 0) + value.set_style_text_color(lv.color_hex(0x666666), 0) + value.set_pos(0, 20) + setting["value_label"] = value # Store reference for updating + setting_cont.add_event_cb( + lambda e, s=setting: self.startSettingActivity(s), lv.EVENT.CLICKED, None + ) + mpos.ui.setContentView(self, screen) + + def startSettingActivity(self, setting): + sa = SettingActivity(setting) + sa.onCreate() + + def onResume(self, screen): + wallet_type = self.prefs.get_string("wallet_type", "lnbits") + # update setting visibility based on wallet_type: + for setting in self.settings: + if setting["key"].startswith("lnbits_"): + if wallet_type != "lnbits": + setting["cont"].add_flag(lv.obj.FLAG.HIDDEN) + else: + setting["cont"].remove_flag(lv.obj.FLAG.HIDDEN) + elif setting["key"].startswith("nwc_"): + if wallet_type != "nwc": + setting["cont"].add_flag(lv.obj.FLAG.HIDDEN) + else: + setting["cont"].remove_flag(lv.obj.FLAG.HIDDEN) -def build_main_ui(): - global main_screen, balance_label, payments_label, receive_qr - main_screen = lv.obj() - main_screen.set_style_pad_all(10, 0) - balance_label = lv.label(main_screen) - balance_label.set_text("") - balance_label.align(lv.ALIGN.TOP_LEFT, 0, 0) - balance_label.set_style_text_font(lv.font_montserrat_22, 0) - receive_qr = lv.qrcode(main_screen) - receive_qr.set_size(50) - receive_qr.set_dark_color(lv.color_black()) - receive_qr.set_light_color(lv.color_white()) - receive_qr.align(lv.ALIGN.TOP_RIGHT,0,0) - receive_qr.set_style_border_color(lv.color_white(), 0) - receive_qr.set_style_border_width(3, 0); - receive_qr.add_flag(lv.obj.FLAG.CLICKABLE) - receive_qr.add_event_cb(qr_clicked_cb,lv.EVENT.CLICKED,None) - #style_line = lv.style_t() - #style_line.init() - #style_line.set_line_width(2) - #style_line.set_line_color(lv.palette_main(lv.PALETTE.PINK)) - #style_line.set_line_rounded(True) - balance_line = lv.line(main_screen) - balance_line.set_points([{'x':0,'y':35},{'x':200,'y':35}],2) - #balance_line.add_style(style_line, 0) - payments_label = lv.label(main_screen) - payments_label.set_text("") - payments_label.align_to(balance_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10) - payments_label.set_style_text_font(lv.font_montserrat_16, 0) - settings_button = lv.button(main_screen) - settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) - snap_label = lv.label(settings_button) - snap_label.set_text(lv.SYMBOL.SETTINGS) - snap_label.center() - settings_button.add_event_cb(settings_button_tap,lv.EVENT.CLICKED,None) - mpos.ui.load_screen(main_screen) +class MainActivity(Activity): + + def __init__(self): + self.wallet = None + self.receive_qr_data = None + # widgets + self.balance_label = None + self.receive_qr = None + self.payments_label = None + def settings_button_tap(self, event): + settings_activity = SettingsActivity() + settings_activity.onCreate() + + def main_ui_set_defaults(self): + self.balance_label.set_text(lv.SYMBOL.REFRESH) + self.payments_label.set_text(lv.SYMBOL.REFRESH) + self.receive_qr.update("EMPTY", len("EMPTY")) + + def qr_clicked_cb(self, event): + if not self.receive_qr_data: + return + print("QR clicked") + qr_screen = lv.obj() + big_receive_qr = lv.qrcode(qr_screen) + big_receive_qr.set_size(240) # TODO: make this dynamic + big_receive_qr.set_dark_color(lv.color_black()) + big_receive_qr.set_light_color(lv.color_white()) + big_receive_qr.center() + big_receive_qr.set_style_border_color(lv.color_white(), 0) + big_receive_qr.set_style_border_width(3, 0); + big_receive_qr.update(self.receive_qr_data, len(self.receive_qr_data)) + mpos.ui.load_screen(qr_screen) + + + def onCreate(self): + main_screen = lv.obj() + main_screen.set_style_pad_all(10, 0) + self.balance_label = lv.label(main_screen) + self.balance_label.set_text("") + self.balance_label.align(lv.ALIGN.TOP_LEFT, 0, 0) + self.balance_label.set_style_text_font(lv.font_montserrat_22, 0) + self.receive_qr = lv.qrcode(main_screen) + self.receive_qr.set_size(50) + self.receive_qr.set_dark_color(lv.color_black()) + self.receive_qr.set_light_color(lv.color_white()) + self.receive_qr.align(lv.ALIGN.TOP_RIGHT,0,0) + self.receive_qr.set_style_border_color(lv.color_white(), 0) + self.receive_qr.set_style_border_width(3, 0); + self.receive_qr.add_flag(lv.obj.FLAG.CLICKABLE) + self.receive_qr.add_event_cb(self.qr_clicked_cb,lv.EVENT.CLICKED,None) + balance_line = lv.line(main_screen) + balance_line.set_points([{'x':0,'y':35},{'x':200,'y':35}],2) + self.payments_label = lv.label(main_screen) + self.payments_label.set_text("") + self.payments_label.align_to(balance_line,lv.ALIGN.OUT_BOTTOM_LEFT,0,10) + self.payments_label.set_style_text_font(lv.font_montserrat_16, 0) + settings_button = lv.button(main_screen) + settings_button.align(lv.ALIGN.BOTTOM_RIGHT, 0, 0) + snap_label = lv.label(settings_button) + snap_label.set_text(lv.SYMBOL.SETTINGS) + snap_label.center() + settings_button.add_event_cb(self.settings_button_tap,lv.EVENT.CLICKED,None) + mpos.ui.setContentView(self, main_screen) + + def redraw_balance_cb(self): + # this gets called from another thread (the wallet) so make sure it happens in the LVGL thread using lv.async_call(): + lv.async_call(lambda l: self.balance_label.set_text(str(self.wallet.last_known_balance)), None) + + def redraw_payments_cb(self): + # this gets called from another thread (the wallet) so make sure it happens in the LVGL thread using lv.async_call(): + lv.async_call(lambda l: self.payments_label.set_text(str(self.wallet.payment_list)), None) -def redraw_balance_cb(): - global balance_label - balance_label.set_text(str(wallet.last_known_balance)) - -def redraw_payments_cb(): - global payments_label - print("redrawing payments") - payments_label.set_text(str(wallet.payment_list)) - -def janitor_cb(timer): - global wallet, config, receive_qr_data, payments_label - if lv.screen_active() == main_screen and (not wallet or not wallet.is_running()): # just started the app or just returned from settings_screen - main_ui_set_defaults() - config = mpos.config.SharedPreferences("com.lightningpiggy.displaywallet") - wallet_type = config.get_string("wallet_type") - if wallet_type == "lnbits": - try: - receive_qr_data = config.get_string("lnbits_static_receive_code") - wallet = LNBitsWallet(config.get_string("lnbits_url"), config.get_string("lnbits_readkey")) - except Exception as e: - payments_label.set_text(f"Couldn't initialize LNBitsWallet\nbecause: {e}") - elif wallet_type == "nwc": - try: - wallet = NWCWallet(config.get_string("nwc_url")) - receive_qr_data = wallet.lud16 - except Exception as e: - payments_label.set_text(f"Couldn't initialize NWCWallet\nbecause: {e}") - else: - payments_label.set_text(f"No or unsupported wallet\ntype configured: '{wallet_type}'") - if receive_qr_data: - print(f"Setting static_receive_code: {receive_qr_data}") - receive_qr.update(receive_qr_data, len(receive_qr_data)) - can_check_network = True - try: - import network - except Exception as e: - can_check_network = False - if can_check_network and not network.WLAN(network.STA_IF).isconnected(): - payments_label.set_text(f"WiFi is not connected, can't\ntalk to {wallet_type} backend.") - else: - if wallet: - payments_label.set_text(f"Connecting to {wallet_type} backend...") - wallet.start(redraw_balance_cb, redraw_payments_cb) + def onStart(self, main_screen): + self.main_ui_set_defaults() + + def onResume(self, main_screen): + if not self.wallet or not self.wallet.is_running(): # just started the app or just returned from settings_screen + config = mpos.config.SharedPreferences("com.lightningpiggy.displaywallet") + wallet_type = config.get_string("wallet_type") + if wallet_type == "lnbits": + try: + self.receive_qr_data = config.get_string("lnbits_static_receive_code") + self.wallet = LNBitsWallet(config.get_string("lnbits_url"), config.get_string("lnbits_readkey")) + except Exception as e: + self.payments_label.set_text(f"Couldn't initialize LNBitsWallet\nbecause: {e}") + elif wallet_type == "nwc": + try: + self.wallet = NWCWallet(config.get_string("nwc_url")) + self.receive_qr_data = wallet.lud16 + except Exception as e: + self.payments_label.set_text(f"Couldn't initialize NWCWallet\nbecause: {e}") else: - payments_label.set_text(f"Could not start {wallet_type} backend.") - elif lv.screen_active() != main_screen and lv.screen_active() != settings_screen and lv.screen_active() != qr_screen and lv.screen_active() != settings_screen_detail and lv.screen_active() != qr_scanner_screen: - print("app backgrounded, cleaning up...") - janitor.delete() - wallet.stop() - if settings_screen: - settings_screen.delete() - if main_screen: - main_screen.delete() + self.payments_label.set_text(f"No or unsupported wallet\ntype configured: '{wallet_type}'") + if self.receive_qr_data: + print(f"Setting static_receive_code: {self.receive_qr_data}") + self.receive_qr.update(self.receive_qr_data, len(self.receive_qr_data)) + can_check_network = True + try: + import network + except Exception as e: + can_check_network = False + if can_check_network and not network.WLAN(network.STA_IF).isconnected(): + self.payments_label.set_text(f"WiFi is not connected, can't\ntalk to {wallet_type} backend.") + else: + if self.wallet: + self.payments_label.set_text(f"Connecting to {wallet_type} backend...") + self.wallet.start(self.redraw_balance_cb, self.redraw_payments_cb) + else: + self.payments_label.set_text(f"Could not start {wallet_type} backend.") -build_main_ui() + def onStop(self, main_screen): + if self.wallet: + self.wallet.stop() -janitor = lv.timer_create(janitor_cb, 500, None) diff --git a/internal_filesystem/builtin/apps/com.example.launcher/assets/launcher.py b/internal_filesystem/builtin/apps/com.example.launcher/assets/launcher.py index f8125261..af90abc3 100644 --- a/internal_filesystem/builtin/apps/com.example.launcher/assets/launcher.py +++ b/internal_filesystem/builtin/apps/com.example.launcher/assets/launcher.py @@ -11,95 +11,98 @@ import uos import lvgl as lv + import mpos.apps import mpos.ui -# Create a container for the grid -main_screen = lv.obj() -cont = lv.obj(main_screen) -cont.set_style_border_width(0, 0) -cont.set_style_radius(0, 0) -cont.set_pos(0, mpos.ui.NOTIFICATION_BAR_HEIGHT) # leave some margin for the notification bar -cont.set_size(lv.pct(100), lv.pct(100)) -cont.set_style_pad_all(10, 0) -cont.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) -mpos.ui.load_screen(main_screen) +class MainActivity(mpos.apps.Activity): -# Grid parameters -icon_size = 64 # Adjust based on your display -label_height = 24 -iconcont_width = icon_size + label_height -iconcont_height = icon_size + label_height - -import time -start = time.ticks_ms() - -def load_icon(icon_path): - with open(icon_path, 'rb') as f: - image_data = f.read() - image_dsc = lv.image_dsc_t({ - 'data_size': len(image_data), - 'data': image_data - }) - return image_dsc - -# Check and collect subdirectories from existing directories -apps_dir = "apps" -apps_dir_builtin = "builtin/apps" -seen_base_names = set() -app_list = [] - -# Check and collect unique subdirectories -for dir_path in [apps_dir, apps_dir_builtin]: - try: - if uos.stat(dir_path)[0] & 0x4000: # Verify directory exists - for d in uos.listdir(dir_path): - full_path = f"{dir_path}/{d}" - #print(f"full_path: {full_path}") - if uos.stat(full_path)[0] & 0x4000: # Check if it's a directory - base_name = d - if base_name not in seen_base_names: # Avoid duplicates - seen_base_names.add(base_name) - app = mpos.apps.parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON") - if app.category != "launcher": # Skip launchers - app_list.append((app.name, full_path)) - except OSError: - pass - -# Sort apps alphabetically by app.name -app_list.sort(key=lambda x: x[0].lower()) # Case-insensitive sorting - -# Create UI for each app -for app_name, app_dir_fullpath in app_list: - #print(f"Adding app {app_name} from {app_dir_fullpath}") - # Create container for each app (icon + label) - app_cont = lv.obj(cont) - app_cont.set_size(iconcont_width, iconcont_height) - app_cont.set_style_border_width(0, 0) - app_cont.set_style_pad_all(0, 0) - # Load and display icon - icon_path = f"{app_dir_fullpath}/res/mipmap-mdpi/icon_64x64.png" - image = lv.image(app_cont) - try: - image.set_src(load_icon(icon_path)) - except Exception as e: - print(f"Error loading icon {icon_path}: {e} - loading default icon") - icon_path = "builtin/res/mipmap-mdpi/default_icon_64x64.png" - try: - image.set_src(load_icon(icon_path)) - except Exception as e: - print(f"Error loading default icon {icon_path}: {e} - using symbol") - image.set_src(lv.SYMBOL.STOP) - image.align(lv.ALIGN.TOP_MID, 0, 0) - image.set_size(icon_size, icon_size) - label = lv.label(app_cont) - label.set_text(app_name) # Use app_name directly - label.set_long_mode(lv.label.LONG.WRAP) - label.set_width(iconcont_width) - label.align(lv.ALIGN.BOTTOM_MID, 0, 0) - label.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) - app_cont.add_event_cb(lambda e, path=app_dir_fullpath: mpos.apps.start_app(path), lv.EVENT.CLICKED, None) - -end = time.ticks_ms() -print(f"Displaying all icons took: {end-start}ms") + def onCreate(self): + main_screen = lv.obj() + main_screen.set_style_border_width(0, 0) + main_screen.set_style_radius(0, 0) + main_screen.set_pos(0, mpos.ui.NOTIFICATION_BAR_HEIGHT) # leave some margin for the notification bar + main_screen.set_size(lv.pct(100), lv.pct(100)) + main_screen.set_style_pad_all(10, 0) + main_screen.set_flex_flow(lv.FLEX_FLOW.ROW_WRAP) + mpos.ui.setContentView(self, main_screen) + def onResume(self, main_screen): + # Grid parameters + icon_size = 64 # Adjust based on your display + label_height = 24 + iconcont_width = icon_size + label_height + iconcont_height = icon_size + label_height + + import time + start = time.ticks_ms() + + def load_icon(icon_path): + with open(icon_path, 'rb') as f: + image_data = f.read() + image_dsc = lv.image_dsc_t({ + 'data_size': len(image_data), + 'data': image_data + }) + return image_dsc + + # Check and collect subdirectories from existing directories + apps_dir = "apps" + apps_dir_builtin = "builtin/apps" + seen_base_names = set() + app_list = [] + + # Check and collect unique subdirectories + for dir_path in [apps_dir, apps_dir_builtin]: + try: + if uos.stat(dir_path)[0] & 0x4000: # Verify directory exists + for d in uos.listdir(dir_path): + full_path = f"{dir_path}/{d}" + #print(f"full_path: {full_path}") + if uos.stat(full_path)[0] & 0x4000: # Check if it's a directory + base_name = d + if base_name not in seen_base_names: # Avoid duplicates + seen_base_names.add(base_name) + app = mpos.apps.parse_manifest(f"{full_path}/META-INF/MANIFEST.JSON") + if app.category != "launcher": # Skip launchers + app_list.append((app.name, full_path)) + except OSError: + pass + + # Sort apps alphabetically by app.name + app_list.sort(key=lambda x: x[0].lower()) # Case-insensitive sorting + + # Create UI for each app + for app_name, app_dir_fullpath in app_list: + #print(f"Adding app {app_name} from {app_dir_fullpath}") + # Create container for each app (icon + label) + app_cont = lv.obj(main_screen) + app_cont.set_size(iconcont_width, iconcont_height) + app_cont.set_style_border_width(0, 0) + app_cont.set_style_pad_all(0, 0) + app_cont.set_style_bg_opa(lv.OPA.TRANSP,0) # prevent default style from adding slight gray to this container + # Load and display icon + icon_path = f"{app_dir_fullpath}/res/mipmap-mdpi/icon_64x64.png" + image = lv.image(app_cont) + try: + image.set_src(load_icon(icon_path)) + except Exception as e: + print(f"Error loading icon {icon_path}: {e} - loading default icon") + icon_path = "builtin/res/mipmap-mdpi/default_icon_64x64.png" + try: + image.set_src(load_icon(icon_path)) + except Exception as e: + print(f"Error loading default icon {icon_path}: {e} - using symbol") + image.set_src(lv.SYMBOL.STOP) + image.align(lv.ALIGN.TOP_MID, 0, 0) + image.set_size(icon_size, icon_size) + label = lv.label(app_cont) + label.set_text(app_name) # Use app_name directly + label.set_long_mode(lv.label.LONG.WRAP) + label.set_width(iconcont_width) + label.align(lv.ALIGN.BOTTOM_MID, 0, 0) + label.set_style_text_align(lv.TEXT_ALIGN.CENTER, 0) + app_cont.add_event_cb(lambda e, path=app_dir_fullpath: mpos.apps.start_app(path), lv.EVENT.CLICKED, None) + + end = time.ticks_ms() + print(f"Displaying all icons took: {end-start}ms") diff --git a/internal_filesystem/lib/mpos/apps.py b/internal_filesystem/lib/mpos/apps.py index 1d814c40..cb4e5c2c 100644 --- a/internal_filesystem/lib/mpos/apps.py +++ b/internal_filesystem/lib/mpos/apps.py @@ -47,6 +47,10 @@ def execute_script(script_source, is_file, cwd=None): print("Classes:", classes.keys()) print("Functions:", functions.keys()) print("Variables:", variables.keys()) + MainActivity = script_globals.get("MainActivity") + if MainActivity: + loaded_activity = MainActivity() + loaded_activity.onCreate() # Call lifecycle method except Exception as e: print(f"Thread {thread_id}: exception during execution:") # Print stack trace with exception type, value, and traceback @@ -186,3 +190,18 @@ def auto_connect(): except Exception as e: print("Couldn't execute {builtin_auto_connect} because exception {e}, continuing...") + +class Activity: + + def onCreate(self): + pass + def onStart(self, screen): + pass + def onResume(self, screen): + pass + def onPause(self, screen): + pass + def onStop(self, screen): + pass + def onDestroy(self, screen): + pass diff --git a/internal_filesystem/lib/mpos/ui.py b/internal_filesystem/lib/mpos/ui.py index 2c8235de..507b28c1 100644 --- a/internal_filesystem/lib/mpos/ui.py +++ b/internal_filesystem/lib/mpos/ui.py @@ -456,50 +456,64 @@ def close_top_layer_msgboxes(): else: print(f"Top layer still has {child_count} children") -def clean_top_layer(): - print("Cleaning top layer") - timer1.delete() - timer2.delete() - timer3.delete() - timer4.delete() - lv.layer_top().clean() -screen_stack = [] +screen_stack = [] # Stack of (activity, screen) tuples def empty_screen_stack(): global screen_stack screen_stack.clear() + def load_screen(screen): + setContentView(None, screen) # for compatibility with old apps + +# new_activity might be None for compatibility, can be removed if compatibility is no longer needed +def setContentView(new_activity, new_screen): global screen_stack - topscreen = None + + # Get current activity and screen + current_activity, current_screen = None, None if len(screen_stack) > 0: - topscreen = screen_stack[-1] - if not topscreen or screen != topscreen: - print("Appending screen to screen_stack") - screen_stack.append(screen) - else: - print("Warning: not adding new screen to screen_stack because it's already there, just bringing to foreground.") + current_activity, current_screen = screen_stack[-1] + + if current_activity and current_screen: + # Notify current activity it's being backgrounded: + current_activity.onPause(current_screen) + current_activity.onStop(current_screen) + + # Start the new one: + print("Appending screen to screen_stack") + screen_stack.append((new_activity, new_screen)) close_top_layer_msgboxes() # otherwise they remain - lv.screen_load(screen) + if new_activity: + new_activity.onStart(new_screen) # Initialize UI elements + lv.screen_load(new_screen) + if new_activity: + new_activity.onResume(new_screen) # Screen is now active + def back_screen(): global screen_stack - if len(screen_stack) > 1: - #clean_top_layer() - #print("Adding notification bar and drawer to top layer") - #mpos.ui.create_notification_bar() - #mpos.ui.create_drawer() - #close_top_layer_msgboxes() # would be nicer to "cancel" all input events - - print("Loading previous screen") - screen_stack.pop() # Remove current screen - prevscreen = screen_stack[-1] # load previous screen - lv.screen_load(prevscreen) - if len(screen_stack) == 1: - open_bar() - else: + if len(screen_stack) <= 1: print("Warning: can't go back because screen_stack is empty.") + return False # No previous screen + #close_top_layer_msgboxes() # would be nicer to "cancel" all input events + print("Loading previous screen") + current_activity, current_screen = screen_stack.pop() # Remove current screen + if current_activity: + current_activity.onPause(current_screen) + current_activity.onStop(current_screen) + current_activity.onDestroy(current_screen) + # System deletes the screen + if current_screen: + current_screen.delete() + current_screen = None + prev_activity, prev_screen = screen_stack[-1] # load previous screen + lv.screen_load(prev_screen) + if prev_activity: + prev_activity.onResume(prev_screen) + if len(screen_stack) == 1: + open_bar()