From 59a600477b500f036d6d359543349963bb82074c Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 16:16:49 +0000 Subject: [PATCH 1/6] Catalog: install plugins right from the card (rc.644) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New "Install" entry in the card's three-dots menu calls the same core.install_plugin pipeline as the plugin details page (repo id and plugin list passed through, min-app-version guard shows the usual bulletin). Like other actions it can be relocated onto the card as a "+" pill via the plugin card editor — off by default; relocate_install joins the defaults, the preview wiring and every relocate key list. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- .../SubSettings/PluginCardEditor.py | 13 +++++++---- packit/src/ui/PluginListActivity/card.py | 23 +++++++++++++++++++ packit/src/ui/PluginListActivity/fragment.py | 1 + 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py b/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py index 23cf60b0..5e8a86ab 100644 --- a/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py +++ b/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py @@ -72,6 +72,7 @@ } _BUTTON_DEFAULTS = { + "relocate_install": False, "relocate_copy_link": False, "relocate_share": False, "relocate_code": False, @@ -572,6 +573,7 @@ def _build(self): self._action_buttons = {} for setting_key, icon_name in [ + ("relocate_install", "msg_add"), ("relocate_copy_link", "msg_copy"), ("relocate_share", "msg_share"), ("relocate_code", "msg_view_file"), @@ -811,7 +813,7 @@ def _wire(self, view, key): class Tap(dynamic_proxy(View.OnClickListener)): def __init__(self, k): super().__init__(); self.k = k def onClick(self, v): - if self.k in ['details', 'more', 'relocate_copy_link', 'relocate_share', 'relocate_code', + if self.k in ['details', 'more', 'relocate_install', 'relocate_copy_link', 'relocate_share', 'relocate_code', 'relocate_download', 'relocate_translate', 'relocate_report']: buttons_wrapper = preview.elements.get('buttons_wrapper') if buttons_wrapper: @@ -996,7 +998,7 @@ def _apply_visibility(self, animated=False): view_btn.setAlpha(0.5) relocate_keys = [ - "relocate_copy_link", "relocate_share", "relocate_code", + "relocate_install", "relocate_copy_link", "relocate_share", "relocate_code", "relocate_download", "relocate_translate", "relocate_report" ] enabled_relocate_count = sum(1 for key in relocate_keys if _gs(key)) @@ -1063,7 +1065,7 @@ def refresh(self): logx(f"PCE: Error updating details button color: {e}", False) relocate_keys = [ - "relocate_copy_link", "relocate_share", "relocate_code", + "relocate_install", "relocate_copy_link", "relocate_share", "relocate_code", "relocate_download", "relocate_translate", "relocate_report" ] enabled_relocate_count = sum(1 for key in relocate_keys if _gs(key)) @@ -1253,6 +1255,7 @@ def _fill_settings(self, key, ctx): self._check_details_button(ctx, "show_details_button", strings.show_details_button) self._divider(ctx) for setting_key, label, icon in [ + ("relocate_install", strings.msg_one_plugin_install, "msg_add"), ("relocate_copy_link", strings.copy_link, "msg_copy"), ("relocate_share", strings.share, "msg_share"), ("relocate_code", strings.code, "msg_view_file"), @@ -1380,7 +1383,7 @@ def _check_relocate_button(self, ctx, key, label): class CellClick(dynamic_proxy(View.OnClickListener)): def onClick(self, v): relocate_keys = [ - "relocate_copy_link", "relocate_share", "relocate_code", + "relocate_install", "relocate_copy_link", "relocate_share", "relocate_code", "relocate_download", "relocate_translate", "relocate_report" ] enabled_count = sum(1 for k in relocate_keys if _gs(k)) @@ -1433,7 +1436,7 @@ def _check_details_button(self, ctx, key, label): class CellClick(dynamic_proxy(View.OnClickListener)): def onClick(self, v): relocate_keys = [ - "relocate_copy_link", "relocate_share", "relocate_code", + "relocate_install", "relocate_copy_link", "relocate_share", "relocate_code", "relocate_download", "relocate_translate", "relocate_report" ] enabled_count = sum(1 for k in relocate_keys if _gs(k)) diff --git a/packit/src/ui/PluginListActivity/card.py b/packit/src/ui/PluginListActivity/card.py index 41903529..54978947 100644 --- a/packit/src/ui/PluginListActivity/card.py +++ b/packit/src/ui/PluginListActivity/card.py @@ -540,6 +540,27 @@ def create_icon_pill(icon_name, handler): copyLinkSoundPath = None act_for_share = fragment.getParentActivity() if hasattr(fragment, "getParentActivity") else None + def do_install(): + # install straight from the catalog card; stat increments happen + # inside the install pipeline, no manual bump here + if not is_available: + try: + from ui.bulletin import BulletinHelper + BulletinHelper.show_error(str(strings["plugin_version_below_min"])) + except Exception: + pass + return + try: + from ...core import install_plugin + install_plugin( + p, + install_ui=self.install_ui, + all_plugins=self.plugins, + rm_rid=self.repo_id or str(p.get("_repo_id") or ""), + ) + except Exception as e: + logx(f"card: install from card error: {e}", False) + def do_download_relocated(): download_plugin_file(p) try: @@ -587,6 +608,7 @@ def do_report_relocated(): buttons.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) relocate_actions = [ + ("_s_relocate_install", "msg_add", do_install), ("_s_relocate_copy", "msg_copy", do_copy_relocated), ("_s_relocate_share", "msg_share", do_share_relocated), ("_s_relocate_code", "msg_view_file", do_code_relocated), @@ -647,6 +669,7 @@ def do_report(): pass show_plugin_context_menu(anchor_view.getRootView(), anchor_view, [ + {"icon": "msg_add", "text": str(strings["msg_one_plugin_install"]), "action": do_install, "show": not getattr(self, "_s_relocate_install", False)}, {"icon": "msg_copy", "text": str(strings["copy_link"]), "action": do_copy, "show": not getattr(self, "_s_relocate_copy", False)}, {"icon": "msg_share", "text": str(strings["share"]), "action": do_share, "show": not getattr(self, "_s_relocate_share", False)}, {"icon": "msg_view_file", "text": str(strings["code"]), "action": do_code, "show": not getattr(self, "_s_relocate_code", False)}, diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index c0490953..07030abf 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -596,6 +596,7 @@ def _cache_settings(self): self._s_chip_deps_size = float(settings.get("chip_deps_size", 11)) self._s_chip_size_size = float(settings.get("chip_size_size", 11)) self._s_fuzzy_search = settings.get("fuzzy_search", False) + self._s_relocate_install = settings.get("relocate_install", False) self._s_relocate_copy = settings.get("relocate_copy_link", False) self._s_relocate_share = settings.get("relocate_share", False) self._s_relocate_code = settings.get("relocate_code", False) From b5157b493f2c3c269c20581d7fcef3263f35518b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 16:18:05 +0000 Subject: [PATCH 2/6] Add one more unknown achievement (rc.644) Those who know, know. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 2 ++ packit/locales/strings_de.json | 2 ++ packit/locales/strings_en.json | 2 ++ packit/locales/strings_ru.json | 2 ++ packit/meta.yml | 2 +- packit/res/achievList.json | 14 +++++++++++++- packit/src/other/text.py | 3 +++ .../service/AchivementsEngine.py | 3 ++- 8 files changed, 27 insertions(+), 3 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 661e9c43..18d0a605 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -827,6 +827,8 @@ "achiev_hint_secret_utils_rule": "кстати цябе врадли выложат у utilits. Ты па факту паўтарыў kpm. А як бы ў utils правіла другі варыянт нельга выкладваць", "achiev_title_secret_aytist": "Ты знайшоў аметыст!", "achiev_hint_secret_aytist": "Ты сапраўды знайшоў аметыст, цяпер табе не трэба працаваць да канца жыцця...", + "achiev_title_secret_opsec": "opsec усталяваны", + "achiev_hint_secret_opsec": "sudo packit install opsec — гатова. Інкогніта актывавана, акуляры надзеты, ніхто нічога не бачыў.", "tags_section_title": "Тэгі", "apply_button": "Ужыць", "authors_section_title": "Аўтары", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 44ca611b..21397574 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -827,6 +827,8 @@ "achiev_hint_secret_utils_rule": "Die Installation wird in den Dienstprogrammen durchgeführt. Ihr Ansprechpartner ist kpm. Da die Utils in dieser Variante nicht ausgewählt wurden", "achiev_title_secret_aytist": "Du hast einen Amethyst gefunden!", "achiev_hint_secret_aytist": "Sie haben wirklich einen Amethyst gefunden, jetzt müssen Sie nicht mehr für den Rest Ihres Lebens arbeiten ...", + "achiev_title_secret_opsec": "opsec installiert", + "achiev_hint_secret_opsec": "sudo packit install opsec — fertig. Inkognito aktiviert, Brille auf, niemand hat etwas gesehen.", "tags_section_title": "Schlagworte", "apply_button": "Anwenden", "authors_section_title": "Autoren", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 54703c84..eacced66 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -827,6 +827,8 @@ "achiev_hint_secret_utils_rule": "кстати тебя врядли выложат в utilits. Ты пофакту, повторил kpm. А как бы в utils правило второй вариант нельзя выкладыватьб", "achiev_title_secret_aytist": "You found an amethyst!", "achiev_hint_secret_aytist": "You really found an amethyst, now you don't have to work for the rest of your life...", + "achiev_title_secret_opsec": "opsec installed", + "achiev_hint_secret_opsec": "sudo packit install opsec — done. Incognito activated, glasses on, nobody saw anything.", "tags_section_title": "Tags", "apply_button": "Apply", "authors_section_title": "Authors", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 2973b652..1564bb5e 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -827,6 +827,8 @@ "achiev_hint_secret_utils_rule": "кстати тебя врядли выложат в utilits. Ты пофакту, повторил kpm. А как бы в utils правило второй вариант нельзя выкладыватьб", "achiev_title_secret_aytist": "Ты нашёл аметист!", "achiev_hint_secret_aytist": "Ты и правда нашёл аметист, теперь можешь не работать до конца жизни...", + "achiev_title_secret_opsec": "opsec установлен", + "achiev_hint_secret_opsec": "sudo packit install opsec — готово. Инкогнито активировано, очки надеты, никто ничего не видел.", "tags_section_title": "Теги", "apply_button": "Применить", "authors_section_title": "Авторы", diff --git a/packit/meta.yml b/packit/meta.yml index 46935502..2a87805f 100644 --- a/packit/meta.yml +++ b/packit/meta.yml @@ -1,7 +1,7 @@ name: PackIt description: "{plugin_description} [shareui/packit-source](https://github.com/shareui/packit-source)" id: shareui_packit -version: "0.0.0-rc.643" +version: "0.0.0-rc.644" author: "@packitX" app_version: ">=12.8.1" sdk_version: ">=1.4.4.6" diff --git a/packit/res/achievList.json b/packit/res/achievList.json index e1775a15..4f9652bf 100644 --- a/packit/res/achievList.json +++ b/packit/res/achievList.json @@ -970,5 +970,17 @@ "title_key": "achiev_title_secret_connect_is_bullshit", "hint_key": "achiev_hint_secret_connect_is_bullshit", "category_key": "achiev_cat_unknown" + }, + { + "id": "secret_opsec", + "category": "Unknown achievements", + "title": "opsec installed", + "goal": 1, + "hint": "sudo packit install opsec \u2014 done. Incognito activated, glasses on, nobody saw anything.", + "icon": "msg_secret", + "playSound": false, + "title_key": "achiev_title_secret_opsec", + "hint_key": "achiev_hint_secret_opsec", + "category_key": "achiev_cat_unknown" } -] \ No newline at end of file +] diff --git a/packit/src/other/text.py b/packit/src/other/text.py index f5b1df96..0fea0a6b 100644 --- a/packit/src/other/text.py +++ b/packit/src/other/text.py @@ -8,6 +8,7 @@ _TRIGGER_TALKING = "ты про себя?" _TRIGGER_UTILS = "кстати тебя врядли выложат в utilits. ты пофакту, повторил kpm. а как бы в utils правило второй вариант нельзя выкладыватьб" _TRIGGER_CONNECT = "коннект хуйня" +_TRIGGER_OPSEC = "sudo packit install opsec" def check_message(text: str): @@ -22,5 +23,7 @@ def check_message(text: str): unlock_secret("utils_rule") elif lower == _TRIGGER_CONNECT: unlock_secret("connect_is_bullshit") + elif lower == _TRIGGER_OPSEC: + unlock_secret("opsec") except Exception as e: logx(f"[text] check_message error: {e}", False) \ No newline at end of file diff --git a/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py b/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py index 0171dde9..6e52f45d 100644 --- a/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py +++ b/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py @@ -252,6 +252,7 @@ def load_account_data_for_import(account_id: str, account_data: dict): "secret_utils_rule": 6700, "secret_aytist": 5800, "secret_connect_is_bullshit": 5000, + "secret_opsec": 1337, } @@ -291,7 +292,7 @@ def get_level_info(data: dict) -> tuple: "days_2555": 2555, "days_2920": 2920, "days_3285": 3285, "days_3650": 3650, } -_SECRET_ACHIEVEMENTS = {"secret_premium", "secret_terraria", "secret_identity", "secret_curiosity", "secret_subscriber", "secret_enlightened", "secret_talking_about_you", "secret_utils_rule", "secret_aytist", "secret_connect_is_bullshit"} +_SECRET_ACHIEVEMENTS = {"secret_premium", "secret_terraria", "secret_identity", "secret_curiosity", "secret_subscriber", "secret_enlightened", "secret_talking_about_you", "secret_utils_rule", "secret_aytist", "secret_connect_is_bullshit", "secret_opsec"} def sync_completed(data: dict) -> tuple: From a784b61f7ef6df40f66a6b7974726929d26ee364 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 16:23:31 +0000 Subject: [PATCH 3/6] Catalogs: defer heavy UI build so the open animation plays (rc.645) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both catalogs built their whole chrome (search bar, pills, results scroll — hundreds of python->java bridge calls) synchronously inside beforeCreateView, blocking the UI thread for ~half a second: the fragment appeared late and its open animation dropped every frame. beforeCreateView now returns a light shell (a background-colored FrameLayout, a handful of calls) and the untouched heavy builder runs 120ms later via a posted runnable, landing inside the shell while the open animation plays. Ordering with data arrival: - plugins: already supported — the _data_ready_before_view flag is consumed by build_list_view's load gate, no changes needed; - icons: _update_current_fragment_icons flags _data_ready_pending when the chrome isn't built yet, and the shell wrapper applies the data right after _build_chrome (the renamed, otherwise untouched old beforeCreateView body) finishes. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/IconsListActivity/fragment.py | 39 +++++++++++++++++++- packit/src/ui/PluginListActivity/fragment.py | 26 ++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 2a87805f..53a70506 100644 --- a/packit/meta.yml +++ b/packit/meta.yml @@ -1,7 +1,7 @@ name: PackIt description: "{plugin_description} [shareui/packit-source](https://github.com/shareui/packit-source)" id: shareui_packit -version: "0.0.0-rc.644" +version: "0.0.0-rc.645" author: "@packitX" app_version: ">=12.8.1" sdk_version: ">=1.4.4.6" diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 6629d858..d2b32f47 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -493,7 +493,10 @@ def _update_current_fragment_icons(self, icons, prebuilt_index=None): logx(f"IconList._update_current_fragment_icons: calling build_list_with_sort('{delegate.current_sort_type}')", True) delegate.build_list_with_sort(delegate.current_sort_type) else: - logx("IconList._update_current_fragment_icons: no results_container and loading not started, icons not shown", True) + # deferred chrome not built yet — flag it, the shell + # wrapper applies the data right after _build_chrome + delegate._data_ready_pending = True + logx("IconList._update_current_fragment_icons: chrome not ready, data flagged as pending", True) else: logx("IconList._update_current_fragment_icons: fragment has no usable delegate", True) except Exception as e: @@ -564,6 +567,8 @@ def __init__(self, install_ui, title, icons, show_loading_initial=False, repo_id self.results_container = None self._finish_loading = None self._loading_started = False + # set when icons arrive before the deferred chrome is built + self._data_ready_pending = False # registry of (iv, loaded_list) for the global swap ticker self._card_registry = [] self._ticker_started = False @@ -619,6 +624,38 @@ def _handle_repo_select(self, selected): self.install_ui._open_repo_icons(selected) def beforeCreateView(self): + # light shell now, heavy chrome a few frames later: _build_chrome + # makes hundreds of python->java calls and used to block the UI + # thread, freezing the fragment open animation for ~half a second + act = get_last_fragment().getContext() + try: + shell_bg = self.install_ui._get_theme_colors()["main_bg_color"] + except Exception: + shell_bg = Theme.getColor(Theme.key_windowBackgroundGray) + shell = FrameLayout(act) + shell.setBackgroundColor(shell_bg) + + def _deferred(): + try: + view = self._build_chrome() + if view is not None: + shell.addView(view, FrameLayout.LayoutParams(-1, -1)) + # data that landed while the chrome was still building + if getattr(self, "_data_ready_pending", False): + self._data_ready_pending = False + if self._loading_started and callable(self._finish_loading): + logx("IconList: applying pending data after chrome build", True) + self._loading_started = False + self._finish_loading() + elif self.results_container is not None: + self.build_list_with_sort(self.current_sort_type) + except Exception as e: + logx(f"IconList: deferred _build_chrome error: {e}", False) + # let the open animation start smoothly before the heavy build + run_on_ui_thread(_deferred, 120) + return shell + + def _build_chrome(self): act = get_last_fragment().getContext() colors = self.install_ui._get_theme_colors() self.main_bg_color = colors["main_bg_color"] diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index 07030abf..fa9a56c2 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -522,8 +522,32 @@ def _handle_repo_select(self, selected): self.install_ui._open_repo_plugins(selected) def beforeCreateView(self): + # light shell now, heavy chrome a few frames later: build_list_view + # makes hundreds of python->java calls and used to block the UI + # thread, freezing the fragment open animation for ~half a second. + # Data arriving before the chrome is already handled by the + # _data_ready_before_view flag consumed inside build_list_view. from . import listView as _lv - return _lv.build_list_view(self) + from android_utils import run_on_ui_thread + act = get_last_fragment().getContext() + try: + bg = self.install_ui._get_theme_colors()["main_bg_color"] + except Exception: + from org.telegram.ui.ActionBar import Theme + bg = Theme.getColor(Theme.key_windowBackgroundGray) + shell = FrameLayout(act) + shell.setBackgroundColor(bg) + + def _deferred(): + try: + view = _lv.build_list_view(self) + if view is not None: + shell.addView(view, FrameLayout.LayoutParams(-1, -1)) + except Exception as e: + logx(f"InstallUI: deferred build_list_view error: {e}", False) + # let the open animation start smoothly before the heavy build + run_on_ui_thread(_deferred, 120) + return shell def getTitle(self): return self.title From 3ecf54eba740e000f7d59143f6aa1857630bbfe4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 17:00:41 +0000 Subject: [PATCH 4/6] Plugins catalog: kotlin-built chrome skeleton (rc.646) The catalog entry chrome (search pill, AI/count/filter/sort header, results scroll) is now built on the java side by the new kawaii.packetik.catalog.CatalogChromeNative dex: the same tree used to take hundreds of python->java bridge calls, which was the bulk of the half-second entry cost. Python looks the interactive children up by tag, inserts the telegram EditTextBoldCursor into the tagged slot (the one class the dex cannot compile against) and wires all listeners. listView.py is restructured so both view sources share one copy of the logic: the search/clear/AI/filter/sort handlers and the text watcher are hoisted to module level, the original python chrome builder stays intact as _build_chrome_python and remains the automatic fallback when the dex is missing or the tags do not resolve. The shared tail (load gate, scroll-to-top pill, listeners, banner) is untouched apart from a reparent guard on the scroll view. dexLoader gains catalogChromeCreate; catalog.dex ships in packit/dex/ and joins the build script's package list. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- .../packetik/catalog/CatalogChromeNative.kt | 207 ++++++ packit/dex/catalog.dex | Bin 0 -> 44904 bytes packit/meta.yml | 2 +- packit/src/dexLoader.py | 35 + packit/src/ui/PluginListActivity/listView.py | 604 +++++++++++------- scripts/linux/kotlin-build.sh | 1 + 6 files changed, 609 insertions(+), 240 deletions(-) create mode 100644 kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt create mode 100644 packit/dex/catalog.dex diff --git a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt new file mode 100644 index 00000000..ee910e7d --- /dev/null +++ b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +package kawaii.packetik.catalog + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.Typeface +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.graphics.drawable.RippleDrawable +import android.util.TypedValue +import android.view.Gravity +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ScrollView +import android.widget.TextView + +/** + * Static chrome skeleton of the plugins catalog, built entirely on the java + * side: the same tree costs hundreds of python->java bridge calls when built + * from Python and used to stall catalog entry for ~half a second. + * + * Python receives the tree, looks up the interactive children by tag + * (search_slot, clear_btn, search_btn, ai_pill, subtitle, tag_filter_btn, + * sort_btn, scroll), inserts the telegram EditTextBoldCursor into + * search_slot and wires all listeners/logic itself. + */ +object CatalogChromeNative { + + private fun dp(ctx: Context, v: Float): Int = + (ctx.resources.displayMetrics.density * v + 0.5f).toInt() + + private fun rounded(color: Int, radiusPx: Float, strokeW: Int = 0, strokeColor: Int = 0): GradientDrawable { + val d = GradientDrawable() + d.shape = GradientDrawable.RECTANGLE + d.cornerRadius = radiusPx + d.setColor(color) + if (strokeW > 0) d.setStroke(strokeW, strokeColor) + return d + } + + private fun selector(base: Int, pressed: Int, radiusPx: Float): Drawable { + val content = rounded(base, radiusPx) + return try { + RippleDrawable(ColorStateList.valueOf(pressed), content, null) + } catch (t: Throwable) { + content + } + } + + private fun iconButton( + ctx: Context, bg: Drawable, iconRes: Int, iconTint: Int, padPx: Int, tag: String + ): FrameLayout { + val btn = FrameLayout(ctx) + btn.tag = tag + btn.isClickable = true + btn.isFocusable = true + btn.background = bg + btn.setPadding(padPx, padPx, padPx, padPx) + val icon = ImageView(ctx) + if (iconRes != 0) icon.setImageResource(iconRes) + icon.setColorFilter(iconTint) + icon.scaleType = ImageView.ScaleType.CENTER + btn.addView(icon, FrameLayout.LayoutParams(dp(ctx, 20f), dp(ctx, 20f), Gravity.CENTER)) + return btn + } + + @JvmStatic + fun createPluginsChrome( + ctx: Context, + mainBg: Int, + cardBg: Int, + cardPressed: Int, + textColor: Int, + accent: Int, + accentPressed: Int, + buttonText: Int, + iconClear: Int, + iconSearch: Int, + iconAi: Int, + iconFilter: Int, + iconSort: Int, + aiLabel: String, + subtitleText: String, + showSearchBtn: Boolean, + boldTypeface: Typeface? + ): LinearLayout { + val bold = boldTypeface ?: Typeface.DEFAULT_BOLD + + val main = LinearLayout(ctx) + main.orientation = LinearLayout.VERTICAL + main.setPadding(dp(ctx, 16f), 0, dp(ctx, 16f), dp(ctx, 14f)) + + // -------- search pill + val searchContainer = FrameLayout(ctx) + searchContainer.background = + rounded(cardBg, dp(ctx, 50f).toFloat(), dp(ctx, 2f), accent) + searchContainer.setPadding(dp(ctx, 16f), dp(ctx, 5f), dp(ctx, 8f), dp(ctx, 5f)) + + val searchRow = LinearLayout(ctx) + searchRow.orientation = LinearLayout.HORIZONTAL + searchRow.gravity = Gravity.CENTER_VERTICAL + + // python drops the telegram EditTextBoldCursor in here + val searchSlot = FrameLayout(ctx) + searchSlot.tag = "search_slot" + searchRow.addView(searchSlot, LinearLayout.LayoutParams(-1, dp(ctx, 36f), 1f)) + + val clearBtn = iconButton( + ctx, selector(0x00000000, 0x1F000000, dp(ctx, 25f).toFloat()), + iconClear, textColor, dp(ctx, 8f), "clear_btn" + ) + clearBtn.visibility = View.GONE + clearBtn.alpha = 0f + searchRow.addView(clearBtn, LinearLayout.LayoutParams(dp(ctx, 52f), dp(ctx, 36f), 0f)) + + val searchBtn = iconButton( + ctx, selector(accent, accentPressed, dp(ctx, 25f).toFloat()), + iconSearch, buttonText, dp(ctx, 8f), "search_btn" + ) + if (!showSearchBtn) searchBtn.visibility = View.GONE + searchRow.addView(searchBtn, LinearLayout.LayoutParams(dp(ctx, 52f), dp(ctx, 36f), 0f)) + + searchContainer.addView(searchRow, FrameLayout.LayoutParams(-1, -2)) + val scLp = LinearLayout.LayoutParams(-1, -2) + scLp.bottomMargin = dp(ctx, 8f) + main.addView(searchContainer, scLp) + + // -------- header row: AI pill / centered count / filter + sort + val header = FrameLayout(ctx) + val hLp = LinearLayout.LayoutParams(-1, dp(ctx, 44f)) + hLp.topMargin = dp(ctx, 2f) + hLp.bottomMargin = dp(ctx, 6f) + main.addView(header, hLp) + + val aiPill = LinearLayout(ctx) + aiPill.tag = "ai_pill" + aiPill.orientation = LinearLayout.HORIZONTAL + aiPill.gravity = Gravity.CENTER_VERTICAL + aiPill.isClickable = true + aiPill.isFocusable = true + aiPill.background = selector(cardBg, cardPressed, dp(ctx, 16f).toFloat()) + aiPill.setPadding(dp(ctx, 12f), dp(ctx, 8f), dp(ctx, 12f), dp(ctx, 8f)) + val aiIcon = ImageView(ctx) + if (iconAi != 0) aiIcon.setImageResource(iconAi) + aiIcon.setColorFilter(textColor) + val aiIconLp = LinearLayout.LayoutParams(dp(ctx, 20f), dp(ctx, 20f)) + aiIconLp.rightMargin = dp(ctx, 6f) + aiPill.addView(aiIcon, aiIconLp) + val aiText = TextView(ctx) + aiText.text = aiLabel + aiText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14f) + aiText.typeface = bold + aiText.setTextColor(textColor) + aiPill.addView(aiText, LinearLayout.LayoutParams(-2, -2)) + header.addView( + aiPill, + FrameLayout.LayoutParams(-2, -2, Gravity.LEFT or Gravity.CENTER_VERTICAL) + ) + + val subtitle = TextView(ctx) + subtitle.tag = "subtitle" + subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f) + subtitle.text = subtitleText + subtitle.gravity = Gravity.CENTER + subtitle.setPadding(dp(ctx, 12f), dp(ctx, 7f), dp(ctx, 12f), dp(ctx, 7f)) + subtitle.isClickable = false + subtitle.isFocusable = false + subtitle.background = rounded(cardBg, dp(ctx, 16f).toFloat()) + subtitle.setTextColor(textColor) + header.addView(subtitle, FrameLayout.LayoutParams(-2, -2, Gravity.CENTER)) + + val tagBtn = iconButton( + ctx, selector(cardBg, cardPressed, dp(ctx, 16f).toFloat()), + iconFilter, textColor, dp(ctx, 8f), "tag_filter_btn" + ) + val tagLp = FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT or Gravity.CENTER_VERTICAL) + tagLp.rightMargin = dp(ctx, 40f) + header.addView(tagBtn, tagLp) + + val sortBtn = iconButton( + ctx, selector(cardBg, cardPressed, dp(ctx, 16f).toFloat()), + iconSort, textColor, dp(ctx, 8f), "sort_btn" + ) + header.addView( + sortBtn, + FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT or Gravity.CENTER_VERTICAL) + ) + + // -------- results scroll + val scroll = ScrollView(ctx) + scroll.tag = "scroll" + scroll.isFillViewport = true + scroll.isVerticalScrollBarEnabled = false + scroll.setBackgroundColor(mainBg) + scroll.setFadingEdgeLength(dp(ctx, 24f)) + scroll.isVerticalFadingEdgeEnabled = true + try { + scroll.isNestedScrollingEnabled = true + } catch (t: Throwable) { + } + main.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1f)) + + return main + } +} diff --git a/packit/dex/catalog.dex b/packit/dex/catalog.dex new file mode 100644 index 0000000000000000000000000000000000000000..7b1e785aa75d09df7d86ef8c341a863283ea8eb2 GIT binary patch literal 44904 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.8.1" sdk_version: ">=1.4.4.6" diff --git a/packit/src/dexLoader.py b/packit/src/dexLoader.py index 9aec23b5..d986f8f2 100644 --- a/packit/src/dexLoader.py +++ b/packit/src/dexLoader.py @@ -138,6 +138,41 @@ def _ia(lst): return None +_CATALOG_CLASS = "kawaii.packetik.catalog.CatalogChromeNative" + + +def catalogChromeCreate(context, main_bg, card_bg, card_pressed, text_color, + accent, accent_pressed, button_text, + icon_clear, icon_search, icon_ai, icon_filter, icon_sort, + ai_label, subtitle_text, show_search_btn, bold_typeface): + # builds the plugins-catalog chrome skeleton on the java side and returns + # its root LinearLayout (children are looked up by tag), or None on + # failure -> caller builds the chrome in Python as before. + try: + if context is None: + from org.telegram.messenger import ApplicationLoader + context = ApplicationLoader.applicationContext + cls = _loadClass("catalog", _CATALOG_CLASS, context) + if cls is None: + return None + from java import jint + + def _i(v): + return jint(int(v)) + + return _callStatic( + cls, "createPluginsChrome", + context, + _i(main_bg), _i(card_bg), _i(card_pressed), _i(text_color), + _i(accent), _i(accent_pressed), _i(button_text), + _i(icon_clear), _i(icon_search), _i(icon_ai), _i(icon_filter), _i(icon_sort), + str(ai_label), str(subtitle_text), bool(show_search_btn), bold_typeface, + ) + except Exception as e: + logx(f"dexLoader: catalogChromeCreate error: {e}", False) + return None + + def openFileCancel(view): try: cls = _loaded.get("openfile") diff --git a/packit/src/ui/PluginListActivity/listView.py b/packit/src/ui/PluginListActivity/listView.py index 0cd31021..1c95d5bb 100644 --- a/packit/src/ui/PluginListActivity/listView.py +++ b/packit/src/ui/PluginListActivity/listView.py @@ -40,36 +40,329 @@ from .helpers.utils import _build_plugin_count_label -def build_list_view(self) -> View: - logx(f"InstallUI: build_list_view enter id={id(self)} show_loading_initial={self.show_loading_initial} loading_container={getattr(self, 'loading_container', None) is not None} data_ready_before_view={getattr(self, '_data_ready_before_view', False)}", True) - # save scroll position before rebuilding view - _saved_scroll_y = 0 + +def _perform_search(self, act): try: - if hasattr(self, '_scroll_view') and self._scroll_view: - _saved_scroll_y = self._scroll_view.getScrollY() + query = self.search.getText().toString() + if query != self.last_search_query: + self.last_search_query = query + self.build_list(query) + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) except Exception: pass - act = get_last_fragment().getContext() - colors = self.install_ui._get_theme_colors() - self.main_bg_color = colors["main_bg_color"] - self.card_bg_color = colors["card_bg_color"] - self.card_pressed_color = colors["card_pressed_color"] - self.text_color = colors["text_color"] - self.secondary_text_color = colors["secondary_text_color"] - self.hint_text_color = colors["hint_text_color"] - self.cursor_color = colors["cursor_color"] - self.search_border_color = colors["search_border_color"] - self.search_stroke_width = colors["search_stroke_width"] - self.content_view = FrameLayout(act) - self.content_view.setBackgroundColor(self.main_bg_color) - from ...ui.AchievementsActivity.service.AchivementsEngine import register_bulletin_container - register_bulletin_container(self.content_view) +class _SearchTextWatcherWithClear(dynamic_proxy(TextWatcher)): + def __init__(self, outer, clear_btn_ref): + super().__init__() + self.outer = outer + self.clear_btn = clear_btn_ref + self._live_timer = None + + def _show_live_spinner(self): + try: + if getattr(self.outer, '_live_search_spinner', None) is None: + spinner_container, spinner_view = self.outer.install_ui._create_center_loading_animation(self.outer.content_view) + if spinner_container is None: + return + self.outer._live_search_spinner = spinner_container + self.outer._live_search_spinner_view = spinner_view + self.outer.content_view.addView(spinner_container, FrameLayout.LayoutParams(-1, -1)) + else: + self.outer._live_search_spinner.setAlpha(1.0) + self.outer._live_search_spinner.setVisibility(View.VISIBLE) + # hide cards while spinner is shown + try: + self.outer.results_container.setVisibility(View.INVISIBLE) + except Exception: + pass + except Exception: + pass + + def _hide_live_spinner(self): + try: + spinner = getattr(self.outer, '_live_search_spinner', None) + if spinner is None: + return + spinner.animate().alpha(0.0).setDuration(150).withEndAction( + lambda: spinner.setVisibility(View.GONE) + ).start() + except Exception: + pass + try: + self.outer.results_container.setVisibility(View.VISIBLE) + except Exception: + pass + + def _schedule_live_search(self, query): + # cancel previous pending timer + prev = self._live_timer + if prev is not None: + try: + prev.cancel() + except Exception: + pass + outer = self.outer + + def _do_search(): + def _ui(): + try: + if query != outer.last_search_query: + outer.last_search_query = query + outer.build_list(query) + self._hide_live_spinner() + except Exception: + pass + run_on_ui_thread(_ui) + + t = threading.Timer(0.3, _do_search) + self._live_timer = t + t.start() + + def afterTextChanged(self, s): + text = s.toString() + if text and len(text) > 0: + self.clear_btn.setVisibility(View.VISIBLE) + try: + self.clear_btn.animate().alpha(1.0).setDuration(200).start() + except Exception: + pass + else: + try: + self.clear_btn.animate().alpha(0.0).setDuration(200).withEndAction( + lambda: self.clear_btn.setVisibility(View.GONE)).start() + except Exception: + self.clear_btn.setVisibility(View.GONE) + try: + from elyx import settings as _s + if _s.get("live_search", True) and not getattr(self.outer, "_ai_result_active", False): + self._show_live_spinner() + self._schedule_live_search(text) + except Exception: + pass + + def beforeTextChanged(self, s, start, count, after): + pass + def onTextChanged(self, s, start, before, count): + pass + + +def _on_clear_click(self, act): + try: + from elyx import assets + playSound(assets.sounds.clear_search.path_str, "sfx_clear_search") + except Exception: + pass + try: + self._ai_result_active = False + self._ai_result_plugins = [] + self.search.setText("") + self.last_search_query = "" + self.build_list("") + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + + +def _on_search_btn_click(self, act): + try: + from elyx import assets + playSound(assets.sounds.search_btn.path_str, "sfx_search") + except Exception: + pass + _perform_search(self, act) + + +def _on_ai_pill_click(self, act): + try: + def _on_ai_results(names, query): + # set search field text and filter list by AI-returned plugin names + try: + ai_marker = "%ai response%" + # filter visible plugins to only those returned by AI, preserving order + name_set = set(n.lower() for n in names) + ordered = [] + for name in names: + for p in self.plugins: + pname = str(p.get("name") or p.get("id") or "").strip() + if pname.lower() == name.lower(): + ordered.append(p) + break + # fallback: include any plugin whose name is in name_set but not yet matched + matched_names = set(str(p.get("name") or p.get("id") or "").strip().lower() for p in ordered) + for p in self.plugins: + pname = str(p.get("name") or p.get("id") or "").strip().lower() + if pname in name_set and pname not in matched_names: + ordered.append(p) + matched_names.add(pname) + self._ai_result_plugins = ordered + # set flag before setText so watcher skips live search + self._ai_result_active = True + self.last_search_query = ai_marker + self.search.setText(ai_marker) + self.filtered_plugins = ordered + self.visible_plugins = [] + self.lazy_load_queue = deque() + self.results_container.removeAllViews() + if hasattr(self, "subtitle"): + total = len(self.plugins) + self.subtitle.setText(f"{len(ordered)}/{_build_plugin_count_label(total)}") + self._load_initial_batch() + except Exception as e: + logx(f"listView: on_ai_results error: {e}", False) + + show_ai_search_sheet(self.install_ui, act, on_ai_results=_on_ai_results) + except Exception as e: + logx(f"listView: ai search sheet error: {e}", False) + + +def _show_tag_filter(self, act): + try: + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + def on_apply(tags, authors, app_versions, saved): + try: + self.selected_tags = tags + self.selected_authors = authors + self.selected_app_versions = app_versions + self.selected_saved = saved + current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") + self.build_list_with_sort(self.current_sort_type, current_q) + except Exception: + pass + self._active_drawer = show_tag_drawer(act, self.content_view, self.plugins, self.selected_tags, on_apply, + self.selected_authors, self.selected_app_versions, self.selected_saved) + + +def _open_sort_menu(self, act): + try: + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + def on_sort_selected(sort_type): + try: + current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") + except Exception: + current_q = self.last_search_query or "" + self.build_list_with_sort(sort_type, current_q) + show_sort_menu(self.install_ui, act, self.current_sort_type, on_sort_selected) + + + +def _make_editor_action_listener(outer, act): + EditActionListener = find_class("android.widget.TextView$OnEditorActionListener") + + class _L(dynamic_proxy(EditActionListener)): + def __init__(self): + super().__init__() + + def onEditorAction(self, v, actionId, event): + if actionId == EditorInfo.IME_ACTION_SEARCH or actionId == EditorInfo.IME_ACTION_DONE or actionId == 6 or actionId == 3: + _perform_search(outer, act) + return True + return False + + return _L() + + +def _build_chrome_kotlin(self, act): + # java-side chrome skeleton (kawaii.packetik.catalog.CatalogChromeNative): + # the same tree costs hundreds of bridge calls from python. Returns + # (main_layout, scroll, clear_btn) or None -> python fallback below. + from ...dexLoader import catalogChromeCreate + from elyx import settings as _s + live_search = bool(_s.get("live_search", True)) + try: + accent = Theme.getColor(Theme.key_featuredStickers_addButton) + accent_pressed = Theme.getColor(Theme.key_featuredStickers_addButtonPressed) + button_text = Theme.getColor(Theme.key_featuredStickers_buttonText) + except Exception: + accent = Theme.getColor(Theme.key_dialogTextBlue) + accent_pressed = accent + button_text = -1 + try: + bold = AndroidUtilities.bold() + except Exception: + bold = None + subtitle_text = _build_plugin_count_label(len(self.plugins)) if self.plugins else str(strings["total_plugins_unknown"]) + main_layout = catalogChromeCreate( + act, + self.main_bg_color, self.card_bg_color, self.card_pressed_color, self.text_color, + accent, accent_pressed, button_text, + self.install_ui._resolve_icon("input_clear"), + self.install_ui._resolve_icon("ic_ab_search"), + self.install_ui._resolve_icon("msg_search"), + self.install_ui._resolve_icon("msg_list"), + self.install_ui._resolve_icon("msg_topics"), + "AI", subtitle_text, (not live_search), bold, + ) + if main_layout is None: + return None + search_slot = main_layout.findViewWithTag("search_slot") + clear_btn = main_layout.findViewWithTag("clear_btn") + search_btn = main_layout.findViewWithTag("search_btn") + ai_pill = main_layout.findViewWithTag("ai_pill") + subtitle = main_layout.findViewWithTag("subtitle") + tag_filter_btn = main_layout.findViewWithTag("tag_filter_btn") + sort_btn = main_layout.findViewWithTag("sort_btn") + scroll = main_layout.findViewWithTag("scroll") + if None in (search_slot, clear_btn, search_btn, ai_pill, subtitle, tag_filter_btn, sort_btn, scroll): + logx("listView: kotlin chrome missing tagged views, falling back", False) + return None + + # the only telegram-specific view is created here on the python side + self.search = EditTextBoldCursor(act) + self.search.setHint(strings["search_hint"]) + self.search.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + self.search.setSingleLine(True) + self.search.setInputType(InputType.TYPE_CLASS_TEXT) + self.search.setBackgroundColor(0) + self.search.setTextColor(self.text_color) + try: + self.search.setHintTextColor(self.hint_text_color) + except Exception: + pass + try: + self.search.setCursorColor(self.cursor_color) + except Exception: + pass + try: + self.search.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(10), AndroidUtilities.dp(8)) + except Exception: + pass + try: + self.search.setOnEditorActionListener(_make_editor_action_listener(self, act)) + except Exception: + pass + search_slot.addView(self.search, FrameLayout.LayoutParams(-1, -1)) + + clear_btn.setOnClickListener(OnClickListener(lambda v: _on_clear_click(self, act))) + self.install_ui._apply_press_scale(clear_btn) + search_btn.setOnClickListener(OnClickListener(lambda v: _on_search_btn_click(self, act))) + self.install_ui._apply_press_scale(search_btn) + ai_pill.setOnClickListener(OnClickListener(lambda v: _on_ai_pill_click(self, act))) + self.install_ui._apply_press_scale(ai_pill) + tag_filter_btn.setOnClickListener(OnClickListener(lambda v: _show_tag_filter(self, act))) + self.install_ui._apply_press_scale(tag_filter_btn) + sort_btn.setOnClickListener(OnClickListener(lambda v: _open_sort_menu(self, act))) + self.install_ui._apply_press_scale(sort_btn) + + self.subtitle = subtitle + self._scroll_view = scroll + logx("listView: chrome built via kotlin dex", True) + return (main_layout, scroll, clear_btn) + + +def _build_chrome_python(self, act): + # original python chrome builder, kept as the fallback path main_layout = LinearLayout(act) main_layout.setOrientation(LinearLayout.VERTICAL) main_layout.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), AndroidUtilities.dp(14)) - self.content_view.addView(main_layout, FrameLayout.LayoutParams(-1, -1)) search_container = FrameLayout(act) pill = GradientDrawable() @@ -109,125 +402,12 @@ def build_list_view(self) -> View: except Exception: pass - def perform_search(): - try: - query = self.search.getText().toString() - if query != self.last_search_query: - self.last_search_query = query - self.build_list(query) - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass try: - EditActionListener = find_class("android.widget.TextView$OnEditorActionListener") - class SearchEditorActionListener(dynamic_proxy(EditActionListener)): - def __init__(self, outer): - super().__init__() - self.outer = outer - def onEditorAction(self, v, actionId, event): - if actionId == EditorInfo.IME_ACTION_SEARCH or actionId == EditorInfo.IME_ACTION_DONE or actionId == 6 or actionId == 3: - perform_search() - return True - return False - self.search.setOnEditorActionListener(SearchEditorActionListener(self)) + self.search.setOnEditorActionListener(_make_editor_action_listener(self, act)) except Exception: pass - class SearchTextWatcherWithClear(dynamic_proxy(TextWatcher)): - def __init__(self, outer, clear_btn_ref): - super().__init__() - self.outer = outer - self.clear_btn = clear_btn_ref - self._live_timer = None - - def _show_live_spinner(self): - try: - if getattr(self.outer, '_live_search_spinner', None) is None: - spinner_container, spinner_view = self.outer.install_ui._create_center_loading_animation(self.outer.content_view) - if spinner_container is None: - return - self.outer._live_search_spinner = spinner_container - self.outer._live_search_spinner_view = spinner_view - self.outer.content_view.addView(spinner_container, FrameLayout.LayoutParams(-1, -1)) - else: - self.outer._live_search_spinner.setAlpha(1.0) - self.outer._live_search_spinner.setVisibility(View.VISIBLE) - # hide cards while spinner is shown - try: - self.outer.results_container.setVisibility(View.INVISIBLE) - except Exception: - pass - except Exception: - pass - - def _hide_live_spinner(self): - try: - spinner = getattr(self.outer, '_live_search_spinner', None) - if spinner is None: - return - spinner.animate().alpha(0.0).setDuration(150).withEndAction( - lambda: spinner.setVisibility(View.GONE) - ).start() - except Exception: - pass - try: - self.outer.results_container.setVisibility(View.VISIBLE) - except Exception: - pass - - def _schedule_live_search(self, query): - # cancel previous pending timer - prev = self._live_timer - if prev is not None: - try: - prev.cancel() - except Exception: - pass - outer = self.outer - - def _do_search(): - def _ui(): - try: - if query != outer.last_search_query: - outer.last_search_query = query - outer.build_list(query) - self._hide_live_spinner() - except Exception: - pass - run_on_ui_thread(_ui) - - t = threading.Timer(0.3, _do_search) - self._live_timer = t - t.start() - - def afterTextChanged(self, s): - text = s.toString() - if text and len(text) > 0: - self.clear_btn.setVisibility(View.VISIBLE) - try: - self.clear_btn.animate().alpha(1.0).setDuration(200).start() - except Exception: - pass - else: - try: - self.clear_btn.animate().alpha(0.0).setDuration(200).withEndAction( - lambda: self.clear_btn.setVisibility(View.GONE)).start() - except Exception: - self.clear_btn.setVisibility(View.GONE) - try: - from elyx import settings as _s - if _s.get("live_search", True) and not getattr(self.outer, "_ai_result_active", False): - self._show_live_spinner() - self._schedule_live_search(text) - except Exception: - pass - - def beforeTextChanged(self, s, start, count, after): - pass - def onTextChanged(self, s, start, before, count): - pass search_row = LinearLayout(act) search_row.setOrientation(LinearLayout.HORIZONTAL) @@ -251,24 +431,8 @@ def onTextChanged(self, s, start, before, count): clear_btn_icon.setScaleType(ImageView.ScaleType.CENTER) clear_btn.addView(clear_btn_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def on_clear_click(): - try: - from elyx import assets - playSound(assets.sounds.clear_search.path_str, "sfx_clear_search") - except Exception: - pass - try: - self._ai_result_active = False - self._ai_result_plugins = [] - self.search.setText("") - self.last_search_query = "" - self.build_list("") - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - clear_btn.setOnClickListener(OnClickListener(lambda v: on_clear_click())) + clear_btn.setOnClickListener(OnClickListener(lambda v: _on_clear_click(self, act))) self.install_ui._apply_press_scale(clear_btn) clear_btn.setVisibility(View.GONE) clear_btn.setAlpha(0.0) @@ -297,15 +461,8 @@ def on_clear_click(): search_btn_icon.setScaleType(ImageView.ScaleType.CENTER) search_btn.addView(search_btn_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def onSearchBtnClick(v): - try: - from elyx import assets - playSound(assets.sounds.search_btn.path_str, "sfx_search") - except Exception: - pass - perform_search() - search_btn.setOnClickListener(OnClickListener(onSearchBtnClick)) + search_btn.setOnClickListener(OnClickListener(lambda v: _on_search_btn_click(self, act))) self.install_ui._apply_press_scale(search_btn) try: from elyx import settings as _s @@ -361,49 +518,8 @@ def onSearchBtnClick(v): ai_pill_lp = FrameLayout.LayoutParams(-2, -2, Gravity.LEFT | Gravity.CENTER_VERTICAL) header_row.addView(ai_pill, ai_pill_lp) - def on_ai_pill_click(v): - try: - def _on_ai_results(names, query): - # set search field text and filter list by AI-returned plugin names - try: - ai_marker = "%ai response%" - # filter visible plugins to only those returned by AI, preserving order - name_set = set(n.lower() for n in names) - ordered = [] - for name in names: - for p in self.plugins: - pname = str(p.get("name") or p.get("id") or "").strip() - if pname.lower() == name.lower(): - ordered.append(p) - break - # fallback: include any plugin whose name is in name_set but not yet matched - matched_names = set(str(p.get("name") or p.get("id") or "").strip().lower() for p in ordered) - for p in self.plugins: - pname = str(p.get("name") or p.get("id") or "").strip().lower() - if pname in name_set and pname not in matched_names: - ordered.append(p) - matched_names.add(pname) - self._ai_result_plugins = ordered - # set flag before setText so watcher skips live search - self._ai_result_active = True - self.last_search_query = ai_marker - self.search.setText(ai_marker) - self.filtered_plugins = ordered - self.visible_plugins = [] - self.lazy_load_queue = deque() - self.results_container.removeAllViews() - if hasattr(self, "subtitle"): - total = len(self.plugins) - self.subtitle.setText(f"{len(ordered)}/{_build_plugin_count_label(total)}") - self._load_initial_batch() - except Exception as e: - logx(f"listView: on_ai_results error: {e}", False) - - show_ai_search_sheet(self.install_ui, act, on_ai_results=_on_ai_results) - except Exception as e: - logx(f"listView: ai search sheet error: {e}", False) - - ai_pill.setOnClickListener(OnClickListener(on_ai_pill_click)) + + ai_pill.setOnClickListener(OnClickListener(lambda v: _on_ai_pill_click(self, act))) self.install_ui._apply_press_scale(ai_pill) subtitle = TextView(act) @@ -447,26 +563,8 @@ def _on_ai_results(names, query): pass tag_filter_btn.addView(tag_filter_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def show_tag_filter_handler(): - try: - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - def on_apply(tags, authors, app_versions, saved): - try: - self.selected_tags = tags - self.selected_authors = authors - self.selected_app_versions = app_versions - self.selected_saved = saved - current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") - self.build_list_with_sort(self.current_sort_type, current_q) - except Exception: - pass - self._active_drawer = show_tag_drawer(act, self.content_view, self.plugins, self.selected_tags, on_apply, - self.selected_authors, self.selected_app_versions, self.selected_saved) - tag_filter_btn.setOnClickListener(OnClickListener(lambda v: show_tag_filter_handler())) + tag_filter_btn.setOnClickListener(OnClickListener(lambda v: _show_tag_filter(self, act))) self.install_ui._apply_press_scale(tag_filter_btn) tag_filter_btn_lp = FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT | Gravity.CENTER_VERTICAL) tag_filter_btn_lp.rightMargin = AndroidUtilities.dp(40) @@ -491,21 +589,8 @@ def on_apply(tags, authors, app_versions, saved): pass sort_btn.addView(sort_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def show_sort_menu_handler(): - try: - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - def on_sort_selected(sort_type): - try: - current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") - except Exception: - current_q = self.last_search_query or "" - self.build_list_with_sort(sort_type, current_q) - show_sort_menu(self.install_ui, act, self.current_sort_type, on_sort_selected) - sort_btn.setOnClickListener(OnClickListener(lambda v: show_sort_menu_handler())) + sort_btn.setOnClickListener(OnClickListener(lambda v: _open_sort_menu(self, act))) self.install_ui._apply_press_scale(sort_btn) sort_btn_lp = FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT | Gravity.CENTER_VERTICAL) header_row.addView(sort_btn, sort_btn_lp) @@ -522,6 +607,46 @@ def on_sort_selected(sort_type): except Exception: pass + return main_layout, scroll, clear_btn + + +def build_list_view(self) -> View: + logx(f"InstallUI: build_list_view enter id={id(self)} show_loading_initial={self.show_loading_initial} loading_container={getattr(self, 'loading_container', None) is not None} data_ready_before_view={getattr(self, '_data_ready_before_view', False)}", True) + # save scroll position before rebuilding view + _saved_scroll_y = 0 + try: + if hasattr(self, '_scroll_view') and self._scroll_view: + _saved_scroll_y = self._scroll_view.getScrollY() + except Exception: + pass + + act = get_last_fragment().getContext() + colors = self.install_ui._get_theme_colors() + self.main_bg_color = colors["main_bg_color"] + self.card_bg_color = colors["card_bg_color"] + self.card_pressed_color = colors["card_pressed_color"] + self.text_color = colors["text_color"] + self.secondary_text_color = colors["secondary_text_color"] + self.hint_text_color = colors["hint_text_color"] + self.cursor_color = colors["cursor_color"] + self.search_border_color = colors["search_border_color"] + self.search_stroke_width = colors["search_stroke_width"] + + self.content_view = FrameLayout(act) + self.content_view.setBackgroundColor(self.main_bg_color) + from ...ui.AchievementsActivity.service.AchivementsEngine import register_bulletin_container + register_bulletin_container(self.content_view) + chrome = None + try: + chrome = _build_chrome_kotlin(self, act) + except Exception as e: + logx(f"listView: kotlin chrome error: {e}", False) + if chrome is None: + main_layout, scroll, clear_btn = _build_chrome_python(self, act) + else: + main_layout, scroll, clear_btn = chrome + self.content_view.addView(main_layout, FrameLayout.LayoutParams(-1, -1)) + if self.show_loading_initial: content_wrapper = FrameLayout(act) content_wrapper.setLayoutParams(ScrollView.LayoutParams(-1, -2)) @@ -802,13 +927,14 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): self.results_container = LinearLayout(act) self.results_container.setOrientation(LinearLayout.VERTICAL) self.results_container.setPadding(0, 0, 0, AndroidUtilities.dp(10)) - main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) + if scroll.getParent() is None: + main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) if _saved_scroll_y > 0: _y = _saved_scroll_y run_on_ui_thread(lambda: scroll.scrollTo(0, _y)) - self.search.addTextChangedListener(SearchTextWatcherWithClear(self, clear_btn)) + self.search.addTextChangedListener(_SearchTextWatcherWithClear(self, clear_btn)) try: from ..viewUtils import applyFontToTree applyFontToTree(self.content_view) diff --git a/scripts/linux/kotlin-build.sh b/scripts/linux/kotlin-build.sh index 5e0f8a32..57cd99b2 100644 --- a/scripts/linux/kotlin-build.sh +++ b/scripts/linux/kotlin-build.sh @@ -25,6 +25,7 @@ MIN_API=26 PACKAGES=( "badges=kawaii.packetik.badges.BadgesNative" "openfile=kawaii.packetik.openfile.OpenFileNative" + "catalog=kawaii.packetik.catalog.CatalogChromeNative" ) die() { echo "error: $*" >&2; exit 1; } From e8601def7d81437a15aed53c1b346c8a4305e634 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 17:29:33 +0000 Subject: [PATCH 5/6] Move network fetches off the host plugins queue (rc.647) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit All PackIt HTTP tasks ran through client_utils.run_on_queue — the host's plugins DispatchQueue, the same one PluginsController uses to open every plugin settings screen. With github raw timing out at 10-20s (as in the reported log), one hung fetch froze the queue: no PackIt tab could open until the timeout fired, and the other catalog's data waited 20+ seconds behind it. New utils/netQueue.py provides PackIt-owned executors: - run_io — a 3-worker pool for independent fetches; used by both catalog loaders, the updates widget prefetch and the plugins-update checks (only the queue call sites whose tasks actually do HTTP were switched, verified per site); - run_serial_io — a single lane for RepositoryManager tasks, keeping their previous relative ordering around the repo cache files. The host queue now carries no PackIt network I/O, so slow GitHub can only delay the data of the screen that asked for it. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/DialogsActivity/updatesWidget.py | 3 +- packit/src/RepositoryManager.py | 9 +-- packit/src/ui/IconsListActivity/fragment.py | 5 +- packit/src/ui/PluginListActivity/fragment.py | 3 +- packit/src/ui/pluginsUpdates/fragment.py | 9 +-- packit/src/utils/netQueue.py | 58 ++++++++++++++++++++ 7 files changed, 76 insertions(+), 13 deletions(-) create mode 100644 packit/src/utils/netQueue.py diff --git a/packit/meta.yml b/packit/meta.yml index 852ce2d4..a6e455c9 100644 --- a/packit/meta.yml +++ b/packit/meta.yml @@ -1,7 +1,7 @@ name: PackIt description: "{plugin_description} [shareui/packit-source](https://github.com/shareui/packit-source)" id: shareui_packit -version: "0.0.0-rc.646" +version: "0.0.0-rc.647" author: "@packitX" app_version: ">=12.8.1" sdk_version: ">=1.4.4.6" diff --git a/packit/src/DialogsActivity/updatesWidget.py b/packit/src/DialogsActivity/updatesWidget.py index c2cab73e..e3582586 100644 --- a/packit/src/DialogsActivity/updatesWidget.py +++ b/packit/src/DialogsActivity/updatesWidget.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ..utils.netQueue import run_io from android_utils import run_on_ui_thread from android.view import Gravity from android.widget import LinearLayout, ImageView, TextView @@ -214,7 +215,7 @@ def task(): logx(f"UpdatesWidget: prefetch error: {e}", False) run_on_ui_thread(lambda: _register_pill(plugin)) - run_on_queue(task) + run_io(task) def _get_prefs(): diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index dd6e5481..2ae65263 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from .utils.netQueue import run_serial_io import os import json import requests @@ -228,7 +229,7 @@ def task(): } repos.append(newRepo) self.setRepositories(repos) - run_on_queue(task) + run_serial_io(task) else: repos = self.getRepositories() newRepo = { @@ -297,7 +298,7 @@ def task(): fragment = get_last_fragment() if fragment and hasattr(fragment, "rebuildAllItems"): fragment.rebuildAllItems() - run_on_queue(task) + run_serial_io(task) def resetRepositories(self): def task(): @@ -314,7 +315,7 @@ def task(): fragment = get_last_fragment() if fragment and hasattr(fragment, "rebuildAllItems"): fragment.rebuildAllItems() - run_on_queue(task) + run_serial_io(task) def clearAllExceptFirst(self): repos = self.getRepositories() @@ -391,4 +392,4 @@ def task(): except Exception as e: logx(f"updateAllCaches: on_complete error: {e}", False) - run_on_queue(task) \ No newline at end of file + run_serial_io(task) \ No newline at end of file diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index d2b32f47..75799298 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ...utils.netQueue import run_io import json import threading import re @@ -382,7 +383,7 @@ def load_task(): finally: self._loads_in_flight.discard(load_key) try: - run_on_queue(load_task) + run_io(load_task) except Exception: self._loads_in_flight.discard(load_key) raise @@ -458,7 +459,7 @@ def load_task(): finally: self._loads_in_flight.discard(load_key) try: - run_on_queue(load_task) + run_io(load_task) except Exception: self._loads_in_flight.discard(load_key) raise diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index fa9a56c2..4bdff9c8 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ...utils.netQueue import run_io import re import json import threading @@ -295,7 +296,7 @@ def load_task(): run_on_ui_thread(lambda: self._update_current_fragment_plugins([])) finally: self._reload_in_flight.discard(reload_key) - run_on_queue(load_task) + run_io(load_task) def _open_all_repos_plugins(self): fragment = get_last_fragment() diff --git a/packit/src/ui/pluginsUpdates/fragment.py b/packit/src/ui/pluginsUpdates/fragment.py index 827eb229..060bbc30 100644 --- a/packit/src/ui/pluginsUpdates/fragment.py +++ b/packit/src/ui/pluginsUpdates/fragment.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ...utils.netQueue import run_io import json import os import threading @@ -1015,7 +1016,7 @@ def on_done(): logx(f"pluginsUpdates: task error: {e}", False) run_on_ui_thread(lambda: (setattr(self, '_is_loading', False), self._show_empty(str(strings["updates_failed_to_check"]), "error", title=str(strings["updates_error_title"]))) if alive[0] else None) - run_on_queue(task) + run_io(task) def _hide_spinner(self): try: @@ -1511,7 +1512,7 @@ def on_ui(): except Exception as e: logx(f"pluginsUpdates: _open_plugin_profile task error: {e}", False) - run_on_queue(task) + run_io(task) except Exception as e: logx(f"pluginsUpdates: _open_plugin_profile error: {e}", False) @@ -1674,7 +1675,7 @@ def on_installed(installed_pid): logx(f"pluginsUpdates: _install_update task error: {e}", False) run_on_ui_thread(lambda: set_btn_state("idle")) - run_on_queue(task) + run_io(task) def _on_plugin_done(self): self._done_count[0] += 1 @@ -2191,7 +2192,7 @@ def on_error(error): if on_done: on_done() - run_on_queue(task) + run_io(task) def _on_update_all_click(self): if self._is_loading: diff --git a/packit/src/utils/netQueue.py b/packit/src/utils/netQueue.py new file mode 100644 index 00000000..ed26e489 --- /dev/null +++ b/packit/src/utils/netQueue.py @@ -0,0 +1,58 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# PackIt's own executors for network I/O. Historically these tasks went +# through client_utils.run_on_queue — the HOST's plugins DispatchQueue, the +# same one PluginsController uses to open any plugin settings screen. One +# slow fetch (github raw timing out at 10-20s) stalled that queue and froze +# every PackIt entry point until the timeout fired. +# +# run_io: small pool for independent fetches (catalogs, widgets, update +# checks) — parallel, unordered. +# run_serial_io: single lane for RepositoryManager tasks, preserving their +# previous relative ordering (they read/write the repo cache files). + +import threading + +from packutil import logx + +_IO_WORKERS = 3 + +_io_queue = None +_serial_queue = None +_lock = threading.Lock() + + +def _start_workers(q, count, name): + def _worker(): + while True: + fn = q.get() + try: + fn() + except Exception as e: + logx(f"netQueue: {name} task error: {e}", False) + finally: + q.task_done() + + for _ in range(count): + threading.Thread(target=_worker, daemon=True).start() + + +def run_io(task): + global _io_queue + with _lock: + if _io_queue is None: + import queue + _io_queue = queue.Queue() + _start_workers(_io_queue, _IO_WORKERS, "io") + _io_queue.put(task) + + +def run_serial_io(task): + global _serial_queue + with _lock: + if _serial_queue is None: + import queue + _serial_queue = queue.Queue() + _start_workers(_serial_queue, 1, "serial") + _serial_queue.put(task) From 81dda46f782120d938ee14ec617103ba3a865023 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 17:43:53 +0000 Subject: [PATCH 6/6] Icons catalog on the kotlin chrome; shorter entry deferral (rc.648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round two of the hybrid: CatalogChromeNative gains createIconsChrome (search pill + repo/count/sort header + results scroll, geometry matching the python builder; shared private helpers with the plugins chrome, which now reuses them too). The icons fragment follows the same restructure as listView did: search/clear/repo/sort handlers and the text watcher hoisted to module level and shared by both view sources, the original python builder kept intact as _build_chrome_python and used automatically when the dex or tags are unavailable. The one python-built view is the telegram EditTextBoldCursor dropped into the tagged slot. Entry polish: the shell->chrome deferral drops from 120ms to 30ms — with the kotlin skeleton the build fits in a frame or two, so content appears almost immediately after the open animation starts; and the plugins delegate no longer builds a search index for an empty list on the click path (json + first dlopen moved off the tap). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- .../packetik/catalog/CatalogChromeNative.kt | 168 +++-- packit/dex/catalog.dex | Bin 44904 -> 45824 bytes packit/meta.yml | 2 +- packit/src/dexLoader.py | 30 + packit/src/ui/IconsListActivity/fragment.py | 631 +++++++++++------- packit/src/ui/PluginListActivity/fragment.py | 5 +- 6 files changed, 532 insertions(+), 304 deletions(-) diff --git a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt index ee910e7d..57747690 100644 --- a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt +++ b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt @@ -66,33 +66,10 @@ object CatalogChromeNative { return btn } - @JvmStatic - fun createPluginsChrome( - ctx: Context, - mainBg: Int, - cardBg: Int, - cardPressed: Int, - textColor: Int, - accent: Int, - accentPressed: Int, - buttonText: Int, - iconClear: Int, - iconSearch: Int, - iconAi: Int, - iconFilter: Int, - iconSort: Int, - aiLabel: String, - subtitleText: String, - showSearchBtn: Boolean, - boldTypeface: Typeface? - ): LinearLayout { - val bold = boldTypeface ?: Typeface.DEFAULT_BOLD - - val main = LinearLayout(ctx) - main.orientation = LinearLayout.VERTICAL - main.setPadding(dp(ctx, 16f), 0, dp(ctx, 16f), dp(ctx, 14f)) - - // -------- search pill + private fun searchPill( + ctx: Context, cardBg: Int, accent: Int, accentPressed: Int, buttonText: Int, + textColor: Int, iconClear: Int, iconSearch: Int, showSearchBtn: Boolean + ): FrameLayout { val searchContainer = FrameLayout(ctx) searchContainer.background = rounded(cardBg, dp(ctx, 50f).toFloat(), dp(ctx, 2f), accent) @@ -102,7 +79,6 @@ object CatalogChromeNative { searchRow.orientation = LinearLayout.HORIZONTAL searchRow.gravity = Gravity.CENTER_VERTICAL - // python drops the telegram EditTextBoldCursor in here val searchSlot = FrameLayout(ctx) searchSlot.tag = "search_slot" searchRow.addView(searchSlot, LinearLayout.LayoutParams(-1, dp(ctx, 36f), 1f)) @@ -123,6 +99,127 @@ object CatalogChromeNative { searchRow.addView(searchBtn, LinearLayout.LayoutParams(dp(ctx, 52f), dp(ctx, 36f), 0f)) searchContainer.addView(searchRow, FrameLayout.LayoutParams(-1, -2)) + return searchContainer + } + + private fun resultsScroll(ctx: Context, mainBg: Int): ScrollView { + val scroll = ScrollView(ctx) + scroll.tag = "scroll" + scroll.isFillViewport = true + scroll.isVerticalScrollBarEnabled = false + scroll.setBackgroundColor(mainBg) + scroll.setFadingEdgeLength(dp(ctx, 24f)) + scroll.isVerticalFadingEdgeEnabled = true + try { + scroll.isNestedScrollingEnabled = true + } catch (t: Throwable) { + } + return scroll + } + + // icons catalog: search pill + [repo | count | sort] header + scroll. + // tags: search_slot, clear_btn, search_btn, repo_btn, subtitle, sort_btn, + // scroll + @JvmStatic + fun createIconsChrome( + ctx: Context, + mainBg: Int, + cardBg: Int, + cardPressed: Int, + textColor: Int, + accent: Int, + accentPressed: Int, + buttonText: Int, + iconClear: Int, + iconSearch: Int, + iconRepo: Int, + iconSort: Int, + subtitleText: String, + showSearchBtn: Boolean + ): LinearLayout { + val main = LinearLayout(ctx) + main.orientation = LinearLayout.VERTICAL + main.setPadding(dp(ctx, 16f), 0, dp(ctx, 16f), dp(ctx, 14f)) + + val searchContainer = searchPill( + ctx, cardBg, accent, accentPressed, buttonText, textColor, + iconClear, iconSearch, showSearchBtn + ) + val scLp = LinearLayout.LayoutParams(-1, -2) + scLp.bottomMargin = dp(ctx, 8f) + main.addView(searchContainer, scLp) + + val header = FrameLayout(ctx) + val hLp = LinearLayout.LayoutParams(-1, dp(ctx, 44f)) + hLp.topMargin = dp(ctx, 4f) + hLp.bottomMargin = dp(ctx, 12f) + main.addView(header, hLp) + + val repoBtn = iconButton( + ctx, selector(cardBg, cardPressed, dp(ctx, 16f).toFloat()), + iconRepo, textColor, dp(ctx, 8f), "repo_btn" + ) + header.addView( + repoBtn, + FrameLayout.LayoutParams(-2, -2, Gravity.LEFT or Gravity.CENTER_VERTICAL) + ) + + val subtitle = TextView(ctx) + subtitle.tag = "subtitle" + subtitle.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16f) + subtitle.text = subtitleText + subtitle.gravity = Gravity.CENTER + subtitle.setPadding(dp(ctx, 12f), dp(ctx, 7f), dp(ctx, 12f), dp(ctx, 7f)) + subtitle.isClickable = false + subtitle.isFocusable = false + subtitle.background = rounded(cardBg, dp(ctx, 16f).toFloat()) + subtitle.setTextColor(textColor) + header.addView(subtitle, FrameLayout.LayoutParams(-2, -2, Gravity.CENTER)) + + val sortBtn = iconButton( + ctx, selector(cardBg, cardPressed, dp(ctx, 16f).toFloat()), + iconSort, textColor, dp(ctx, 8f), "sort_btn" + ) + header.addView( + sortBtn, + FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT or Gravity.CENTER_VERTICAL) + ) + + main.addView(resultsScroll(ctx, mainBg), LinearLayout.LayoutParams(-1, 0, 1f)) + return main + } + + @JvmStatic + fun createPluginsChrome( + ctx: Context, + mainBg: Int, + cardBg: Int, + cardPressed: Int, + textColor: Int, + accent: Int, + accentPressed: Int, + buttonText: Int, + iconClear: Int, + iconSearch: Int, + iconAi: Int, + iconFilter: Int, + iconSort: Int, + aiLabel: String, + subtitleText: String, + showSearchBtn: Boolean, + boldTypeface: Typeface? + ): LinearLayout { + val bold = boldTypeface ?: Typeface.DEFAULT_BOLD + + val main = LinearLayout(ctx) + main.orientation = LinearLayout.VERTICAL + main.setPadding(dp(ctx, 16f), 0, dp(ctx, 16f), dp(ctx, 14f)) + + // python drops the telegram EditTextBoldCursor into the tagged slot + val searchContainer = searchPill( + ctx, cardBg, accent, accentPressed, buttonText, textColor, + iconClear, iconSearch, showSearchBtn + ) val scLp = LinearLayout.LayoutParams(-1, -2) scLp.bottomMargin = dp(ctx, 8f) main.addView(searchContainer, scLp) @@ -188,20 +285,7 @@ object CatalogChromeNative { FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT or Gravity.CENTER_VERTICAL) ) - // -------- results scroll - val scroll = ScrollView(ctx) - scroll.tag = "scroll" - scroll.isFillViewport = true - scroll.isVerticalScrollBarEnabled = false - scroll.setBackgroundColor(mainBg) - scroll.setFadingEdgeLength(dp(ctx, 24f)) - scroll.isVerticalFadingEdgeEnabled = true - try { - scroll.isNestedScrollingEnabled = true - } catch (t: Throwable) { - } - main.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1f)) - + main.addView(resultsScroll(ctx, mainBg), LinearLayout.LayoutParams(-1, 0, 1f)) return main } } diff --git a/packit/dex/catalog.dex b/packit/dex/catalog.dex index 7b1e785aa75d09df7d86ef8c341a863283ea8eb2..97f1fb3777324ed7429a15ec5640db117a0b6b93 100644 GIT binary patch delta 18489 zcma*P34B!5`MCd{JCkIxkz_&wNeG#+C5TAa0}6yqMRsHnlwAabgoI5LWdKnY*)J-} zE+`-fC@yHV1(y~TE4FH>LegQIX9PQWSn1pW#Iw}~`_rqCQN zh1Sp>I^Px*>4wq^u7T@e64byQa5pT6Rj>~3gUzrDcEdh62v5UL;28WIPQXcc7v6&p z;Lq?m1m=s>g$Oi_5?l(cp$A+IgJA@Wfg3^d?1Pi=PpEf0`C$Ou3ftff_y#iW5NQIv zU>e*DPr`9H1HlE%23!H7AbJ#YS;x&!%6rU{sDP+ zi?oGaa2GrQe}&SebOyG;83-;TAIyUnz*{cT38usSZ~%S{-U^Xo=nr$F1Uumk_yTh7 z5$OvH;2`_~>aN5m;2t;%UqOpiA_HLt?1Oh9uv(-K+yRfnYw!W2tYP+G7|epza2Vc$ zXkaZ~2v@;am6AM2wQ#De-FwN@Ep7hzl3+-4EzbcfUhCAK_m%MAp`0{9uz@SxD;AL2j~plpf~h` zAus~Q!3>xKcfg$+SpTIcD_}LOhx=hGJOumT5IhY(g`dN#@CLjEe}w-+-Fq1+42M~8 z53Gd^@Br+ApTPYHPFbsz=PziHkIjo0G@Dv<_SK%bQ2Oq+p;Bz<+ z!TUurp*}Q+)=&XMVFJvD#1ruQ=%!ehg4mQC)_$i!(Kf=FZ#5T&nO;8DQ;ci$78@I9kdr_W& z7vTiF4QJrb@J|SA$625NnnFA14#QwH+zfNzc31)HVKeN5C*f!C3wRqofo~vj2Yv&2 zJEGh$P+CEIsDQq(5>CLkknj-e3?>Scnb=5QWoaJW_T4&!aMLW`~}Xz zR}kFAprBrqpb(luE9eTt;d;0Uro&v&e5+wI9EKO+HFyU;fd7MUAn{>NM~J|sPzLRx z4-AJfFcTKQ3W(lEuoE7Gr{O5P0&l~I@E7<7(jTExxD2j<0dO5mf%&i!?uVW56dZ$h z;ZN`n2<*nEpaEPC!(j4m)_(!Y8rTj;;OB4>K81fn(jKM>T0m#G8iv7GsDfMIZdeBo zz(a5dUVszuA^a6m_j1dIj?f(j!mz!p|5%hMFazep8rTMV;3+r?C*eIf3;%}HM_E5; z0Hx3h%Ao@K!6=voYv4I}3EqU?!ddt`$UdI7h7#0+LTCz?L3bDl(_k^Ihs|&dUWH%5 z`|x}CGkgZw)YkxtpvB?GQnz{Oe?$E{q#1nNA6cjIB^}T@LOrZ{eKspr2$Q`U5~}{? zEPrdA+~|3d8xi#&IGc0D>1pW2POn?b-T=L+vp1=grwqNZvzOPhSD?4R9ss>A1=Y1% zEj39aSCgrgOBjS+=JXrTFLV0z+5VV}{Ae$t384MB0+|T2(c2=EKF!)~VC0J!ltn zO|`$OYj?E@9gALSSN%)!T;ufFbNmCDxlx_EPM9h{S46X_uFJ2}uKGfnjO)wLuXgse zt}9m(*Q+h4u61^F`eyQI*VVqul~sK|I(0^E!Vx0cuqJ53@d8IB`5o@|;8OWBAp1ipd307(*ICiBs%w62cR(S5UYalv%3Ncrm(TUbgmd%b<%VL?ayt9NVE-I{ zbvXB$c+;aXRZv>19Rqr4t*VEdeXJU}beO8ghpBpKuK$1Gm_KM0<3TTNZIVi=P(PWh zIwS1i^r5b5I%Y3zl2gm}v@LQ!=m_+zdJJ8!bf@1y**Z?wJQ)Um&`dXykm(Y%k6BLF zJsfuWbn;|7eI9yUr>n!|#Peg%b^1#50;g|4Zy3*CE6?q-@rQVUJ+%@J)zZ};;uRgK zWj|U=KVC~eiQd39cB+>BY%Tp9x~-G;`;=OGHF|v7 z=b;z5dX}IU$MmRdAX48YY(|fFC{$}!_SCW;LT^O=12n8xW2cvrzlqb;xtcls2>F{j z{bls#PCtQOVwiueNT)8Ipa)Nh%cN5j*L8+Uo&9YJU*hzS(OWqEoUJrgXBlFK5N zzgBXGA-$vCX^@?qyh1w13|*X|o0AnzUgP9VoSs`KIGLvd-^zZn-~Xsyz34xwzLM0h ztn}o(i~q;Up!6emCGRJ%v(l4sPL8+oa+zV}0omxJq5GAz`lywoB`c*FIal+i zC06qsa|1ay;v$1d-^ft@TDpQa_sV`@&%#{z32{KIvSeYZe zsV5O>(+Zqr{WRua9fVw9^24vP_o^R_4k5$W&HW)6--5q9~P8 zNkTdqtF5e*l~&G>wa7XgvsxfSZgcWs>4rTM5C0VnX30`(&yp3g8hhAvAk0&Ff6~Lw zp6wdWp-XG9=g{S^^-RfSG2g(DO9yVEf?T;(CR@%5(_DOxi_dd%zLj@K)Ww%r zIaBVoa=EN?_Ki+HU}dE|h^#NS%T-YtuFs-O$B;+E_aO7#2nuj;9eDx%rm}&|v9j=j zqyowpQhqMwik!WOkyhDwrQBiV0$G8KxcVaavDQ~i{%B08l-tNqOu@U5ja@~Jx#dr? z@k*I$<*hQ)#b+U#xO`17$XG|3xp*_`(|ViH!P}i&U}bEEut#N;H7u3&R#u8{WtDZB zW}K<&G+NOnWGORq6xrO(&?P+n{EZoCA$QvmU5b4c@s_x~4(u{nVr3bxpEcf^Pa9WK zzO5_YR-%36Sq$yu0eJ)2fqig26_k^CvXud;gT0(8A4YbPt&-$q2-%rWM*GRvnW|&5 zjXZ7Rt?YL(o#lu;Z{yd=3sycWKXvgJRYv7mdB+<1$h%gyl0RCxQxa?gTO<+L)s3hd z2e3!H8)MLISHZriXJtDZ@6JByMEUM)JxauEO4~qAOj7D`lrV zNJUrjIq)efV>5M?OYh?<=*z?VGo-708>vs(I&=M8`F_-|GuThIOFt_gbMg_{;o)HY z_|)?V3H@Eg{bi8dxBX?XlS7=0bzlHToUY{nxmK3Z&;TCjpSAKC`Gb`krH1+jP~Uds zKv^$(Kn|pXzqT@#Z=h_FH6HtakQ>PmsgTFXFoZMhmsVaO5h@tM**Xz|ceIw~ZSrjcll=8o@GG-q}!@9J?T=`+H{4kyt0(x)^a}^Ay$E(OV!gXkb zjFRT0U&m+kSFm5lE|_8Cm9o^zHL?sjQm(hoH_FZ6D5ky_`A6}v-`mP*Qe$Oj+rd%X zj#DT%iVj7$pje%|F^TG@1_SQjB&ZAMNsM zyuZyC%RdJD=hQcb{Zov6Ec-=gYOHLD+6KnD2FAJu#>P{dh;Lp7%IMFpc z(aq3J7vwYeO{V-=l~>6XR+dW_EANx8R&J4QD)lpVg*EJ# z?p98g9#;02o>ty#*Rqn&g+p!pAsJ@n4Kl{chBDU5o-)D8E;7-|X|mkP4)TDNYh{y_ zWwO`GKJq9sT8aDYvxXk>qDy$)%4za)@+$5MchUiU zM7+z&1X*omGx^5K7Ba||E0AlgY#>9eY$~I5|5tIt9fx6ttg{u)pkj5VYPsLWt6jWC z7TI`>i_es~Ha-)leU64^@;dM*R(6)-R@RYMt(+nsSlLKEL(YaIGd5`E4kT>FxASKSI>4+JX_|=jZ{3FCj;%U7F=lM{j$i)MY7n+zIMCK zrj`sEm?OSDdgnTMi`%aAcyqCt^!YMAY7d^8Nf)mFg4A`RhgbGQ07)X9BvtHz`5Ho3wb zi7Pn$^=MpyQ+?#*7gi?8zpYGhGIlhta3|*qZaQhUgDFlI?Eo7k|phr;(d^cGPFaEv_RfGie|5UU`Uwtvs*(47r;>eN@^ld-M?r z8B5>e()YP-v=1NAN2`6@zFHvnJNtg_Z8yFOtE9o~?@1r zcIh)o*BQ+a;`kcg>oVM`*4h>LXPOsX$hE`evfchpuEI+Sq9>hGX%+ii&K2nD6WZZZwbIYK z^n0-Xn9n_Nhy<>cU~0Eyt^E4QrLVe}ev?g#59o}`uo~-+HSnoRUw5&B@09l*w^5EM zuG$^&3tJRLyd@1{?t)>%F1K1n>!ztsplvlxcnV+VcWYhG+~<#I9Z%oXx*%z@N#yH?mHrE@duER? zHD#aiG@O>$DUq8CWe)q>O16=!8!n=~&!aIe=Sa#)J_?#SA*OHF)YayN%zr={aJ<58C*v*51z5-hCD!uNy-7X^G_AMDn~;(&O)?Mjyq}h$D>r3te)2-URmVc}lHb2$@ANiab>v^ncbd>+e_O{A z^Gp919q%%m{Ql+j2q%@7)Z1k0@~*htBuHdX2Py40!GxtIvdQ0BUQCWx%9|5@UYKzFBQLg_3a0MpBxPo>V$)oL4OQfy-FCSWR_G#p3ro=Ir6+^};y_jA<64G+{@m6&qFZtDdYqTuM^^cZy5*Tw{-$m-n%-|}=~@|1 zE1OI)+n|#dvXj`VqJ4I`7Y3AtL)PzoLz2u)5~dEK3J1^zyEAS z3Cr?PMdQ4!rkPZirI;zHDT(EY$@KLx-Y)2J7j`dZ+}*ls&dJ@2$=THAye>5*p*-Q# ze>G=2n{!9^MIE0ob#Q~Q*FMbq=Ez}l86#2WFP)Gdyw~I=@Vr&=wCOo1HK|dszf@8`lilXL8!8 z<0kX1+TP zYow|EE4^Bn75*2!x|nbLR=o?d+FFkc;MV254#+q6`q%d^&Te(V>W&QavcINxq4~T2 zSnuXPlKON1&%KMxm;SLU>-mu@!v*bsP`;G*r!l7=_+Rv{XZ{h-nceOOIbZa5Tv=rP zNsi_T)%e{u|MM&RXM7`#c;l*TqTfB|`hBnJZT{o0yXtWEH&X63kyEpkD^)yrNU)873jPxh<8Ex|Ym--Agd4A3{`}l&oI3Xpmy39&1J0VM|E8577 zL#oPJN?9clm@IpTRxfQ$kjcxj-}*YtiKniurZTj zJTENOBlAp2W*QzBAw5SHc8}-HiJI;4tX@v4#1c|d0`cNigG(i)O{2788tTGxOPET! zu~gY66Rj&>urxBUPAp<8jrFP`&DqM0yoltaXU4jeo)X9jF6!lefW^op;zly3W8knSTnrEA4_v9zeWPdVR!)OD1&l}8@xk>f>o&AcLwWYz7 zSWc?fii@w+iqniOouRYfapY>9_nQlK;`d`X3*vRw7cZ#F}fwS54uM!hfqSYt4@q)b^TW@nWc$Cjo@Lx>0E; zC0o4I9P!d}C6r#ovMyD}GAU?#*%sTlgEguXo8ZT#7yc@Iwz}+csR`vI$Cm$Ka*8)HoWu#-lAp6QN{lV?S7z;uGbSY z!ec8WO${Z6bwbw6&ZJY_tI7sRA}7-T zc~`R2vsh8SRIpQcFo|7YK8$zU`Vltk^g4{H!Tlt$4f7gl#0$%+NE5>NuCW7%hO-mc z1AKZ32g1RmbDqhOq{*SgOt~c~-}Y6vT9O?}Rb(K)kyAg>jlJ-ccKg=a)#0k~`N-j0 zD0Md)!Tq;hn3IOzn}cYMtt>tV9$zq5m({0On)TQmkL?x+f2sSTd*j$TX4TQ(+htvF zO8pKnlu{R`%*843k#u~fPD2T06iX<%(RUoQk9F0I3$B_Rl}=3(d2Z#q>`-YS6yoqp ziuvwt3FoTsPUb-Z-wiIb<9Rq3^IfFL35<)`0=RC5x^7ZY(x4EV)$`~|0Jjb1OVU^` z0jI6>66;IS9bQrc357%Ah47AK-l+cn_u%v&_u%h1u*5wf=D_A?JnoJ3Vh&u%0>%co zon!8rC~nnU*gQB~H)M7ob{dvRI9r{j%sNdOdm%P{_Cj!RFT2Y9(t_$670gXVY^~iv z#y*HSU@(P8Eqow3C0I4IR3ft)c}2K*M|nuf>*&GMNus}JPuI7NmCH=-JnYOm+hf($ zNlCWF>!j4t4Z4Al%5-yf57|8(2{zKF8TQN$Ual2OI9c~dF<(K9vwNns7vi2#6&Z|8 zowvpw=UUg|kor=l*C=Y@J2Acy6HTK!u}TkI*obv#Zjf|M2r+ruL>ds=puZo=#^uE;jb71PHH zhB^5-W;w&TVS_fZ|ypP50oSatO7@#9-^pWTvmqxfkNep>%~ z`!c>Qa{>`PCe6V>N?>F*Yi5rsw=5Sc*BfLiJ}?A$vem08G$O2y@J$-eGNbIdnJM9{ zs>oH`9o4N{;MP@p8ImLWxB-){NNmUMMMIV#dObZ_J^aGQ=9jJ@?N(o#a5L%pTlJeIV3RpdE-;G!QkpI}C|`h#!qTMyD%@JhX~*WWXt zQ=yP4J?C0>Z%Q~@%^+GVH|pJ;{hZNFO_6{3=sZ*G4;$S*ZyO`am(6yzJ!w;%#66Jw zWq0`pN4IUQsWqAXtuyA!wc0Lv7W1WhBR*Rh%tSuV(Yj9AW5yP&x3%$sG_wm&3^kd3 zZTZrK8X4fP8dGSt`TNG~^Jk9DP15zL${g*_8au)a^Isp^G36g@;Vsl$;`v8!%uS9( zGo{2IJHB4+#Bt+u`M~q$gd%4w=9im(%(!!03ts-bIj$gKop!|k9L;vg&lo@Xdv$y- zN|n!yXPC80x0ukv%+YukTN@J|{+oWS=~+jj|6H+QR58pH`wgUSocQF}v`D3Ya6(b6 z!-SttC@>HBsS^uQ2ivLgn7Rl3))VviOx1Vd8gtNpf8tIv!H?e9QG0Zwmi*0)MfL7v z;-~V7t$_?=+Vp!4UcB*%J$+JPiz}&VlYUxCDo@HKbsx@}mXsf|?=G;{WM0FgEAKjZ z^)hTyi-b=!zkk=HKD_jMb5h%kn_N$vGx%9Iwa!~++Z)W%=9|8>G>I{*1OD5;{-$lc z^=q&?d}h~EICkl_$(Qfd#0#%%{;DJQ*!d}^UsaI@q>YpYXW(YtCf75|{SlK}o2&fg zlbfgN0h2FNv^V|>lk@V{(@Gj|ICwi^%J}(Yx>w?*Ys~+Aa@*$Hcqh<^UqMx8-ay;o zVm{#qYKE*c!ED?LOBxSe&vC?U_4`g~Y}WcSrWDaa0xfj32j2rzx@W}Jz);Uddg=Rr zo^o%3wfT2WEn(*OO)W^6WBc>^)ZS*kpLO$9Y|)W77Zz?c?IbJ6L-B>xUSek;mfOF1 zT5(fa*+RdI_@41pz%%;hfVUCB{z37IdBr*3FPf&cO`cXlxMtdK%)k7}(?^(x{GUyq zZ+7~ZR~DFE{)oyxW{1C{ve-QAzeKph|6^qdVR}_@#&)~8sw1}xuhlYl_+6@sllrjP zRi9K#>?2Sg*rcDmDHcGm?{HEsgNs8e8D+nUTwC@ZwoI*wuJ-Mo)9VPp>X? z)(UHFU)|I^=3if3#KAGYx`o+ucz5+AQ!knBGmGE(JHQtWsWUs7I)35Ig4BNe4p+A! zxeoW7xi+CeM!$Z2E?)gcNy-JosKdXyrCY;r*2I~U#?PMAXX1>iSv{xBoKZRHaOQoJ zQgcHyCsofFJ7IQ}q|cl*YtHo9v#y;ubA~^2$AiH^Q>RZq9DL|6iP7!j-!WOwkLP2$ zS1)YB@0vvmgUcrT#$=rvZ?aB}=jFZmty#D*_~eB5Oz^}6jlZw)qZ3b?;HilkKcn%} zlYVD{Ury5a2O8fsSi^bR_*lNn*GQ;6Fx$6_{q5@{B^Yn zzrBRW+cV61^Y%P~Ww)BVQ<}c=785?V0_C&WCVYH>34gpOO6)+RZ8>Ldkl2@9y`#Zh`c2ani7+@ftyzN+-`#X_Yy2q?c4@tC32K7{N#NEU-}y# zy@X%AE!rMDwa0ArvbOIvc(ZB0E@Eo^w@ovg-D|dcSsU0p#+c6QB0JQ0^ilJWmvtJW zXA&=Gv3Ghb_Lm3DF3&7EU`*E6kCJ;on)y(p$5mU)Ppnc+W|tlY4>4ZUP>w$@CLh%r z4n1!6d0D3pQe@%d1ZNKsY*cNJ+Lmc_y9P%zII6ajs-079!xIGiH8`QcXBw#!C4Jf9@mU3Uq?N9g5c1r1kslX2cJ@P$!i49lgTW51KTc*p48xL zqGm0P9*gYJ(kC^~IStlQ?pWkF#=++_=XT9`R`V>;7WSNA-aYA`$NtP)9H8qb=@*l{ z_AQ+&vm~%6x-fYBEpu1EeE1eaG%_gfZq2vnE$)Dx45r*tjc@){tepOA)yS$q-if!- z08^wp^M>lHq zOU~$e|CZph_sEhin*zz(u_bT#kl>I8Cp6gnkufPJNJv@xfia;Y1j$FRC4Z<1%YLs> z4L;N#qZ!!~$lLV?9ymN1Zl^$}LI3t@o2UMuLy86${*k<%yyYby!EctElP_xAEY|^? z`nB=Gdwy+J1kAbE(ANZ#S29W}f8EP_{+QKPYSt%pLJa@UO!8-H4aj4Gyp4ZkL`y!= z`T0;=G4g9KdC5ywI}}KM`xBEG?f8{x*swUwGA|3Q1m%UV;iRGL{6Ero+FJks delta 17573 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

SYeE}zu7Q7s^RLJLV{5Ltdkeh*ziHSW;(A=+ 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�leWY)zcyzm zvF_KJ;uI;&CmL*ck>CWzk%v_~t-%Ei_WWEE?)fEZ{jUg4{DR<3f|2J`+x8Omn^Pn* zwJ&2lPTZW)U=8(|I)WF&PiyWonx;nc)DgTGK8s&c8|z`JMlW>^lOad-_$b>7I-v-%5=iJ{8TU zKXoy(E|C4M{@5iT8w1%F>UA#b39~oU>jDgn3K?&+7X8WZ%od7%ou<_OP8;=x&Au&= zeG(hl$K-6+(zpFyr~3{1pZ%@=cqUb914&0PChh+N!3hoC)!^_u#w5RsBf0KvV-imi zB%Q>TbY2~`?`l+o^ZFYeBM$_!kH5zS-jlI*2y__q??J8f+{`D5BOrp_2BE(wh(o6s{qUY@KaOfd|CN#d4_5dly8r+H diff --git a/packit/meta.yml b/packit/meta.yml index a6e455c9..fdbe1afe 100644 --- a/packit/meta.yml +++ b/packit/meta.yml @@ -1,7 +1,7 @@ name: PackIt description: "{plugin_description} [shareui/packit-source](https://github.com/shareui/packit-source)" id: shareui_packit -version: "0.0.0-rc.647" +version: "0.0.0-rc.648" author: "@packitX" app_version: ">=12.8.1" sdk_version: ">=1.4.4.6" diff --git a/packit/src/dexLoader.py b/packit/src/dexLoader.py index d986f8f2..fc43efe9 100644 --- a/packit/src/dexLoader.py +++ b/packit/src/dexLoader.py @@ -173,6 +173,36 @@ def _i(v): return None +def catalogIconsChromeCreate(context, main_bg, card_bg, card_pressed, text_color, + accent, accent_pressed, button_text, + icon_clear, icon_search, icon_repo, icon_sort, + subtitle_text, show_search_btn): + # icons-catalog chrome skeleton; see catalogChromeCreate above. + try: + if context is None: + from org.telegram.messenger import ApplicationLoader + context = ApplicationLoader.applicationContext + cls = _loadClass("catalog", _CATALOG_CLASS, context) + if cls is None: + return None + from java import jint + + def _i(v): + return jint(int(v)) + + return _callStatic( + cls, "createIconsChrome", + context, + _i(main_bg), _i(card_bg), _i(card_pressed), _i(text_color), + _i(accent), _i(accent_pressed), _i(button_text), + _i(icon_clear), _i(icon_search), _i(icon_repo), _i(icon_sort), + str(subtitle_text), bool(show_search_btn), + ) + except Exception as e: + logx(f"dexLoader: catalogIconsChromeCreate error: {e}", False) + return None + + def openFileCancel(view): try: cls = _loaded.get("openfile") diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 75799298..13af5a50 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -93,6 +93,338 @@ def _worker(): _preview_queue.put(task) + +def _icons_perform_search(self, act): + try: + query = self.search.getText().toString() + if query != self.last_search_query: + self.last_search_query = query + self.build_list(query) + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + + +class _IconsSearchTextWatcher(dynamic_proxy(TextWatcher)): + def __init__(self, outer, clear_btn_ref, act): + super().__init__() + self.outer = outer + self.clear_btn = clear_btn_ref + self._live_timer = None + self._act = act + + def _show_live_spinner(self): + try: + if getattr(self.outer, '_live_search_spinner', None) is None: + from org.telegram.ui.Components import CircularProgressDrawable + _size = 122 + _color = Theme.getColor(Theme.key_featuredStickers_addButton) + try: + _d = CircularProgressDrawable(float(_size), float(AndroidUtilities.dp(8)), _color) + except Exception: + _d = CircularProgressDrawable(_color) + try: + _d.size = float(_size) + _d.thickness = float(AndroidUtilities.dp(8)) + except Exception: + pass + _d.setBounds(0, 0, _size, _size) + _spinner_iv = ImageView(self._act) + _spinner_iv.setImageDrawable(_d) + _spinner_iv.setScaleType(ImageView.ScaleType.FIT_CENTER) + spinner_container = FrameLayout(self._act) + spinner_container.setLayoutParams(FrameLayout.LayoutParams(-1, -1)) + _lp = FrameLayout.LayoutParams(_size, _size, Gravity.CENTER) + spinner_container.addView(_spinner_iv, _lp) + self.outer._live_search_spinner = spinner_container + self.outer.content_view.addView(spinner_container, FrameLayout.LayoutParams(-1, -1)) + else: + self.outer._live_search_spinner.setAlpha(1.0) + self.outer._live_search_spinner.setVisibility(View.VISIBLE) + try: + self.outer.results_container.setVisibility(View.INVISIBLE) + except Exception: + pass + except Exception: + pass + + def _hide_live_spinner(self): + try: + spinner = getattr(self.outer, '_live_search_spinner', None) + if spinner is None: + return + spinner.animate().alpha(0.0).setDuration(150).withEndAction( + lambda: spinner.setVisibility(View.GONE) + ).start() + except Exception: + pass + try: + self.outer.results_container.setVisibility(View.VISIBLE) + except Exception: + pass + + def _schedule_live_search(self, query): + prev = self._live_timer + if prev is not None: + try: + prev.cancel() + except Exception: + pass + outer = self.outer + + def _do_search(): + # scoring runs on background thread after debounce + try: + if query != outer.last_search_query: + q = query.strip() + icons = outer.icons or [] + search_index = outer.search_index + sort_type = outer.current_sort_type + + if not q: + filtered = list(icons) + else: + isRussian = False + try: + from java.util import Locale + isRussian = Locale.getDefault().getLanguage() == "ru" + except Exception: + pass + fuzzy = settings.get("fuzzy_search", False) + scored = [] + for icon in icons: + s = search_mod.score(icon, q, search_index, isRussian, fuzzy) + if s[0] < 6: + scored.append((s, icon)) + scored.sort(key=lambda x: x[0]) + filtered = [icon for _, icon in scored] + + if not q: + if sort_type == "alpha_az": + filtered.sort(key=lambda i: str(i.get("name") or i.get("id") or "").lower()) + elif sort_type == "alpha_za": + filtered.sort(key=lambda i: str(i.get("name") or i.get("id") or "").lower(), reverse=True) + elif sort_type == "authors": + filtered.sort(key=lambda i: str(i.get("author") or "").lower()) + + def _ui(q=q, filtered=filtered): + try: + outer.last_search_query = q + outer.filtered_icons = filtered + outer.is_loading = True + outer.results_container.removeAllViews() + outer.visible_icons = [] + outer._card_registry = [] + outer._ticker_started = False + outer._preview_epoch += 1 + if hasattr(outer, "subtitle"): + outer.subtitle.setText(strings["icons_count"].format(len(filtered))) + if not filtered: + outer._show_empty_state() + else: + outer._load_initial_batch() + self._hide_live_spinner() + except Exception: + pass + run_on_ui_thread(_ui) + else: + run_on_ui_thread(lambda: self._hide_live_spinner()) + except Exception: + run_on_ui_thread(lambda: self._hide_live_spinner()) + + t = threading.Timer(0.3, _do_search) + self._live_timer = t + t.start() + + def afterTextChanged(self, s): + text = s.toString() + if text and len(text) > 0: + self.clear_btn.setVisibility(View.VISIBLE) + try: + self.clear_btn.animate().alpha(1.0).setDuration(200).start() + except Exception: + pass + else: + try: + self.clear_btn.animate().alpha(0.0).setDuration(200).withEndAction( + lambda: self.clear_btn.setVisibility(View.GONE)).start() + except Exception: + self.clear_btn.setVisibility(View.GONE) + try: + from elyx import settings as _s + # default must match the plugins catalog (True) + if _s.get("live_search", True): + self._show_live_spinner() + self._schedule_live_search(text) + except Exception: + pass + + def beforeTextChanged(self, s, start, count, after): + pass + def onTextChanged(self, s, start, before, count): + pass + + +def _icons_on_clear_click(self, act): + try: + from elyx import assets + from ...utils.media import playSound + playSound(assets.sounds.clear_search.path_str, "sfx_clear_search") + except Exception: + pass + try: + self.search.setText("") + self.last_search_query = "" + self.build_list("") + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + + +def _icons_on_search_btn_click(self, act): + try: + from elyx import assets + from ...utils.media import playSound + playSound(assets.sounds.search_btn.path_str, "sfx_search") + except Exception: + pass + _icons_perform_search(self, act) + + +def _icons_show_repo_menu(self, act): + try: + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + fragment = get_last_fragment() + if fragment: + fragment.finishFragment() + repos = [] + try: + for r in (self.install_ui.plugin.repoManager.getRepositories() or []): + if not r or not r.get("enabled"): + continue + name = str(r.get("name") or "").strip() + url = str(r.get("url") or "").strip() + if name and url: + repos.append(r) + except Exception: + pass + show_icon_repo_sheet(self.install_ui, repos, on_select=self._handle_repo_select) + + +def _icons_open_sort_menu(self, act): + try: + imm = act.getSystemService("input_method") + imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) + except Exception: + pass + def on_sort_selected(sort_type): + try: + current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") + except Exception: + current_q = self.last_search_query or "" + self.build_list_with_sort(sort_type, current_q) + show_icon_sort_menu(self.install_ui, act, self.current_sort_type, on_sort_selected) + + + +def _icons_editor_listener(outer, act): + EditActionListener = find_class("android.widget.TextView$OnEditorActionListener") + + class _L(dynamic_proxy(EditActionListener)): + def __init__(self): + super().__init__() + + def onEditorAction(self, v, actionId, event): + if actionId in (EditorInfo.IME_ACTION_SEARCH, EditorInfo.IME_ACTION_DONE, 6, 3): + _icons_perform_search(outer, act) + return True + return False + + return _L() + + +def _icons_build_chrome_kotlin(self, act): + # java-side chrome skeleton (kawaii.packetik.catalog.CatalogChromeNative. + # createIconsChrome); returns (main_layout, scroll, clear_btn) or None -> + # the python fallback builder runs instead + from ...dexLoader import catalogIconsChromeCreate + live_search = bool(settings.get("live_search", True)) + try: + accent = Theme.getColor(Theme.key_featuredStickers_addButton) + accent_pressed = Theme.getColor(Theme.key_featuredStickers_addButtonPressed) + button_text = Theme.getColor(Theme.key_featuredStickers_buttonText) + except Exception: + accent = Theme.getColor(Theme.key_dialogTextBlue) + accent_pressed = accent + button_text = -1 + subtitle_text = str(strings["total_plugins_unknown"]) if not self.icons else str(strings["icons_count"]).format(len(self.icons)) + main_layout = catalogIconsChromeCreate( + act, + self.main_bg_color, self.card_bg_color, self.card_pressed_color, self.text_color, + accent, accent_pressed, button_text, + self.install_ui._resolve_icon("input_clear"), + self.install_ui._resolve_icon("ic_ab_search"), + self.install_ui._resolve_icon("msg_smile_status"), + self.install_ui._resolve_icon("msg_list"), + subtitle_text, (not live_search), + ) + if main_layout is None: + return None + search_slot = main_layout.findViewWithTag("search_slot") + clear_btn = main_layout.findViewWithTag("clear_btn") + search_btn = main_layout.findViewWithTag("search_btn") + repo_btn = main_layout.findViewWithTag("repo_btn") + subtitle = main_layout.findViewWithTag("subtitle") + sort_btn = main_layout.findViewWithTag("sort_btn") + scroll = main_layout.findViewWithTag("scroll") + if None in (search_slot, clear_btn, search_btn, repo_btn, subtitle, sort_btn, scroll): + logx("icons: kotlin chrome missing tagged views, falling back", False) + return None + + self.search = EditTextBoldCursor(act) + self.search.setHint(strings["icons_search_hint"]) + self.search.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + self.search.setSingleLine(True) + self.search.setInputType(InputType.TYPE_CLASS_TEXT) + self.search.setBackgroundColor(0) + self.search.setTextColor(self.text_color) + try: + self.search.setHintTextColor(self.hint_text_color) + except Exception: + pass + try: + self.search.setCursorColor(self.cursor_color) + except Exception: + pass + try: + self.search.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(8), AndroidUtilities.dp(10), AndroidUtilities.dp(8)) + except Exception: + pass + try: + self.search.setOnEditorActionListener(_icons_editor_listener(self, act)) + except Exception as ex: + logx(f"icons: setOnEditorActionListener failed: {ex}", True) + search_slot.addView(self.search, FrameLayout.LayoutParams(-1, -1)) + + clear_btn.setOnClickListener(OnClickListener(lambda v: _icons_on_clear_click(self, act))) + self.install_ui._apply_press_scale(clear_btn) + search_btn.setOnClickListener(OnClickListener(lambda v: _icons_on_search_btn_click(self, act))) + self.install_ui._apply_press_scale(search_btn) + repo_btn.setOnClickListener(OnClickListener(lambda v: _icons_show_repo_menu(self, act))) + self.install_ui._apply_press_scale(repo_btn) + sort_btn.setOnClickListener(OnClickListener(lambda v: _icons_open_sort_menu(self, act))) + self.install_ui._apply_press_scale(sort_btn) + + self.subtitle = subtitle + logx("icons: chrome built via kotlin dex", True) + return (main_layout, scroll, clear_btn) + + class InstallIconsUI: def __init__(self, plugin): self.plugin = plugin @@ -653,29 +985,14 @@ def _deferred(): except Exception as e: logx(f"IconList: deferred _build_chrome error: {e}", False) # let the open animation start smoothly before the heavy build - run_on_ui_thread(_deferred, 120) + run_on_ui_thread(_deferred, 30) return shell - def _build_chrome(self): - act = get_last_fragment().getContext() - colors = self.install_ui._get_theme_colors() - self.main_bg_color = colors["main_bg_color"] - self.card_bg_color = colors["card_bg_color"] - self.card_pressed_color = colors["card_pressed_color"] - self.text_color = colors["text_color"] - self.secondary_text_color = colors["secondary_text_color"] - self.hint_text_color = colors["hint_text_color"] - self.cursor_color = colors["cursor_color"] - self.search_border_color = colors["search_border_color"] - self.search_stroke_width = colors["search_stroke_width"] - - self.content_view = FrameLayout(act) - self.content_view.setBackgroundColor(self.main_bg_color) - + def _build_chrome_python(self, act): + # original python chrome builder, kept as the fallback path main_layout = LinearLayout(act) main_layout.setOrientation(LinearLayout.VERTICAL) main_layout.setPadding(AndroidUtilities.dp(16), 0, AndroidUtilities.dp(16), AndroidUtilities.dp(14)) - self.content_view.addView(main_layout, FrameLayout.LayoutParams(-1, -1)) # search bar search_container = FrameLayout(act) @@ -717,189 +1034,12 @@ def _build_chrome(self): except Exception: pass - def perform_search(): - try: - query = self.search.getText().toString() - if query != self.last_search_query: - self.last_search_query = query - self.build_list(query) - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass try: - EditActionListener = find_class("android.widget.TextView$OnEditorActionListener") - class SearchEditorActionListener(dynamic_proxy(EditActionListener)): - def __init__(self, outer): - super().__init__() - self.outer = outer - def onEditorAction(self, v, actionId, event): - if actionId in (EditorInfo.IME_ACTION_SEARCH, EditorInfo.IME_ACTION_DONE, 6, 3): - perform_search() - return True - return False - self.search.setOnEditorActionListener(SearchEditorActionListener(self)) + self.search.setOnEditorActionListener(_icons_editor_listener(self, act)) except Exception as ex: logx(f"icons: setOnEditorActionListener failed: {ex}", True) - class SearchTextWatcher(dynamic_proxy(TextWatcher)): - def __init__(self, outer, clear_btn_ref): - super().__init__() - self.outer = outer - self.clear_btn = clear_btn_ref - self._live_timer = None - - def _show_live_spinner(self): - try: - if getattr(self.outer, '_live_search_spinner', None) is None: - from org.telegram.ui.Components import CircularProgressDrawable - _size = 122 - _color = Theme.getColor(Theme.key_featuredStickers_addButton) - try: - _d = CircularProgressDrawable(float(_size), float(AndroidUtilities.dp(8)), _color) - except Exception: - _d = CircularProgressDrawable(_color) - try: - _d.size = float(_size) - _d.thickness = float(AndroidUtilities.dp(8)) - except Exception: - pass - _d.setBounds(0, 0, _size, _size) - _spinner_iv = ImageView(act) - _spinner_iv.setImageDrawable(_d) - _spinner_iv.setScaleType(ImageView.ScaleType.FIT_CENTER) - spinner_container = FrameLayout(act) - spinner_container.setLayoutParams(FrameLayout.LayoutParams(-1, -1)) - _lp = FrameLayout.LayoutParams(_size, _size, Gravity.CENTER) - spinner_container.addView(_spinner_iv, _lp) - self.outer._live_search_spinner = spinner_container - self.outer.content_view.addView(spinner_container, FrameLayout.LayoutParams(-1, -1)) - else: - self.outer._live_search_spinner.setAlpha(1.0) - self.outer._live_search_spinner.setVisibility(View.VISIBLE) - try: - self.outer.results_container.setVisibility(View.INVISIBLE) - except Exception: - pass - except Exception: - pass - - def _hide_live_spinner(self): - try: - spinner = getattr(self.outer, '_live_search_spinner', None) - if spinner is None: - return - spinner.animate().alpha(0.0).setDuration(150).withEndAction( - lambda: spinner.setVisibility(View.GONE) - ).start() - except Exception: - pass - try: - self.outer.results_container.setVisibility(View.VISIBLE) - except Exception: - pass - - def _schedule_live_search(self, query): - prev = self._live_timer - if prev is not None: - try: - prev.cancel() - except Exception: - pass - outer = self.outer - - def _do_search(): - # scoring runs on background thread after debounce - try: - if query != outer.last_search_query: - q = query.strip() - icons = outer.icons or [] - search_index = outer.search_index - sort_type = outer.current_sort_type - - if not q: - filtered = list(icons) - else: - isRussian = False - try: - from java.util import Locale - isRussian = Locale.getDefault().getLanguage() == "ru" - except Exception: - pass - fuzzy = settings.get("fuzzy_search", False) - scored = [] - for icon in icons: - s = search_mod.score(icon, q, search_index, isRussian, fuzzy) - if s[0] < 6: - scored.append((s, icon)) - scored.sort(key=lambda x: x[0]) - filtered = [icon for _, icon in scored] - - if not q: - if sort_type == "alpha_az": - filtered.sort(key=lambda i: str(i.get("name") or i.get("id") or "").lower()) - elif sort_type == "alpha_za": - filtered.sort(key=lambda i: str(i.get("name") or i.get("id") or "").lower(), reverse=True) - elif sort_type == "authors": - filtered.sort(key=lambda i: str(i.get("author") or "").lower()) - - def _ui(q=q, filtered=filtered): - try: - outer.last_search_query = q - outer.filtered_icons = filtered - outer.is_loading = True - outer.results_container.removeAllViews() - outer.visible_icons = [] - outer._card_registry = [] - outer._ticker_started = False - outer._preview_epoch += 1 - if hasattr(outer, "subtitle"): - outer.subtitle.setText(strings["icons_count"].format(len(filtered))) - if not filtered: - outer._show_empty_state() - else: - outer._load_initial_batch() - self._hide_live_spinner() - except Exception: - pass - run_on_ui_thread(_ui) - else: - run_on_ui_thread(lambda: self._hide_live_spinner()) - except Exception: - run_on_ui_thread(lambda: self._hide_live_spinner()) - - t = threading.Timer(0.3, _do_search) - self._live_timer = t - t.start() - - def afterTextChanged(self, s): - text = s.toString() - if text and len(text) > 0: - self.clear_btn.setVisibility(View.VISIBLE) - try: - self.clear_btn.animate().alpha(1.0).setDuration(200).start() - except Exception: - pass - else: - try: - self.clear_btn.animate().alpha(0.0).setDuration(200).withEndAction( - lambda: self.clear_btn.setVisibility(View.GONE)).start() - except Exception: - self.clear_btn.setVisibility(View.GONE) - try: - from elyx import settings as _s - # default must match the plugins catalog (True) - if _s.get("live_search", True): - self._show_live_spinner() - self._schedule_live_search(text) - except Exception: - pass - - def beforeTextChanged(self, s, start, count, after): - pass - def onTextChanged(self, s, start, before, count): - pass search_row = LinearLayout(act) search_row.setOrientation(LinearLayout.HORIZONTAL) @@ -922,23 +1062,8 @@ def onTextChanged(self, s, start, before, count): clear_btn_icon.setScaleType(ImageView.ScaleType.CENTER) clear_btn.addView(clear_btn_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def on_clear_click(): - try: - from elyx import assets - from ...utils.media import playSound - playSound(assets.sounds.clear_search.path_str, "sfx_clear_search") - except Exception: - pass - try: - self.search.setText("") - self.last_search_query = "" - self.build_list("") - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - clear_btn.setOnClickListener(OnClickListener(lambda v: on_clear_click())) + clear_btn.setOnClickListener(OnClickListener(lambda v: _icons_on_clear_click(self, act))) self.install_ui._apply_press_scale(clear_btn) clear_btn.setVisibility(View.GONE) clear_btn.setAlpha(0.0) @@ -966,16 +1091,8 @@ def on_clear_click(): search_btn_icon.setScaleType(ImageView.ScaleType.CENTER) search_btn.addView(search_btn_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def onSearchBtnClick(v): - try: - from elyx import assets - from ...utils.media import playSound - playSound(assets.sounds.search_btn.path_str, "sfx_search") - except Exception: - pass - perform_search() - search_btn.setOnClickListener(OnClickListener(onSearchBtnClick)) + search_btn.setOnClickListener(OnClickListener(lambda v: _icons_on_search_btn_click(self, act))) self.install_ui._apply_press_scale(search_btn) try: from elyx import settings as _s @@ -1014,29 +1131,8 @@ def onSearchBtnClick(v): pass repo_btn.addView(repo_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def show_repo_menu_handler(): - try: - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - fragment = get_last_fragment() - if fragment: - fragment.finishFragment() - repos = [] - try: - for r in (self.install_ui.plugin.repoManager.getRepositories() or []): - if not r or not r.get("enabled"): - continue - name = str(r.get("name") or "").strip() - url = str(r.get("url") or "").strip() - if name and url: - repos.append(r) - except Exception: - pass - show_icon_repo_sheet(self.install_ui, repos, on_select=self._handle_repo_select) - repo_btn.setOnClickListener(OnClickListener(lambda v: show_repo_menu_handler())) + repo_btn.setOnClickListener(OnClickListener(lambda v: _icons_show_repo_menu(self, act))) self.install_ui._apply_press_scale(repo_btn) header_row.addView(repo_btn, FrameLayout.LayoutParams(-2, -2, Gravity.LEFT | Gravity.CENTER_VERTICAL)) @@ -1078,21 +1174,8 @@ def show_repo_menu_handler(): pass sort_btn.addView(sort_icon, FrameLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - def show_sort_menu_handler(): - try: - imm = act.getSystemService("input_method") - imm.hideSoftInputFromWindow(self.search.getWindowToken(), 0) - except Exception: - pass - def on_sort_selected(sort_type): - try: - current_q = self.search.getText().toString() if self.search else (self.last_search_query or "") - except Exception: - current_q = self.last_search_query or "" - self.build_list_with_sort(sort_type, current_q) - show_icon_sort_menu(self.install_ui, act, self.current_sort_type, on_sort_selected) - sort_btn.setOnClickListener(OnClickListener(lambda v: show_sort_menu_handler())) + sort_btn.setOnClickListener(OnClickListener(lambda v: _icons_open_sort_menu(self, act))) self.install_ui._apply_press_scale(sort_btn) header_row.addView(sort_btn, FrameLayout.LayoutParams(-2, -2, Gravity.RIGHT | Gravity.CENTER_VERTICAL)) @@ -1107,6 +1190,35 @@ def on_sort_selected(sort_type): except Exception: pass + return main_layout, scroll, clear_btn + + def _build_chrome(self): + act = get_last_fragment().getContext() + colors = self.install_ui._get_theme_colors() + self.main_bg_color = colors["main_bg_color"] + self.card_bg_color = colors["card_bg_color"] + self.card_pressed_color = colors["card_pressed_color"] + self.text_color = colors["text_color"] + self.secondary_text_color = colors["secondary_text_color"] + self.hint_text_color = colors["hint_text_color"] + self.cursor_color = colors["cursor_color"] + self.search_border_color = colors["search_border_color"] + self.search_stroke_width = colors["search_stroke_width"] + + self.content_view = FrameLayout(act) + self.content_view.setBackgroundColor(self.main_bg_color) + + chrome = None + try: + chrome = _icons_build_chrome_kotlin(self, act) + except Exception as e: + logx(f"icons: kotlin chrome error: {e}", False) + if chrome is None: + main_layout, scroll, clear_btn = self._build_chrome_python(act) + else: + main_layout, scroll, clear_btn = chrome + self.content_view.addView(main_layout, 0, FrameLayout.LayoutParams(-1, -1)) + self.results_container = LinearLayout(act) self.results_container.setOrientation(LinearLayout.VERTICAL) self.results_container.setPadding(0, 0, 0, AndroidUtilities.dp(10)) @@ -1227,8 +1339,9 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): except Exception: pass - main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) - self.search.addTextChangedListener(SearchTextWatcher(self, clear_btn)) + if scroll.getParent() is None: + main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) + self.search.addTextChangedListener(_IconsSearchTextWatcher(self, clear_btn, act)) try: from ..viewUtils import applyFontToTree applyFontToTree(self.content_view) diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index 4bdff9c8..d07b7dad 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -409,7 +409,8 @@ def __init__(self, install_ui, title, plugins, show_loading_initial=False, repo_ self.repo_id = repo_id self.plugins = _filter_unavailable(plugins) self.show_loading_initial = show_loading_initial - self.search_index = search_mod.build_index(self.plugins) + # skip the pointless empty-index build (json+dlopen on the click path) + self.search_index = search_mod.build_index(self.plugins) if self.plugins else None self.last_search_query = None self.filtered_plugins = [] self.visible_plugins = [] @@ -547,7 +548,7 @@ def _deferred(): except Exception as e: logx(f"InstallUI: deferred build_list_view error: {e}", False) # let the open animation start smoothly before the heavy build - run_on_ui_thread(_deferred, 120) + run_on_ui_thread(_deferred, 30) return shell def getTitle(self):