|
| 1 | +import os |
| 2 | +import lvgl as lv |
| 3 | + |
| 4 | +from mpos import Activity, DownloadManager, PackageManager, TaskManager |
| 5 | + |
| 6 | +class AppDetail(Activity): |
| 7 | + |
| 8 | + action_label_install = "Install" |
| 9 | + action_label_uninstall = "Uninstall" |
| 10 | + action_label_restore = "Restore Built-in" |
| 11 | + action_label_nothing = "Disable" # This could mark builtin apps as "Disabled" somehow and also allow for "Enable" then |
| 12 | + |
| 13 | + # Widgets: |
| 14 | + install_button = None |
| 15 | + update_button = None |
| 16 | + progress_bar = None |
| 17 | + install_label = None |
| 18 | + long_desc_label = None |
| 19 | + version_label = None |
| 20 | + buttoncont = None |
| 21 | + publisher_label = None |
| 22 | + |
| 23 | + # Received from the Intent extras: |
| 24 | + app = None |
| 25 | + appstore = None |
| 26 | + |
| 27 | + def onCreate(self): |
| 28 | + print("Creating app detail screen...") |
| 29 | + self.app = self.getIntent().extras.get("app") |
| 30 | + self.appstore = self.getIntent().extras.get("appstore") |
| 31 | + app_detail_screen = lv.obj() |
| 32 | + app_detail_screen.set_style_pad_all(5, 0) |
| 33 | + app_detail_screen.set_size(lv.pct(100), lv.pct(100)) |
| 34 | + app_detail_screen.set_pos(0, 40) |
| 35 | + app_detail_screen.set_flex_flow(lv.FLEX_FLOW.COLUMN) |
| 36 | + |
| 37 | + headercont = lv.obj(app_detail_screen) |
| 38 | + headercont.set_style_border_width(0, 0) |
| 39 | + headercont.set_style_pad_all(0, 0) |
| 40 | + headercont.set_flex_flow(lv.FLEX_FLOW.ROW) |
| 41 | + headercont.set_size(lv.pct(100), lv.SIZE_CONTENT) |
| 42 | + headercont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) |
| 43 | + icon_spacer = lv.image(headercont) |
| 44 | + icon_spacer.set_size(64, 64) |
| 45 | + if self.app.icon_data: |
| 46 | + image_dsc = lv.image_dsc_t({ |
| 47 | + 'data_size': len(self.app.icon_data), |
| 48 | + 'data': self.app.icon_data |
| 49 | + }) |
| 50 | + icon_spacer.set_src(image_dsc) |
| 51 | + else: |
| 52 | + icon_spacer.set_src(lv.SYMBOL.IMAGE) |
| 53 | + detail_cont = lv.obj(headercont) |
| 54 | + detail_cont.set_style_border_width(0, 0) |
| 55 | + detail_cont.set_style_radius(0, 0) |
| 56 | + detail_cont.set_style_pad_all(0, 0) |
| 57 | + detail_cont.set_flex_flow(lv.FLEX_FLOW.COLUMN) |
| 58 | + detail_cont.set_size(lv.pct(75), lv.SIZE_CONTENT) |
| 59 | + detail_cont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) |
| 60 | + name_label = lv.label(detail_cont) |
| 61 | + name_label.set_text(self.app.name) |
| 62 | + name_label.set_style_text_font(lv.font_montserrat_24, 0) |
| 63 | + self.publisher_label = lv.label(detail_cont) |
| 64 | + if self.app.publisher: |
| 65 | + self.publisher_label.set_text(self.app.publisher) |
| 66 | + else: |
| 67 | + self.publisher_label.set_text("Unknown publisher") |
| 68 | + self.publisher_label.set_style_text_font(lv.font_montserrat_16, 0) |
| 69 | + |
| 70 | + self.progress_bar = lv.bar(app_detail_screen) |
| 71 | + self.progress_bar.set_width(lv.pct(100)) |
| 72 | + self.progress_bar.set_range(0, 100) |
| 73 | + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) |
| 74 | + # Always have this button: |
| 75 | + self.buttoncont = lv.obj(app_detail_screen) |
| 76 | + self.buttoncont.set_style_border_width(0, 0) |
| 77 | + self.buttoncont.set_style_radius(0, 0) |
| 78 | + self.buttoncont.set_style_pad_all(0, 0) |
| 79 | + self.buttoncont.set_flex_flow(lv.FLEX_FLOW.ROW) |
| 80 | + self.buttoncont.set_size(lv.pct(100), lv.SIZE_CONTENT) |
| 81 | + self.buttoncont.set_scrollbar_mode(lv.SCROLLBAR_MODE.OFF) |
| 82 | + self.add_action_buttons(self.buttoncont, self.app) |
| 83 | + # version label: |
| 84 | + self.version_label = lv.label(app_detail_screen) |
| 85 | + self.version_label.set_width(lv.pct(100)) |
| 86 | + if self.app.version: |
| 87 | + self.version_label.set_text(f"Latest version: {self.app.version}") # would be nice to make this bold if this is newer than the currently installed one |
| 88 | + else: |
| 89 | + self.version_label.set_text(f"Unknown version") |
| 90 | + self.version_label.set_style_text_font(lv.font_montserrat_12, 0) |
| 91 | + self.version_label.align_to(self.install_button, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) |
| 92 | + self.long_desc_label = lv.label(app_detail_screen) |
| 93 | + self.long_desc_label.align_to(self.version_label, lv.ALIGN.OUT_BOTTOM_MID, 0, lv.pct(5)) |
| 94 | + if self.app.long_description: |
| 95 | + self.long_desc_label.set_text(self.app.long_description) |
| 96 | + else: |
| 97 | + self.long_desc_label.set_text(self.app.short_description) |
| 98 | + self.long_desc_label.set_style_text_font(lv.font_montserrat_12, 0) |
| 99 | + self.long_desc_label.set_width(lv.pct(100)) |
| 100 | + print("Loading app detail screen...") |
| 101 | + self.setContentView(app_detail_screen) |
| 102 | + |
| 103 | + def onResume(self, screen): |
| 104 | + backend_type = self.appstore.get_backend_type_from_settings() |
| 105 | + if backend_type == self.appstore._BACKEND_API_BADGEHUB: |
| 106 | + TaskManager.create_task(self.fetch_and_set_app_details()) |
| 107 | + else: |
| 108 | + print("No need to fetch app details as the github app index already contains all the app data.") |
| 109 | + |
| 110 | + def add_action_buttons(self, buttoncont, app): |
| 111 | + buttoncont.clean() |
| 112 | + print(f"Adding (un)install button for url: {self.app.download_url}") |
| 113 | + self.install_button = lv.button(buttoncont) |
| 114 | + self.install_button.add_event_cb(lambda e, a=self.app: self.toggle_install(a), lv.EVENT.CLICKED, None) |
| 115 | + self.install_button.set_size(lv.pct(100), 40) |
| 116 | + self.install_label = lv.label(self.install_button) |
| 117 | + self.install_label.center() |
| 118 | + self.set_install_label(self.app.fullname) |
| 119 | + if app.version and PackageManager.is_update_available(self.app.fullname, app.version): |
| 120 | + self.install_button.set_size(lv.pct(47), 40) # make space for update button |
| 121 | + print("Update available, adding update button.") |
| 122 | + self.update_button = lv.button(buttoncont) |
| 123 | + self.update_button.set_size(lv.pct(47), 40) |
| 124 | + self.update_button.add_event_cb(lambda e, a=self.app: self.update_button_click(a), lv.EVENT.CLICKED, None) |
| 125 | + update_label = lv.label(self.update_button) |
| 126 | + update_label.set_text("Update") |
| 127 | + update_label.center() |
| 128 | + |
| 129 | + async def fetch_and_set_app_details(self): |
| 130 | + await self.appstore.fetch_badgehub_app_details(self.app) |
| 131 | + print(f"app has version: {self.app.version}") |
| 132 | + self.version_label.set_text(self.app.version) |
| 133 | + self.long_desc_label.set_text(self.app.long_description) |
| 134 | + self.publisher_label.set_text(self.app.publisher) |
| 135 | + self.add_action_buttons(self.buttoncont, self.app) |
| 136 | + |
| 137 | + def set_install_label(self, app_fullname): |
| 138 | + # Figure out whether to show: |
| 139 | + # - "install" option if not installed |
| 140 | + # - "update" option if already installed and new version |
| 141 | + # - "uninstall" option if already installed and not builtin |
| 142 | + # - "restore builtin" option if it's an overridden builtin app |
| 143 | + # So: |
| 144 | + # - install, uninstall and restore builtin can be same button, always shown |
| 145 | + # - update is separate button, only shown if already installed and new version |
| 146 | + is_installed = True |
| 147 | + update_available = False |
| 148 | + builtin_app = PackageManager.is_builtin_app(app_fullname) |
| 149 | + overridden_builtin_app = PackageManager.is_overridden_builtin_app(app_fullname) |
| 150 | + if not overridden_builtin_app: |
| 151 | + is_installed = PackageManager.is_installed_by_name(app_fullname) |
| 152 | + if is_installed: |
| 153 | + if builtin_app: |
| 154 | + if overridden_builtin_app: |
| 155 | + action_label = self.action_label_restore |
| 156 | + else: |
| 157 | + action_label = self.action_label_nothing |
| 158 | + else: |
| 159 | + action_label = self.action_label_uninstall |
| 160 | + else: |
| 161 | + action_label = self.action_label_install |
| 162 | + self.install_label.set_text(action_label) |
| 163 | + |
| 164 | + def toggle_install(self, app_obj): |
| 165 | + print(f"Install button clicked for {app_obj}") |
| 166 | + download_url = app_obj.download_url |
| 167 | + fullname = app_obj.fullname |
| 168 | + print(f"With {download_url} and fullname {fullname}") |
| 169 | + label_text = self.install_label.get_text() |
| 170 | + if label_text == self.action_label_install: |
| 171 | + print("Starting install task...") |
| 172 | + TaskManager.create_task(self.download_and_install(app_obj, f"apps/{fullname}")) |
| 173 | + elif label_text == self.action_label_uninstall or label_text == self.action_label_restore: |
| 174 | + print("Starting uninstall task...") |
| 175 | + TaskManager.create_task(self.uninstall_app(fullname)) |
| 176 | + |
| 177 | + def update_button_click(self, app_obj): |
| 178 | + download_url = app_obj.download_url |
| 179 | + fullname = app_obj.fullname |
| 180 | + print(f"Update button clicked for {download_url} and fullname {fullname}") |
| 181 | + self.update_button.add_flag(lv.obj.FLAG.HIDDEN) |
| 182 | + self.install_button.set_size(lv.pct(100), 40) |
| 183 | + TaskManager.create_task(self.download_and_install(app_obj, f"apps/{fullname}")) |
| 184 | + |
| 185 | + async def uninstall_app(self, app_fullname): |
| 186 | + self.install_button.add_state(lv.STATE.DISABLED) |
| 187 | + self.install_label.set_text("Please wait...") |
| 188 | + self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) |
| 189 | + self.progress_bar.set_value(21, True) |
| 190 | + await TaskManager.sleep(1) # seems silly but otherwise it goes so quickly that the user can't tell something happened and gets confused |
| 191 | + self.progress_bar.set_value(42, True) |
| 192 | + await TaskManager.sleep(1) # seems silly but otherwise it goes so quickly that the user can't tell something happened and gets confused |
| 193 | + PackageManager.uninstall_app(app_fullname) |
| 194 | + await TaskManager.sleep(1) # seems silly but otherwise it goes so quickly that the user can't tell something happened and gets confused |
| 195 | + self.progress_bar.set_value(100, False) |
| 196 | + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) |
| 197 | + self.progress_bar.set_value(0, False) |
| 198 | + self.set_install_label(app_fullname) |
| 199 | + self.install_button.remove_state(lv.STATE.DISABLED) |
| 200 | + if PackageManager.is_builtin_app(app_fullname): |
| 201 | + self.update_button.remove_flag(lv.obj.FLAG.HIDDEN) |
| 202 | + self.install_button.set_size(lv.pct(47), 40) # if a builtin app was removed, then it was overridden, and a new version is available, so make space for update button |
| 203 | + |
| 204 | + async def pcb(self, percent): |
| 205 | + print(f"pcb called: {percent}") |
| 206 | + scaled_percent_start = 5 # before 5% is preparation |
| 207 | + scaled_percent_finished = 60 # after 60% is unzip |
| 208 | + scaled_percent_diff = scaled_percent_finished - scaled_percent_start |
| 209 | + scale = 100 / scaled_percent_diff # 100 / 55 = 1.81 |
| 210 | + scaled_percent = round(percent / scale) |
| 211 | + scaled_percent += scaled_percent_start |
| 212 | + self.progress_bar.set_value(scaled_percent, True) |
| 213 | + |
| 214 | + async def download_and_install(self, app_obj, dest_folder): |
| 215 | + zip_url = app_obj.download_url |
| 216 | + app_fullname = app_obj.fullname |
| 217 | + download_url_size = None |
| 218 | + if hasattr(app_obj, "download_url_size"): |
| 219 | + download_url_size = app_obj.download_url_size |
| 220 | + self.install_button.add_state(lv.STATE.DISABLED) |
| 221 | + self.install_label.set_text("Please wait...") |
| 222 | + self.progress_bar.remove_flag(lv.obj.FLAG.HIDDEN) |
| 223 | + self.progress_bar.set_value(5, True) |
| 224 | + await TaskManager.sleep(1) # seems silly but otherwise it goes so quickly that the user can't tell something happened and gets confused |
| 225 | + # Download the .mpk file to temporary location |
| 226 | + try: |
| 227 | + # Make sure there's no leftover file filling the storage |
| 228 | + os.remove(temp_zip_path) |
| 229 | + except Exception: |
| 230 | + pass |
| 231 | + try: |
| 232 | + os.mkdir("tmp") |
| 233 | + except Exception: |
| 234 | + pass |
| 235 | + temp_zip_path = "tmp/temp.mpk" |
| 236 | + print(f"Downloading .mpk file from: {zip_url} to {temp_zip_path}") |
| 237 | + try: |
| 238 | + result = await DownloadManager.download_url(zip_url, outfile=temp_zip_path, total_size=download_url_size, progress_callback=self.pcb) |
| 239 | + if result is not True: |
| 240 | + print("Download failed...") # Would be good to show an error to the user if this failed... |
| 241 | + else: |
| 242 | + print("Downloaded .mpk file, size:", os.stat(temp_zip_path)[6], "bytes") |
| 243 | + # Install it: |
| 244 | + PackageManager.install_mpk(temp_zip_path, dest_folder) # 60 until 90 percent is the unzip but no progress there... |
| 245 | + self.progress_bar.set_value(90, True) |
| 246 | + except Exception as e: |
| 247 | + print(f"Download failed with exception: {e}") |
| 248 | + if DownloadManager.is_network_error(e): |
| 249 | + self.install_label.set_text(f"Network error - check WiFi") |
| 250 | + else: |
| 251 | + self.install_label.set_text(f"Download failed: {str(e)[:30]}") |
| 252 | + self.install_button.remove_state(lv.STATE.DISABLED) |
| 253 | + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) |
| 254 | + self.progress_bar.set_value(0, False) |
| 255 | + # Make sure there's no leftover file filling the storage: |
| 256 | + try: |
| 257 | + os.remove(temp_zip_path) |
| 258 | + except Exception: |
| 259 | + pass |
| 260 | + return |
| 261 | + # Make sure there's no leftover file filling the storage: |
| 262 | + try: |
| 263 | + os.remove(temp_zip_path) |
| 264 | + except Exception: |
| 265 | + pass |
| 266 | + # Success: |
| 267 | + await TaskManager.sleep(1) # seems silly but otherwise it goes so quickly that the user can't tell something happened and gets confused |
| 268 | + self.progress_bar.set_value(100, False) |
| 269 | + self.progress_bar.add_flag(lv.obj.FLAG.HIDDEN) |
| 270 | + self.progress_bar.set_value(0, False) |
| 271 | + self.set_install_label(app_fullname) |
| 272 | + self.install_button.remove_state(lv.STATE.DISABLED) |
0 commit comments