From 0fb72f7a7655a113db4943dcd84a3068c0314200 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 08:44:02 +0000 Subject: [PATCH 01/46] Format the translated message through telegram entities (0.1.1-dev.19) Bold survived the original send but not the translation: that path rebuilt the message as HTML and handed it to the SDK's parse_mode, where the description formatting was lost. Entities are what Telegram actually stores, so both paths now produce them directly. Extract the message construction into messageBuilder.build_plugin_message (text + TLRPC entities, UTF-16 offsets, description blockquote, sorted output) and use it for the inline send and the translated rebuild alike, so a translated message is formatted by exactly the same code as the original. The SDK's edit_message() can only carry entities produced by its own parse modes, so the rebuild drives the host instead: editingMessage / editingMessageEntities on the MessageObject followed by SendMessagesHelper.editMessage(), which is what the SDK does internally anyway. Falls back to a plain-text edit if that fails. The now-unused HTML serialisation (88 lines) is gone. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ChatActivity/inline/enterView.py | 146 +--------------- packit/src/ChatActivity/inline/inlineBtns.py | 154 +++-------------- .../src/ChatActivity/inline/messageBuilder.py | 162 ++++++++++++++++++ 4 files changed, 191 insertions(+), 273 deletions(-) create mode 100644 packit/src/ChatActivity/inline/messageBuilder.py diff --git a/packit/meta.yml b/packit/meta.yml index 6ee6bd0..ea408f8 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.1.1-dev.18" +version: "0.1.1-dev.19" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ChatActivity/inline/enterView.py b/packit/src/ChatActivity/inline/enterView.py index 371e8a6..3511dd7 100644 --- a/packit/src/ChatActivity/inline/enterView.py +++ b/packit/src/ChatActivity/inline/enterView.py @@ -831,145 +831,15 @@ def _packit_send_plugin_info(self, plugin_data): except Exception: show_version = show_author = show_description = show_install = True - entities = [] - message_parts = [] - current_offset = 0 - output_type = getattr(self, "_packit_output_type", None) - plugin_link = f"tg://packit?plugin={plugin_id}&repo={repo_id}" - - if output_type == "release": - # "{name} has been released" — name is a link+bold, rest is plain - name_text = name - message_parts.append(name_text) - entity_name = TLRPC.TL_messageEntityTextUrl() - entity_name.offset = current_offset - entity_name.length = _u16len(name_text) - entity_name.url = plugin_link - entities.append(entity_name) - entity_name_bold = TLRPC.TL_messageEntityBold() - entity_name_bold.offset = current_offset - entity_name_bold.length = _u16len(name_text) - entities.append(entity_name_bold) - current_offset += _u16len(name_text) - suffix = " has been released!" - message_parts.append(suffix) - current_offset += _u16len(suffix) - - elif output_type == "update": - # "{name} updated to {version}" — name is link+bold, "updated to" is bold, version is plain - name_text = name - message_parts.append(name_text) - entity_name = TLRPC.TL_messageEntityTextUrl() - entity_name.offset = current_offset - entity_name.length = _u16len(name_text) - entity_name.url = plugin_link - entities.append(entity_name) - entity_name_bold = TLRPC.TL_messageEntityBold() - entity_name_bold.offset = current_offset - entity_name_bold.length = _u16len(name_text) - entities.append(entity_name_bold) - current_offset += _u16len(name_text) - updated_text = " updated to " - message_parts.append(updated_text) - entity_upd = TLRPC.TL_messageEntityBold() - entity_upd.offset = current_offset - entity_upd.length = _u16len(updated_text) - entities.append(entity_upd) - current_offset += _u16len(updated_text) - ver_text = version if version else "?" - message_parts.append(ver_text) - current_offset += _u16len(ver_text) - - else: - # default: "{name} (v{version})" - name_text = name - message_parts.append(name_text) - entity_name = TLRPC.TL_messageEntityTextUrl() - entity_name.offset = current_offset - entity_name.length = _u16len(name_text) - entity_name.url = plugin_link - entities.append(entity_name) - entity_name_bold = TLRPC.TL_messageEntityBold() - entity_name_bold.offset = current_offset - entity_name_bold.length = _u16len(name_text) - entities.append(entity_name_bold) - current_offset += _u16len(name_text) - if show_version and version: - version_text = f" (v{version})" - message_parts.append(version_text) - current_offset += _u16len(version_text) - - message_parts.append("\n") - current_offset += 1 - - if show_author and author: - by_text = "by " - message_parts.append(by_text) - current_offset += _u16len(by_text) - author_text = author - message_parts.append(author_text) - current_offset += _u16len(author_text) - message_parts.append("\n") - current_offset += 1 - - desc_quote_start = current_offset - - if show_description and description: - from ...utils.markdown import parse as _md_parse - parsed_desc = _md_parse(description) or parse_markdown(description) - desc_text = parsed_desc.text - - for ent in parsed_desc.entities: - tl_entity = ent.to_tlrpc_object() - tl_entity.offset = current_offset + ent.offset - entities.append(tl_entity) - - message_parts.append(desc_text) - current_offset += _u16len(desc_text) - message_parts.append("\n") - current_offset += 1 - - entity_blockquote = TLRPC.TL_messageEntityBlockquote() - entity_blockquote.offset = desc_quote_start - entity_blockquote.length = current_offset - desc_quote_start - entities.append(entity_blockquote) - - if show_install: - install_text = "Install" - install_link = f"tg://packit?install&repo={repo_id}&plugin={plugin_id}" - if version: - install_link += f"&version={version}" - - message_parts.append(install_text) - entity_install = TLRPC.TL_messageEntityTextUrl() - entity_install.offset = current_offset - entity_install.length = _u16len(install_text) - entity_install.url = install_link - entities.append(entity_install) - current_offset += _u16len(install_text) - - via_sep = " via " - message_parts.append(via_sep) - current_offset += _u16len(via_sep) - - packit_text = "PackIt" - message_parts.append(packit_text) - entity_via = TLRPC.TL_messageEntityTextUrl() - entity_via.offset = current_offset - entity_via.length = _u16len(packit_text) - entity_via.url = "https://t.me/packitX" - entities.append(entity_via) - - message_text = "".join(message_parts) - - # entities must be ordered by offset, with a container (the description - # blockquote) ahead of what it wraps; we append them in build order, so - # sort before handing the message over - try: - entities.sort(key=lambda e: (int(e.offset), -int(e.length))) - except Exception as e: - logx(f"Packit send: entity sort skipped: {e}", True) + + from .messageBuilder import build_plugin_message + message_text, entities = build_plugin_message( + name, version, author, plugin_id, repo_id, description, + output_type=output_type, + show_version=show_version, show_author=show_author, + show_description=show_description, show_install=show_install, + ) try: from client_utils import send_message diff --git a/packit/src/ChatActivity/inline/inlineBtns.py b/packit/src/ChatActivity/inline/inlineBtns.py index ffefaef..5dbac9c 100644 --- a/packit/src/ChatActivity/inline/inlineBtns.py +++ b/packit/src/ChatActivity/inline/inlineBtns.py @@ -153,142 +153,25 @@ def _get_random_pending_text(): return "Translating..." -def _html_escape(s: str) -> str: - return s.replace("&", "&").replace("<", "<").replace(">", ">") - - -def _html_tags_for(name: str, tl): - # maps a TLRPC message-entity type to its (open, close) HTML tags, or - # (None, None) for entity kinds we don't render inline - if name == "TL_messageEntityBold": - return "", "" - if name == "TL_messageEntityItalic": - return "", "" - if name == "TL_messageEntityUnderline": - return "", "" - if name in ("TL_messageEntityStrike", "TL_messageEntityStrikethrough"): - return "", "" - if name == "TL_messageEntityCode": - return "", "" - if name == "TL_messageEntityPre": - return "
", "
" - if name == "TL_messageEntitySpoiler": - return "", "" - if name == "TL_messageEntityBlockquote": - return "
", "
" - if name == "TL_messageEntityTextUrl": - url = str(getattr(tl, "url", "") or "").replace('"', "%22") - return f'', "" - return None, None - - -def _entities_to_html(text: str, entities) -> str: - # serializes host markdown entities (offset/length spans) back into HTML so - # the translated description formats exactly like the original send, whose - # entities came from the same parse_markdown. Offsets are treated as python - # string indices (BMP text), matching how the initial send in enterView.py - # applies them. - spans = [] - for ent in (entities or []): - try: - tl = ent.to_tlrpc_object() - except Exception: - continue - o = int(getattr(ent, "offset", 0)) - l = int(getattr(ent, "length", 0) or getattr(tl, "length", 0) or 0) - if l <= 0: - continue - open_tag, close_tag = _html_tags_for(type(tl).__name__, tl) - if open_tag is None: - continue - spans.append((o, o + l, open_tag, close_tag)) - - open_at = {} - close_at = {} - for s, e, ot, ct in spans: - open_at.setdefault(s, []).append(ot) - close_at.setdefault(e, []).append(ct) - - out = [] - n = len(text) - for i in range(n + 1): - if i in close_at: - # close in reverse open order so nested spans unwind correctly - for ct in reversed(close_at[i]): - out.append(ct) - if i < n and i in open_at: - for ot in open_at[i]: - out.append(ot) - if i < n: - out.append(_html_escape(text[i])) - return "".join(out) - - -def _markdown_to_html(text: str) -> str: - # turns the plugin's markdown description (**bold**, `code`, links, ...) - # into HTML so parse_mode=HTML renders it instead of showing raw markers. - # Reuses the host parser so it matches the non-translated send byte for byte. - if not text: - return "" - try: - from ...utils.markdown import parse as _md_parse - parsed = _md_parse(text) - if parsed is None: - return _html_escape(text) - return _entities_to_html(parsed.text, parsed.entities) - except Exception as e: - logx(f"inlineBtns: markdown->html failed, escaping raw: {e}", False) - return _html_escape(text) - - -def _build_plugin_message_html(params, translated_desc: str) -> str: - # rebuilds the full plugin message as HTML using stored params and a translated description - # params may be a Java HashMap — always use single-arg .get() +def _build_plugin_message(params, translated_desc: str): + # rebuilds the full plugin message from stored params and the translated + # description, using the same builder as the original send so the result is + # formatted identically. params may be a Java HashMap — single-arg .get(). def _p(key): v = params.get(key) return str(v) if v is not None else "" - name = _p("packit_name") - version = _p("packit_version") - author = _p("packit_author") - plugin_id = _p("packit_plugin_id") - repo_id = _p("packit_repo_id") - output_type = _p("packit_output_type") or None - show_version = _p("packit_show_version") != "0" - show_author = _p("packit_show_author") != "0" - show_description = _p("packit_show_description") != "0" - show_install = _p("packit_show_install") != "0" - - plugin_link = f"tg://packit?plugin={plugin_id}&repo={repo_id}" - parts = [] - - if output_type == "release": - parts.append(f'{name} has been released!') - elif output_type == "update": - parts.append(f'{name} updated to {version}') - else: - header = f'{name}' - if show_version and version: - header += f" (v{version})" - parts.append(header) - - parts.append("\n") - - if show_author and author: - parts.append(f"by {author}\n") - - if show_description and translated_desc: - desc_html = _markdown_to_html(translated_desc) - parts.append(f"
{desc_html}\n
") - - if show_install: - install_link = f"tg://packit?install&repo={repo_id}&plugin={plugin_id}" - if version: - install_link += f"&version={version}" - parts.append(f'Install via PackIt') - - return "".join(parts) + from .messageBuilder import build_plugin_message + return build_plugin_message( + _p("packit_name"), _p("packit_version"), _p("packit_author"), + _p("packit_plugin_id"), _p("packit_repo_id"), translated_desc, + output_type=_p("packit_output_type") or None, + show_version=_p("packit_show_version") != "0", + show_author=_p("packit_show_author") != "0", + show_description=_p("packit_show_description") != "0", + show_install=_p("packit_show_install") != "0", + ) def _do_translate_inline(message_object): @@ -331,12 +214,15 @@ def set_pending(): translated_desc = _translate_text(raw_desc, target_lang) logx(f"inlineBtns: translation done, len={len(translated_desc)}", True) - rebuilt = _build_plugin_message_html(msg_params, translated_desc) - logx(f"inlineBtns: rebuilt message html, len={len(rebuilt)}", True) + rebuilt_text, rebuilt_entities = _build_plugin_message(msg_params, translated_desc) + logx(f"inlineBtns: rebuilt message, len={len(rebuilt_text)} entities={len(rebuilt_entities)}", True) def set_translated(): try: - edit_message(message_object, text=rebuilt, parse_mode="HTML") + from .messageBuilder import edit_message_with_entities + if not edit_message_with_entities(message_object, rebuilt_text, rebuilt_entities): + # last resort: at least put the translated text in place + edit_message(message_object, text=rebuilt_text) logx("inlineBtns: translated message set", True) except Exception as e: logx(f"inlineBtns: set_translated error: {e}", False) diff --git a/packit/src/ChatActivity/inline/messageBuilder.py b/packit/src/ChatActivity/inline/messageBuilder.py new file mode 100644 index 0000000..5b8537a --- /dev/null +++ b/packit/src/ChatActivity/inline/messageBuilder.py @@ -0,0 +1,162 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Builds the plugin message (text + TLRPC entities) shared by the inline send +# and the "translate" rebuild, so a translated message is formatted by exactly +# the same code that formats the original. +# +# The rebuild used to go through the SDK's HTML parse mode instead, which lost +# the description formatting; entities are what Telegram actually stores, so +# both paths produce them directly now. + +from packutil import logx + +try: + from org.telegram.tgnet import TLRPC +except Exception as e: + import android_utils as _au; _au.log(f"messageBuilder: import TLRPC failed: {e}") + TLRPC = None + + +def u16len(text) -> int: + # Telegram entity offsets/lengths are UTF-16 code units, not code points: + # counting with len() drifts as soon as an emoji appears earlier in the + # message and every following entity lands on the wrong range. + try: + from markdown_utils import to_utf16_len + return to_utf16_len(str(text)) + except Exception: + return len(str(text).encode("utf-16-le")) // 2 + + +def build_plugin_message(name, version, author, plugin_id, repo_id, description, + output_type=None, show_version=True, show_author=True, + show_description=True, show_install=True): + # returns (message_text, entities) — entities sorted by offset, with the + # description blockquote ahead of the entities it wraps + entities = [] + parts = [] + offset = 0 + + plugin_link = f"tg://packit?plugin={plugin_id}&repo={repo_id}" + + def add(text): + nonlocal offset + parts.append(text) + offset += u16len(text) + + def span(entity, start, text, **attrs): + entity.offset = start + entity.length = u16len(text) + for key, value in attrs.items(): + setattr(entity, key, value) + entities.append(entity) + + name_text = str(name or "") + if output_type == "release": + # "{name} has been released" — name is a bold link, the rest is plain + span(TLRPC.TL_messageEntityTextUrl(), offset, name_text, url=plugin_link) + span(TLRPC.TL_messageEntityBold(), offset, name_text) + add(name_text) + add(" has been released!") + elif output_type == "update": + # "{name} updated to {version}" — name is a bold link, "updated to" bold + span(TLRPC.TL_messageEntityTextUrl(), offset, name_text, url=plugin_link) + span(TLRPC.TL_messageEntityBold(), offset, name_text) + add(name_text) + updated_text = " updated to " + span(TLRPC.TL_messageEntityBold(), offset, updated_text) + add(updated_text) + add(str(version) if version else "?") + else: + # default: "{name} (v{version})" + span(TLRPC.TL_messageEntityTextUrl(), offset, name_text, url=plugin_link) + span(TLRPC.TL_messageEntityBold(), offset, name_text) + add(name_text) + if show_version and version: + add(f" (v{version})") + + add("\n") + + if show_author and author: + add("by ") + add(str(author)) + add("\n") + + quote_start = offset + + if show_description and description: + from ...utils.markdown import parse as md_parse + parsed = md_parse(description) + if parsed is not None: + desc_text = parsed.text + for ent in parsed.entities: + try: + tl_entity = ent.to_tlrpc_object() + tl_entity.offset = offset + ent.offset + entities.append(tl_entity) + except Exception as e: + logx(f"messageBuilder: entity convert error: {e}", False) + else: + desc_text = str(description) + + add(desc_text) + add("\n") + + quote = TLRPC.TL_messageEntityBlockquote() + quote.offset = quote_start + quote.length = offset - quote_start + entities.append(quote) + + if show_install: + install_link = f"tg://packit?install&repo={repo_id}&plugin={plugin_id}" + if version: + install_link += f"&version={version}" + install_text = "Install" + span(TLRPC.TL_messageEntityTextUrl(), offset, install_text, url=install_link) + add(install_text) + add(" via ") + packit_text = "PackIt" + span(TLRPC.TL_messageEntityTextUrl(), offset, packit_text, url="https://t.me/packitX") + add(packit_text) + + try: + entities.sort(key=lambda e: (int(e.offset), -int(e.length))) + except Exception as e: + logx(f"messageBuilder: entity sort skipped: {e}", True) + + return "".join(parts), entities + + +def edit_message_with_entities(message_object, text, entities): + # SDK edit_message() only accepts entities via its own parse modes, so it + # cannot carry ours. Drive the host directly instead: the fields it fills + # are plain MessageObject members read by SendMessagesHelper.editMessage. + try: + from java.util import ArrayList + from client_utils import get_send_messages_helper + from android_utils import run_on_ui_thread + + java_entities = None + if entities: + java_entities = ArrayList() + for entity in entities: + java_entities.add(entity) + + message_object.editingMessage = text + message_object.editingMessageEntities = java_entities + + helper = get_send_messages_helper() + + def _edit(): + try: + helper.editMessage(message_object, None, None, None, None, + None, None, False, False, None) + except Exception as e: + logx(f"messageBuilder: editMessage failed: {e}", False) + + run_on_ui_thread(_edit) + return True + except Exception as e: + logx(f"messageBuilder: edit_message_with_entities error: {e}", False) + return False From 5a070311f6efed36066fb7c2455479c2712a9a40 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 15:27:09 +0000 Subject: [PATCH 02/46] Unstick the install button after cancelling deps; send files into the topic (0.1.1-dev.20) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install button: cancelling the dependency sheet left the profile's install button spinning until the screen was reopened. The sheet does report the cancel — it calls on_cancel(False) on every path — but the profile's _finish(ok) only forwarded to an override and otherwise did nothing, and the spinner it had started before install_plugin() is stopped solely by the download callback. Stop it when the install did not go through, in both the inline and the FAB variant; the same guard already exists in the updates screen. This covers failed installs too, which were left spinning the same way. Send as file: the inline button sent the plugin with send_document(), which only takes a peer, so inside a forum the file always landed in the General topic. Resolve the topic the message lives in and send via send_message() with replyToTopMsg, exactly as the inline message send already does; outside forums nothing changes. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ChatActivity/inline/inlineBtns.py | 42 +++++++++++++++++--- packit/src/ui/PluginActivity/fragment.py | 14 +++++++ 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index ea408f8..df93409 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.1.1-dev.19" +version: "0.1.1-dev.20" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ChatActivity/inline/inlineBtns.py b/packit/src/ChatActivity/inline/inlineBtns.py index 5dbac9c..baf685c 100644 --- a/packit/src/ChatActivity/inline/inlineBtns.py +++ b/packit/src/ChatActivity/inline/inlineBtns.py @@ -285,13 +285,36 @@ def hasSeparator(self, row_idx): logx(f"inlineBtns: _rebuild_keyboard_without_send_file error: {e}", False) +def _resolve_topic_message(dialog_id): + # topic start message of the chat currently open, or None outside forums — + # sending needs it as replyToTopMsg or the message lands in General + try: + from client_utils import get_last_fragment + frag = get_last_fragment() + if frag is None or not getattr(frag, "isTopic", False): + return None + topic_id = frag.getTopicId() + if not topic_id: + return None + from org.telegram.messenger import MessageObject as MsgObj + topic = frag.getMessagesController().getTopicsController().findTopic(-dialog_id, topic_id) + if topic is None or topic.topicStartMessage is None: + return None + topic_msg = MsgObj(frag.getCurrentAccount(), topic.topicStartMessage, False, False) + topic_msg.isTopicMainMessage = True + return topic_msg + except Exception as e: + logx(f"inlineBtns: topic resolve error: {e}", False) + return None + + def _do_send_file_inline(message_object, plugin_ref): # background thread: resolves plugin link, downloads, sends as document, removes button try: import os import requests as _req from android_utils import run_on_ui_thread as _run - from client_utils import send_document, get_last_fragment + from client_utils import get_last_fragment from ui.alert import AlertDialogBuilder owner = message_object.messageOwner @@ -388,13 +411,22 @@ def dismiss_loading(): f.write(r.content) logx(f"inlineBtns: downloaded {len(r.content)} bytes to {file_path}", True) - # send document to current dialog + # send document to the current dialog — and, in a forum, to the topic + # the message lives in. send_document() only takes a peer, so the file + # always landed in the General topic; send_message() carries + # replyToTopMsg the same way the inline message send does. try: dialog_id = message_object.getDialogId() - send_document(dialog_id, file_path) - logx(f"inlineBtns: sent document to dialog_id={dialog_id}", True) + params = {"peer": dialog_id, "path": file_path} + topic_msg_obj = _resolve_topic_message(dialog_id) + if topic_msg_obj is not None: + params["replyToMsg"] = topic_msg_obj + params["replyToTopMsg"] = topic_msg_obj + from client_utils import send_message + send_message(params) + logx(f"inlineBtns: sent document to dialog_id={dialog_id} topic={topic_msg_obj is not None}", True) except Exception as e: - logx(f"inlineBtns: send_document error: {e}", False) + logx(f"inlineBtns: send document error: {e}", False) _run(dismiss_loading) _run(lambda: _show_send_file_error(strings["send_as_file_failed"])) return diff --git a/packit/src/ui/PluginActivity/fragment.py b/packit/src/ui/PluginActivity/fragment.py index dcb8661..676b64f 100644 --- a/packit/src/ui/PluginActivity/fragment.py +++ b/packit/src/ui/PluginActivity/fragment.py @@ -1065,6 +1065,13 @@ def _finish(ok): pass if on_finish_override: run_on_ui_thread(lambda: on_finish_override(ok)) + elif not ok: + # nothing was installed (deps sheet cancelled, or the + # install failed): only the download path stops the + # spinner, so without this the button span forever + run_on_ui_thread( + lambda: _set_loading(_btn, _label, _btn_text_color, _act, False) + ) def _on_downloaded(): if succ_download: @@ -1247,6 +1254,13 @@ def _finish(ok): pass if on_finish_override: run_on_ui_thread(lambda: on_finish_override(ok)) + elif not ok: + # nothing was installed (deps sheet cancelled, or the + # install failed): only the download path stops the + # spinner, so without this the FAB span forever + run_on_ui_thread( + lambda: _set_loading_fab(_btn, _label, _btn_text_color, _act, False) + ) def _on_downloaded(): if succ_download: From da893ecd86d1be97ba51b0709f1865c99936c345 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 17:48:37 +0000 Subject: [PATCH 03/46] Bind sticker icons at runtime and stand in while they load (0.1.1-dev.21) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Icons stayed blank until the plugins page was reopened. The pending list held weakrefs to the views, but a weakref there points at the chaquopy wrapper rather than the java view: the wrapper is collected as soon as the caller's local goes out of scope, while the view is still on screen. diceStickersDidLoad then fired with nothing left to bind, so the icon only appeared on the next visit — by which time the set is cached and binds instantly. Hold the views strongly instead, drop each entry the moment it binds, and cap the list so a set that never loads cannot grow it without bound. A chat sticker shows its document's svg thumb while the media downloads; before the set is loaded there is no document and so no thumb, which is why nothing was drawn underneath. Paint a neutral rounded block in the same placeholder colour until the set arrives, then hand over to the image receiver (svg thumb first, sticker once downloaded) and clear it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/utils/stickers.py | 74 +++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 15 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index df93409..ad724c0 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.1.1-dev.20" +version: "0.1.1-dev.21" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/utils/stickers.py b/packit/src/utils/stickers.py index 254f4f5..64f9a7d 100644 --- a/packit/src/utils/stickers.py +++ b/packit/src/utils/stickers.py @@ -15,16 +15,25 @@ # per-site polling with time.sleep()/postDelayed, which showed the sticker # late (fixed delay) or never (a single retry that raced the download). # -# A single global diceStickersDidLoad observer serves every pending view; views -# are held weakly, so nothing leaks and dead views drop themselves on the next -# event. All sites call load_sticker(view, "pack/index", size_dp). +# A single global diceStickersDidLoad observer serves every pending view, and a +# neutral block stands in underneath until the set arrives — the same idea as +# the placeholder a chat sticker shows while its media downloads. +# All sites call load_sticker(view, "pack/index", size_dp). from packutil import logx -import weakref -# pending binds waiting for their set to load: each is -# (weakref(view), pack, index, size_dp). Served by the single global observer. +# Views waiting for their set to load: [view, pack, index, size_dp]. Served by +# the single global diceStickersDidLoad observer. +# +# These are STRONG references on purpose. A weakref here points at the chaquopy +# wrapper, not at the java view: the wrapper dies as soon as the caller's local +# goes out of scope, even though the view is alive on screen — so the pending +# entry was dropped and the icon only appeared after leaving and re-entering the +# page (by then the set is cached and binds instantly). Entries are removed the +# moment they bind, and the list is capped so a set that never loads cannot +# grow it without bound. _pending = [] +_PENDING_CAP = 256 _global_obs = None @@ -60,6 +69,35 @@ def _resolve_set(mdc, pack): return ss +def _set_placeholder(view, size_dp): + # A chat sticker shows its document's svg thumb while the media downloads. + # Before the set is loaded we have no document and therefore no thumb, so + # paint the same kind of neutral block underneath until one arrives. + try: + import ctypes + from org.telegram.messenger import AndroidUtilities + from org.telegram.ui.ActionBar import Theme + from android.graphics.drawable import GradientDrawable + color = Theme.getColor(Theme.key_emptyListPlaceholder) + r = (color >> 16) & 0xFF + g = (color >> 8) & 0xFF + b = color & 0xFF + block = GradientDrawable() + block.setShape(GradientDrawable.RECTANGLE) + block.setCornerRadius(float(AndroidUtilities.dp(max(4, int(size_dp) // 6)))) + block.setColor(ctypes.c_int32((0x33 << 24) | (r << 16) | (g << 8) | b).value) + view.setBackground(block) + except Exception as e: + logx(f"stickers: placeholder error: {e}", False) + + +def _clear_placeholder(view): + try: + view.setBackground(None) + except Exception: + pass + + def _apply_now(view, pack, idx, size_dp) -> bool: # binds the sticker if its set is cached; returns True on success from org.telegram.messenger import MediaDataController, ImageLocation, DocumentObject @@ -82,25 +120,29 @@ def _apply_now(view, pack, idx, size_dp) -> bool: f"{size_dp}_{size_dp}", "tgs", svg, ss, ) + # from here the image receiver owns the visuals (svg thumb first, sticker + # once downloaded), so our stand-in has to go + _clear_placeholder(view) return True def _flush(name): - # re-bind every pending view whose set just loaded; prune bound/dead ones + # bind every pending view whose set just loaded; drop the ones that bound survivors = [] - for ref, pack, idx, size_dp in _pending: - view = ref() - if view is None: - continue # view gone -> drop + for entry in _pending: + view, pack, idx, size_dp = entry if name is not None and pack != name: - survivors.append((ref, pack, idx, size_dp)) + survivors.append(entry) continue try: if not _apply_now(view, pack, idx, size_dp): - survivors.append((ref, pack, idx, size_dp)) + survivors.append(entry) except Exception as e: logx(f"stickers: flush apply error: {e}", False) + bound = len(_pending) - len(survivors) _pending[:] = survivors + if bound: + logx(f"stickers: bound {bound} pending view(s) for '{name}'", True) def _ensure_observer(): @@ -140,12 +182,16 @@ def load_sticker(view, icon_str, size_dp=130): return if _apply_now(view, pack, idx, size_dp): return + # set isn't cached: show a stand-in and wait for the load notification + _set_placeholder(view, size_dp) try: from org.telegram.messenger import MediaDataController MediaDataController.getInstance(_account()).loadStickersByEmojiOrName(pack, False, True) except Exception as e: logx(f"stickers: loadStickersByEmojiOrName error: {e}", False) - _pending.append((weakref.ref(view), pack, idx, size_dp)) + _pending.append([view, pack, idx, size_dp]) + if len(_pending) > _PENDING_CAP: + del _pending[:len(_pending) - _PENDING_CAP] _ensure_observer() except Exception as e: logx(f"stickers: load_sticker error: {e}", False) From 8f38cb335bc85a5ee123306d254221393ccb932f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 18:22:14 +0000 Subject: [PATCH 04/46] Resolve sticker sets the way the host does (0.1.1-dev.22) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The export sheet lists every installed plugin at once, so it asks for ~25 sticker sets in one go — and in 0.1.0-rel that menu bound icons with a one-shot retry 2 s after opening (plus a hardcoded account 0), which loses the race against that many parallel set loads and never tries again: the icons stay empty for as long as the sheet is open. The central loader had since replaced that with loadStickersByEmojiOrName + NotificationCenter.diceStickersDidLoad, but that route has its own dead ends: it dedups by pack name through loadingDiceStickerSets and only posts the notification for sets that actually resolve, so a view whose load is swallowed or whose pack never resolves has nothing left to wake it. Resolution now copies what the host itself does for a plugin icon — PluginCell -> MediaDataController.setPlaceholderImageByIndex -> getStickerSet(TL_inputStickerSetShortName, 0, false, callback). The callback belongs to one request and always answers, whether the set comes from stickerSetsByName, from the sqlite copy or from a messages.getStickerSet fetch, and a resolved set lands in stickerSetsByName so later views for the same pack bind synchronously. Views carry a "packit_sticker__" tag and a late answer whose tag no longer matches is dropped, so a recycled row cannot show the previous row's icon — the same guard the host uses. The notification route stays as a fallback if that API is unavailable. Export sheet: the icon also has to exist. We only parse the first 5 KB of a plugin file for __icon__, so a header that sits further in leaves the row without one; missing icon, name and version now fall back to the host's own Plugin registry, which the engine fills from the plugin metadata. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ExportBottomSheet.py | 42 ++++++ packit/src/utils/stickers.py | 203 ++++++++++++++++++++++------- 3 files changed, 196 insertions(+), 51 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index ad724c0..3de937a 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.1.1-dev.21" +version: "0.1.1-dev.22" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ExportBottomSheet.py b/packit/src/ui/ExportBottomSheet.py index 0718b6f..35f59b3 100644 --- a/packit/src/ui/ExportBottomSheet.py +++ b/packit/src/ui/ExportBottomSheet.py @@ -125,6 +125,39 @@ def _readPluginMeta(filepath): return meta +def _jstr(value) -> str: + try: + return "" if value is None else str(value) + except Exception: + return "" + + +def _hostPluginsByFile() -> dict: + # The host keeps a Plugin per installed id, filled by the engine from the + # plugin's own metadata, and exposes the icon as "pack/index" (Plugin.getIcon). + # We only read the first 5 KB of the file, so a header that sits further in — + # or a plugin whose file is not python source at all — leaves us without an + # icon; the registry has it either way. Keyed by file name so it lines up + # with the directory listing below. + index = {} + try: + from com.exteragram.messenger.plugins import PluginsController + controller = PluginsController.getInstance() + for entry in controller.getPlugins().entrySet(): + try: + plugin_id = _jstr(entry.getKey()) + plugin = entry.getValue() + if plugin is None or not plugin_id: + continue + path = _jstr(controller.getPluginPath(plugin_id)) + index[os.path.basename(path) if path else f"{plugin_id}.plugin"] = plugin + except Exception: + continue + except Exception as e: + logx(f"ExportBottomSheet._hostPluginsByFile: {e}", False) + return index + + def loadPlugins(): # returns list of (filename, name, version, icon) try: @@ -140,6 +173,7 @@ def loadPlugins(): return [] result = [] + host = _hostPluginsByFile() try: for fname in sorted(os.listdir(plugins_dir)): if not fname.endswith((".py", ".plugin")): @@ -151,6 +185,14 @@ def loadPlugins(): name = meta.get("__name__") or os.path.splitext(fname)[0] version = meta.get("__version__") or "" icon = meta.get("__icon__") or "" + plugin = host.get(fname) + if plugin is not None: + if not icon: + icon = _jstr(plugin.getIcon()) + if not meta.get("__name__"): + name = _jstr(plugin.getName()) or name + if not version: + version = _jstr(plugin.getVersion()) result.append((fname, name, version, icon)) except Exception as e: logx(f"ExportBottomSheet.loadPlugins: {e}\n{traceback.format_exc()}", False) diff --git a/packit/src/utils/stickers.py b/packit/src/utils/stickers.py index 64f9a7d..d70af48 100644 --- a/packit/src/utils/stickers.py +++ b/packit/src/utils/stickers.py @@ -3,37 +3,47 @@ # Central sticker loader for the whole plugin. # -# Mirrors org.telegram.ui.Components.StickerImageView (the host's own widget for -# "show sticker N from pack "): -# * resolve the set: getStickerSetByName -> getStickerSetByEmojiOrName -# * setImage(location, filter, "tgs", svgThumb, set) — the "tgs" ext makes -# animated stickers actually animate, the SVG thumb is a placeholder while -# the media downloads, and passing the set as parentObject lets the image -# receiver resolve the document -# * when the set is not cached yet: fire loadStickersByEmojiOrName and wait -# for NotificationCenter.diceStickersDidLoad, then bind — instead of the old -# per-site polling with time.sleep()/postDelayed, which showed the sticker -# late (fixed delay) or never (a single retry that raced the download). +# Resolving the set copies what the host itself does for a plugin icon — +# PluginCell -> MediaDataController.setPlaceholderImageByIndex -> +# getStickerSet(TL_inputStickerSetShortName, 0, false, callback): +# * the callback belongs to one request and always answers, whether the set +# comes from stickerSetsByName, from the sqlite copy or from a +# messages.getStickerSet network fetch +# * a resolved set lands in stickerSetsByName, so every later view for the +# same pack binds synchronously +# * the view carries a "packit_sticker__" tag and a callback +# whose tag no longer matches is dropped, so a recycled row cannot end up +# showing the previous row's icon +# +# The older route — loadStickersByEmojiOrName + NotificationCenter. +# diceStickersDidLoad — is kept only as a fallback. It dedups by pack name +# through loadingDiceStickerSets and posts the notification just for sets that +# resolve, so a screen that opens many icons at once (the export sheet lists +# every installed plugin) could sit unbound with nothing left to wake it. +# +# Binding copies org.telegram.ui.Components.StickerImageView: +# setImage(location, filter, "tgs", svgThumb, set) — the "tgs" ext makes +# animated stickers actually animate, the svg thumb stands in while the media +# downloads, and passing the set as parentObject lets the image receiver +# resolve the document. Until the set itself arrives there is no document and +# therefore no thumb, so a neutral block stands in underneath — the same idea +# as the placeholder a chat sticker shows while its media downloads. # -# A single global diceStickersDidLoad observer serves every pending view, and a -# neutral block stands in underneath until the set arrives — the same idea as -# the placeholder a chat sticker shows while its media downloads. # All sites call load_sticker(view, "pack/index", size_dp). from packutil import logx -# Views waiting for their set to load: [view, pack, index, size_dp]. Served by -# the single global diceStickersDidLoad observer. -# -# These are STRONG references on purpose. A weakref here points at the chaquopy -# wrapper, not at the java view: the wrapper dies as soon as the caller's local -# goes out of scope, even though the view is alive on screen — so the pending -# entry was dropped and the icon only appeared after leaving and re-entering the -# page (by then the set is cached and binds instantly). Entries are removed the -# moment they bind, and the list is capped so a set that never loads cannot -# grow it without bound. +_CAP = 256 + +# Callbacks handed to getStickerSet, held until they answer. Strong refs on +# purpose: everything here that outlives the call has to be owned on the python +# side (a weakref would point at the chaquopy wrapper, which dies as soon as the +# caller's local goes out of scope even though the java object is alive). +_inflight = [] + +# Fallback route only: [view, pack, index, size_dp] waiting for +# diceStickersDidLoad, served by the single global observer below. _pending = [] -_PENDING_CAP = 256 _global_obs = None @@ -55,6 +65,20 @@ def _parse(icon_str): return None, 0 +def _tag(pack, idx) -> str: + return f"packit_sticker_{pack}_{idx}" + + +def _tag_matches(view, want) -> bool: + # the host guards setPlaceholderImageByIndex the same way, so a late answer + # for a view that has since been rebound is ignored instead of overwriting it + try: + tag = view.getTag() + except Exception: + return True + return tag is None or str(tag) == want + + def _resolve_set(mdc, pack): ss = None try: @@ -70,9 +94,6 @@ def _resolve_set(mdc, pack): def _set_placeholder(view, size_dp): - # A chat sticker shows its document's svg thumb while the media downloads. - # Before the set is loaded we have no document and therefore no thumb, so - # paint the same kind of neutral block underneath until one arrives. try: import ctypes from org.telegram.messenger import AndroidUtilities @@ -98,16 +119,23 @@ def _clear_placeholder(view): pass -def _apply_now(view, pack, idx, size_dp) -> bool: - # binds the sticker if its set is cached; returns True on success - from org.telegram.messenger import MediaDataController, ImageLocation, DocumentObject +def _bind(view, ss, idx, size_dp) -> bool: + # binds document #idx of an already resolved set + if ss is None: + return False + try: + docs = getattr(ss, "documents", None) + if docs is None or idx < 0 or docs.size() <= idx: + return False + doc = docs.get(idx) + if doc is None: + return False + except Exception: + return False + + from org.telegram.messenger import ImageLocation, DocumentObject from org.telegram.ui.ActionBar import Theme from java import jfloat - mdc = MediaDataController.getInstance(_account()) - ss = _resolve_set(mdc, pack) - if ss is None or getattr(ss, "documents", None) is None or ss.documents.size() <= idx: - return False - doc = ss.documents.get(idx) svg = None try: svg = DocumentObject.getSvgThumb(doc, Theme.key_emptyListPlaceholder, jfloat(0.2)) @@ -126,8 +154,69 @@ def _apply_now(view, pack, idx, size_dp) -> bool: return True +def _apply_now(view, pack, idx, size_dp) -> bool: + # binds the sticker if its set is already in memory; returns True on success + from org.telegram.messenger import MediaDataController + mdc = MediaDataController.getInstance(_account()) + return _bind(view, _resolve_set(mdc, pack), idx, size_dp) + + +def _request_set(view, pack, idx, size_dp) -> bool: + # the host's own plugin-icon route: one callback per view, answered from + # memory, from the cache or from the network. Returns False if the route is + # unavailable, so the caller can fall back to the notification one. + try: + from elyxcore import gen + from android_utils import run_on_ui_thread + from org.telegram.messenger import MediaDataController, Utilities + from hook_utils import find_class + + ShortName = find_class("org.telegram.tgnet.TLRPC$TL_inputStickerSetShortName") + if ShortName is None: + return False + + want = _tag(pack, idx) + holder = {} + + def _on_loaded(ss): + try: + _inflight.remove(holder.get("cb")) + except Exception: + pass + + def _apply(): + try: + if not _tag_matches(view, want): + return + if not _bind(view, ss, idx, size_dp): + logx(f"stickers: '{pack}/{idx}' unresolved (set missing or too short)", False) + except Exception as e: + logx(f"stickers: bind error for '{pack}/{idx}': {e}", False) + + run_on_ui_thread(_apply) + + cb = gen(Utilities.Callback, "run")(_on_loaded) + holder["cb"] = cb + _inflight.append(cb) + if len(_inflight) > _CAP: + del _inflight[:len(_inflight) - _CAP] + + inp = ShortName() + inp.short_name = pack + mdc = MediaDataController.getInstance(_account()) + try: + mdc.getStickerSet(inp, 0, False, cb) + except TypeError: + from java.lang import Integer as JInteger + mdc.getStickerSet(inp, JInteger(0), False, cb) + return True + except Exception as e: + logx(f"stickers: getStickerSet route unavailable ({e}), using notifications", False) + return False + + def _flush(name): - # bind every pending view whose set just loaded; drop the ones that bound + # fallback route: bind every pending view whose set just loaded survivors = [] for entry in _pending: view, pack, idx, size_dp = entry @@ -135,7 +224,7 @@ def _flush(name): survivors.append(entry) continue try: - if not _apply_now(view, pack, idx, size_dp): + if not _bind(view, _resolve_set(_mdc(), pack), idx, size_dp): survivors.append(entry) except Exception as e: logx(f"stickers: flush apply error: {e}", False) @@ -145,6 +234,11 @@ def _flush(name): logx(f"stickers: bound {bound} pending view(s) for '{name}'", True) +def _mdc(): + from org.telegram.messenger import MediaDataController + return MediaDataController.getInstance(_account()) + + def _ensure_observer(): global _global_obs if _global_obs is not None: @@ -173,26 +267,35 @@ def didReceivedNotification(self, id, acc, *args): logx(f"stickers: addObserver failed: {e}", False) +def _load_via_notification(view, pack, idx, size_dp): + try: + _mdc().loadStickersByEmojiOrName(pack, False, True) + except Exception as e: + logx(f"stickers: loadStickersByEmojiOrName error: {e}", False) + _pending.append([view, pack, idx, size_dp]) + if len(_pending) > _CAP: + del _pending[:len(_pending) - _CAP] + _ensure_observer() + + def load_sticker(view, icon_str, size_dp=130): - # binds "pack/index" into `view` (a BackupImageView). If the set is not - # cached, triggers the load and binds on diceStickersDidLoad — no polling. + # binds "pack/index" into `view` (a BackupImageView), loading the set if it + # is not in memory yet — no polling, no fixed delays. try: pack, idx = _parse(icon_str) if not pack: return + try: + view.setTag(_tag(pack, idx)) + except Exception: + pass if _apply_now(view, pack, idx, size_dp): return - # set isn't cached: show a stand-in and wait for the load notification + # set isn't loaded: show a stand-in and ask for it _set_placeholder(view, size_dp) - try: - from org.telegram.messenger import MediaDataController - MediaDataController.getInstance(_account()).loadStickersByEmojiOrName(pack, False, True) - except Exception as e: - logx(f"stickers: loadStickersByEmojiOrName error: {e}", False) - _pending.append([view, pack, idx, size_dp]) - if len(_pending) > _PENDING_CAP: - del _pending[:len(_pending) - _PENDING_CAP] - _ensure_observer() + if _request_set(view, pack, idx, size_dp): + return + _load_via_notification(view, pack, idx, size_dp) except Exception as e: logx(f"stickers: load_sticker error: {e}", False) From bbbc31b931f9259c6ffae15be76c21f17dd24760 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 20:24:27 +0000 Subject: [PATCH 05/46] Stop the icon catalog ticker from stranding previews (0.1.1-dev.23) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cards in the icon-set catalog showed their name and count with an empty icon, as if the preview had failed to download. It had not: all ten packs in the official catalog serve every one of their preview images (100/100 answer 200 and decode), and the initial bind sets the bitmap with no animation at all. The only thing that writes to that image view afterwards is the swap ticker, which crossfades a random card every two seconds: fade to alpha 0, then set the next bitmap and fade back in from withEndAction. That end action is not guaranteed to run — ViewPropertyAnimator's listener does mAnimatorOnEndMap.remove(animation) in onAnimationCancel, so an interrupted fade drops the runnable that would have restored the view. Nothing else ever touches alpha, so the preview stays invisible for good. Interruptions were easy to come by, because a list rebuild (search, sort) replaced _card_registry and cleared _ticker_started without stopping the previous ticker: it kept animating from its captured list forever, and every rebuild added another one. Two tickers picking cards at random eventually pick the same view and cancel each other's fades. The heartbeat also re-posted itself on the first card's View, and postDelayed on a detached view waits for re-attach, so the ticker could stall outright and never heal anything. Now: tickers carry a generation and stop when the registry is replaced, the heartbeat runs on the main handler instead of a view, the swap is driven by time rather than by an end action that can be dropped, and every tick restores any card left below full alpha — so an interrupted fade costs one frame instead of the rest of the session. Cards also get the neutral placeholder used elsewhere until their first preview decodes, and a pack whose previews all fail is retried once instead of staying empty until the screen is reopened. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/IconsListActivity/fragment.py | 144 ++++++++++++++++---- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 3de937a..748e90f 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.1.1-dev.22" +version: "0.1.1-dev.23" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 10eecd9..6cef5a3 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -218,6 +218,7 @@ def _ui(q=q, filtered=filtered): outer.visible_icons = [] outer._card_registry = [] outer._ticker_started = False + outer._ticker_gen += 1 outer._preview_epoch += 1 if hasattr(outer, "subtitle"): outer.subtitle.setText(strings["icons_count"].format(len(filtered))) @@ -922,6 +923,9 @@ def __init__(self, install_ui, title, icons, show_loading_initial=False, repo_id # registry of (iv, loaded_list) for the global swap ticker self._card_registry = [] self._ticker_started = False + # bumped with the registry: a ticker whose generation is stale stops + # instead of animating cards from a discarded list forever + self._ticker_gen = 0 self._live_search_spinner = None # bumped on every list rebuild; preview tasks captured with an # older epoch abort instead of downloading for discarded cards @@ -1416,6 +1420,7 @@ def build_list_with_sort(self, sort_type: str, q=None): self.lazy_load_queue.clear() self._card_registry = [] self._ticker_started = False + self._ticker_gen += 1 self._preview_epoch += 1 if not q: @@ -1586,13 +1591,55 @@ def _start_ticker_if_needed(self): import random Runnable = find_class("java.lang.Runnable") registry = self._card_registry + delegate = self + # the ticker used to re-post itself on the first card's view: a + # detached view queues the runnable until it is attached again, so + # the ticker could stall — and a card left mid-crossfade then stays + # invisible forever. The heartbeat now runs on the main handler, + # which no view can hold up, and every tick heals stranded cards. + gen = self._ticker_gen ticker_runnable = [None] + # keeps runnables handed to java alive until they fire + pending = [] # track last swapped card and bitmap to prevent consecutive repeats last_iv = [None] last_bmp = [None] + def _runnable(fn): + class _R(dynamic_proxy(Runnable)): + def __init__(self): + super().__init__() + def run(self): + try: + fn() + finally: + try: + pending.remove(self) + except Exception: + pass + r = _R() + pending.append(r) + if len(pending) > 64: + del pending[:len(pending) - 64] + return r + + def _post(fn, delay): + AndroidUtilities.runOnUIThread(_runnable(fn), delay) + + def _restore_alpha(v): + # a crossfade that gets interrupted leaves the preview at alpha + # 0 — the card then shows its name and count with an empty icon, + # exactly like a preview that never loaded + try: + if v.getAlpha() < 1.0: + v.setAlpha(1.0) + except Exception: + pass + def tick(): try: + if delegate._ticker_gen != gen or delegate._card_registry is not registry: + return # the list was rebuilt; a newer ticker owns it candidates = [(iv, bitmaps) for iv, bitmaps in registry if len(bitmaps) >= 1] if candidates: # exclude last card if other options exist @@ -1605,48 +1652,56 @@ def tick(): bmp = random.choice(other_bmps if other_bmps else bitmaps) last_iv[0] = iv last_bmp[0] = bmp + def do_swap(v=iv, b=bmp): try: - fade_out_done = make_end_action(lambda: ( - v.setImageBitmap(b), - v.animate().alpha(1.0).setDuration(200).start() - )) - v.animate().alpha(0.0).setDuration(200).withEndAction(fade_out_done).start() + # heal anything a previous interrupted fade left + # behind, including the card we are about to use + for other_iv, _ in registry: + if other_iv is not v: + _restore_alpha(other_iv) + try: + v.animate().cancel() + except Exception: + pass + v.setAlpha(1.0) + + def _swap_in(): + try: + v.setImageBitmap(b) + v.animate().alpha(1.0).setDuration(200).start() + except Exception: + pass + # the fade-in can be interrupted too, so pin + # the end state instead of trusting it + _post(lambda: _restore_alpha(v), 260) + + v.animate().alpha(0.0).setDuration(200).start() + # driven by time, not by withEndAction: a cancelled + # animation drops its end action and the swap with it + _post(_swap_in, 210) except Exception: try: v.setImageBitmap(b) + v.setAlpha(1.0) except Exception: pass + run_on_ui_thread(do_swap) - try: - if registry: - registry[0][0].postDelayed(ticker_runnable[0], 2000) - except Exception: - pass + else: + for iv, _ in registry: + _restore_alpha(iv) + AndroidUtilities.runOnUIThread(ticker_runnable[0], 2000) except Exception as ex: logx(f"icons ticker: tick error: {ex}", True) - class _TickerRunnable(dynamic_proxy(Runnable)): - def __init__(self): - super().__init__() - def run(self): - tick() - - # helper: wrap lambda as Runnable for withEndAction - def make_end_action(fn): - class _R(dynamic_proxy(Runnable)): - def __init__(self): - super().__init__() - def run(self): - fn() - return _R() - - ticker_runnable[0] = _TickerRunnable() + ticker_runnable[0] = _runnable(tick) + # _runnable drops its ref once it fires, but the heartbeat is reused + pending.append(ticker_runnable[0]) def post_start(): try: - if registry: - registry[0][0].postDelayed(ticker_runnable[0], 2000) + AndroidUtilities.runOnUIThread(ticker_runnable[0], 2000) except Exception as ex: logx(f"icons ticker: post_start error: {ex}", True) run_on_ui_thread(post_start) @@ -1680,6 +1735,19 @@ def make_item(self, icon): iv_lp = LinearLayout.LayoutParams(icon_size_px, icon_size_px) iv_lp.bottomMargin = AndroidUtilities.dp(8) inner.addView(iv, iv_lp) + # neutral block until the first preview decodes, so a card that is + # still downloading reads as loading instead of as broken + try: + _ph_color = Theme.getColor(Theme.key_emptyListPlaceholder) + _ph = GradientDrawable() + _ph.setShape(GradientDrawable.RECTANGLE) + _ph.setCornerRadius(AndroidUtilities.dp(10)) + _ph.setColor(ctypes.c_int32( + (0x33 << 24) | (_ph_color & 0xFFFFFF) + ).value) + iv.setBackground(_ph) + except Exception: + pass name_tv = TextView(act) try: @@ -1811,7 +1879,15 @@ def fetch_rest(rest): if bmp is not None: loaded.append(bmp) - def fetch_first(urls=all_urls): + def _bind_preview(b): + try: + iv.setImageBitmap(b) + iv.setBackground(None) + iv.setAlpha(1.0) + except Exception: + pass + + def fetch_first(urls=all_urls, attempt=0): # phase 1: get any single preview on screen ASAP for i, url in enumerate(urls): if delegate._preview_epoch != epoch: @@ -1820,11 +1896,19 @@ def fetch_first(urls=all_urls): if bmp is not None: loaded.append(bmp) if delegate._preview_epoch == epoch: - run_on_ui_thread(lambda b=bmp: iv.setImageBitmap(b)) + run_on_ui_thread(lambda b=bmp: _bind_preview(b)) rest = urls[i + 1:] if rest: _preview_pool_submit(lambda: fetch_rest(rest)) return + # every preview of this pack failed — a card used to stay empty + # for the rest of the session over one network blip, with no + # retry and nothing to tell it apart from a loaded card + if attempt == 0 and urls and delegate._preview_epoch == epoch: + logx(f"icons: no preview loaded for '{_display_name}', retrying", False) + threading.Timer( + 3.0, lambda: _preview_pool_submit(lambda: fetch_first(urls, 1)) + ).start() _preview_pool_submit(fetch_first) From 6e8feda46159e874bd01ab9ff500f5df8a7034b3 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 6 Aug 2026 20:41:55 +0000 Subject: [PATCH 06/46] Stop cropping the round catalog buttons' icons (0.1.1-dev.24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The smiley on the repo button in the icon catalog came out with its rim shaved flat on all four sides. Measuring the screenshot: the white artwork forms a 20dp square with straight edges where the circle should curve, i.e. it is drawn larger than its slot and clipped to it. CatalogChromeNative.iconButton put the icon in a 20dp view with ScaleType.CENTER, which draws a drawable at its intrinsic size and lets the view clip whatever does not fit. The host's action icons are 24dp (msg_smile_status is a 72x72 asset at xxhdpi), so 2dp came off every side. msg_list survives that — its strokes sit ~3dp inside the canvas — but the smiley draws its circle 1dp from the edge, so the crop ate the rim and only that button looked broken. CENTER_INSIDE keeps the drawable centered and only scales it down when it does not fit, so every icon renders whole at the same button size. All the buttons built by iconButton are covered: search clear and submit, the repo and sort buttons in the icon catalog, tags and sort in the plugin catalog. Dex rebuilt from all four kotlin sources (same 22 classes as before). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- .../packetik/catalog/CatalogChromeNative.kt | 7 ++++++- packit/dex/badges.dex | Bin 55668 -> 55720 bytes packit/dex/catalog.dex | Bin 55668 -> 55720 bytes packit/dex/openfile.dex | Bin 55668 -> 55720 bytes packit/dex/sfx.dex | Bin 55668 -> 55720 bytes packit/meta.yml | 2 +- 6 files changed, 7 insertions(+), 2 deletions(-) diff --git a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt index 25e22c6..6cc0845 100644 --- a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt +++ b/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt @@ -86,7 +86,12 @@ object CatalogChromeNative { val icon = ImageView(ctx) if (iconRes != 0) icon.setImageResource(iconRes) icon.setColorFilter(iconTint) - icon.scaleType = ImageView.ScaleType.CENTER + // CENTER draws the drawable at its intrinsic size and clips it to the + // view: the host's action icons are 24dp and this slot is 20dp, so 2dp + // was shaved off every side. msg_list survives it (its strokes sit well + // inside the canvas) but msg_smile_status draws its circle 1dp from the + // edge, and the rim came out flattened on all four sides. + icon.scaleType = ImageView.ScaleType.CENTER_INSIDE btn.addView(icon, FrameLayout.LayoutParams(dp(ctx, 20f), dp(ctx, 20f), Gravity.CENTER)) return btn } diff --git a/packit/dex/badges.dex b/packit/dex/badges.dex index 24de1cc5db95a4e5766a3386bb81f004796ff27a..27c77d65be3c2c11ddaa75e30709ae4e983a714f 100644 GIT binary patch delta 17808 zcmaK!30zgx+K1QL=OCb<6e88eXNT+JVnKx zTBAnKD4)`-wYvRy%Z;TEd!8BiTg$t5yg28HOK#7uGA^`5-vfwt5ggmK^GVZ4?r$# zg=6ps)Gtt~7fdTqJ~am;3zooYcov?6&9D>pzyT7{R;Z^t)Ecmx*0MmPz-K*cqzf7dliJql}JKYRhdfae*dYC;R> z3!`8*6u<`f7`}nupz^ayHH7wXFN}o@cplz`FX5lyVdK_=?l2bS6#CdG7~A1(I0KiV z>RKE^7Z?PS;Az+bW$-O{pHr$4bc4~53D3Y*D1$R_6)HW?@<2Q23!`8TEQjZP1nkK>_T8ui;-1{{ojC zjE5&-ANYPEh<%Z%gW<3e-i51>yorWj4IGDv%}RBK>F_kX4ws?!7S0A(0SDoCsJfNe zfqP*#tc5q>Q}`3=Z==pO);}F1AGX0!_!TN{XUd=#q(dPbg5RLdOKe7X9A1W#pm*R9 z+Ce{f09<$q*1;$6HB>2P!$K`+25pO3|85vPp)U-C(J&EaLKfu13Rn&6U@MfsoA4fd z03X9K_zX_NKj1w40$1R7&^whX2N6&iVxhK=AQ6(FC3Jxv&<_T|{V)th!NV{fros%! zfLzFjW$+}dg>_H_o1qjwg$oe!GP@tT!6=vwzAS( z89WV};0+(aVK@s{A#^Vl;31d{Ghr?)hgGl+UWT{e12_Sv;aj)_&OXk6h=m5w1lq#g zFcxy)1vmsJ!S@5fC2-zSDiW$gZAgLE&FVKVHRYA3E8k7 zJ_P*^2MqLqHSiu>g&JjSIv59&VS1U5%LF4ImcdGR1~x$nyaD^+5FCTE@H;pMnJuUc zHK0DUfX*-&#y~pEhFo|8*28w#18>7|_}WMC4V(w{E}ID|Lv=`ow$Kgw!7vyLQy>H8 z!ZKI`8{rjr7e0sY;WC80$E-pp2u-he~!DSQLJK+GYQ1nz(V z@DNOgC9noILNUA!$KYph-sd(B)u9ojLN6E!V_*u*eV_Hu$9M|X!xnf2_QOYT8ZLtV zfc1oS&;tg*NEicCU^dKyCGb2H!)`bTN8t?o0GA=;L!JR35gLEU`nSR80zF{>42LoB zD9nS^Py%1WcW?>*fbxeqmZ1(bhPKcR`ocgM24i406u<`91MkCexCY+lAF+|3D%6E0 z&>B*q6U4DCszYt454S?A;)9WYI?=T>$;lronHS{K5X6@9jjX*I`!;K@!)|Qt?bs>SF2PQ+cIjf)GucMUf(jkRZer6<1XVbL z9VmZ3DE%UKE6Rs}d|HFp(z!O)zJ}e_+TO?68^&fmqlnyYGZL`dW0wQ@h#hJy?!nfU z&ZS|8fpoeP8V=H!lrInR=>buYXnn>=BA+fGb_(g8XEcxEhp`7*dy*|LYcHSfP!nd8-rw4J*n_RT%9fWUm-ba(kj+v;#!zb?#_ozO>wFr! zH!ACV0lSa2Z(iabaT0O)$mkmz%RfW1RIMq~6Kx6|Yf&(~Y=%I)Kc~B+@&gcfpJ$jNf#Ch^(I^ zrh#l0aVj>u)U3VOnN*g2Aa)-1{nlP;8|z10KH^hs@hKe;do^Xmr`YST#YaDx|IOBs zWbDM22E;DK9$@V>)P2E_R?y3ibmwy&bmH6S@bb55x|1XcTs!Lyuq&qwIY!6b@mEAA~+{uMrJ@h;z(@2BeR>0AMr5~Hd9_^PVB(4id|y;JdAC|FO7|XzzD`;m)b%T zu;n9V#Fi;CDbX4l5gM6fOzDEhla4I{0I>^qKH4^)% zZ7dsGKGKN;ARQH_Z-Y3MyGa?gd{V7FgQ@OdZ7I{y+Nbc@$=Y<cKTTZ}xtQ|&~zQ$(#qlg45-x6fk4YCud z94K>JP+r1!M;VeCLBxWaJuqkh zvTp)qM_M}4f*GeHf@RO}S z)DEKu)z?-BGvW+7(!jJgR&_*u@+S>u+MZ>qZ1pV_GI>J(&S*LHy-|-kZ`7-Pu==CX z_tZ~TFIfGj(edhMqm9)sXcm5D{e^UNj@3m* zN2nz>-jee9{K?Gc`zY9yiiP}1!dm`BpX1N3BbD4Eo}+>GI9R8iH;>Zm)q0b^o`>s? zN#DjMUP!rZjIbZ^mx$NJ&o2JN&n})YT3BsPIf=JH-SaiWd=hDE6WgKX_;Tdnq#R!* zn;MN(t=a6b8=dl)>2@mpLb9j=An zuZh=ID^)1@byN-2nRs10;CjrQPo}yadsei*T4J=pb@e-HfTJI&c&kmnm6?&H5xrz| zwJJvg4RI)b8ggxjHnQa#k-n62jVS-KRe7-8QhjIR-=mGK zqsEM&iHYZ`->o`0O1AmQwxdmKya`vVJX^hQOZDXR~wAB zQyY!8_p$F^qe7~6kjmGJHaP5{-ca|Tozy<{1P(gUlw4k&X-f1?oV`f;oy_AS)W1`; zRJBO&qTW)&h>MOe+S-)wO4C;;-<9_ue)V7GzOM8wgo0hw7plC`i7LYAWEE-jOI6XP zSGMu`MjueO8f~N689k^TFuG4Yh<4{@E*-s#FZZH%+ZpI#wI>}P!co8(mTC&5ai~tC zVw&n;;=MQlCehOt1gAUoxQ_&!u>@{d?Oq-s!qkQ%0Hql0Y+ z2CKiBb7io4*yo4Pq_zKjCn@4sFUb02I6aMjAJU<=nzw2 z7+1lk#D_EK;$S!qZZ`3)rs8m$KY~|GLrEWrqeP?rJt1-NJ5u@mj%4banfL%(-tT{u z+V4}t$r#NlXQK}>WyfjoZ*~NKQ&Us{=?`-st7;TQv1+Hxcu&?;3Kxe zBaBSSkGBntmveyqG2YJ9cw1iLd#N|xmY+a+H{uhxFmup}TyHY76MeSAMBBhbTOpk% zwkQgwv&J>h$-Hqc3rd8A@ZlI$4!T(a}~sx_yZ~xonhN%r#tLKeot0P7`sWV24)jy2ht}YsVKwZ*Y{~1j3uO^|dk`pe2DZj;Nw(4QEr`0s8 zy^Ox7?lrnk4YcWlj6R^I8uh8^MtiHrjP_7-jUH9ojm}js`HYdRb{l<3ymD)XQ*06lT~e_Ji}YP&1wsyDXNW) zcQC3|N27bxX`?Txe;VDZ{$;enE_Kxyxhljxd275+_Hqs#l8MYwPpS<}Tn=9&4;bC4 z-ZuK0ddKLqs?6wcbCKonHFZ4Ra``1*>gU??alVbuxA8o++vMlj_yQj6rThZ*T#QvI7c`7Fk7*03_884AB)raOnmTB- zr+U|D1NDK?N#@GR=N05-;)~QP=Ek+y>f`qCTEZ=?Astx4E$?O;Ty9Te*QUG5=g%mx z87nx?zQW-O;*+Vkf`5+Knu;rJ`bwMrtW7VZqX8-SJaskLN-T zZaJn4*~s%nePq0B62@Egs8>up+nn>_P;RMfZ9}5c&|0EJ%AZ_8uaTE5--N9ex;uH_bzz?835$5kt%pIY@lz&>YZ zLR4nr`RnT61M6)5`XH5Z8*I4^c8_dO`_)Gd$Mi;YJ_^79&WO_m2ZM? zk-n90g8Pk*H2K@qo2CQX>{@TLYrGAAGScnbGv!&}CEJmg)Cn_?m()q4JNT(RPriol zps8&}w_VrmHvW>)QR?%=7xui2^zGqi&y0@~EZn^|p+7W19Eg!(UAeZ@Y) z@1kSoSQ0~~sD$tLHK|a-*NjwjuX^7+fbLZvm~WKltAz(*huyTG*g1E6Q0$7k zIJsK+K*_L=-L1)0uSakG*!>_Vn)tE%V{+BVK;hQB2>jWNPN{sop@D8vO0}_p{IN&; zoykOOFaNERTWo&Tf64#a=Fj~v`QD@M)|4BajXmmqkW%$}U%I11jZmh_7cT1CM4Fh>+ zsqCEOAGcA71KU&A*+je#~i=2mQ4RX29)HI2VM+?H-y(`t7GYDjJQ6hfdo zlY)lZ;h5yg*>EF&Mo|7TcT3Y7z3t|{9~2wr{ty(K>_#@bQ8Lp_3W}|C)0$PIpQD?_ z-x}!I+T*5SUWLg^V*gmPgIa#Y-4f*CnEQTE?6mtsQ0$5u*<8HWYkp%E?r_tZ-{?(u zcf2W)(>$JX>rHqdsPq{3hoI6k-N+U<>MnJYf?`j(X)Vl1THI)Box9kitq)4u>b`H% z&bE*iLt08$ujP$8ueoV0jUSVfW5V?&JYd4JCJbq1>Y1>M2}heS$As%mc))~bO&HSp zMhgesq}De^@r9cf6ywToEv@IYHuGV^113D%dV#*xo!F-RxW=qWoZ9B3Cw!~Ds(*(H zyrhbWZWYDN3!9(ub)q-Yn^+}AjnKEK{&z<3yqMxiPdKORVnurDM#*mr5h}eynC|E# zhK=BDT%3;aq^FeEQQnFwgxmzB`HOOs+tzSXZm+1fxv96;)5qMbww2u(x2IHIuG3Q@ z^e`u)TpM0mRmAU~?(4TFRldWC@ziPdq47PeX@nE!iFU8vUPE_rW7~EN^3u89mw)ll zMd~)Ubvselo9}+xwtBTBCpa}d>L1!sao(HU>g^iDS2Uy1DniR>hQ%aSh%A@rROEIh zW7_NvZzruj->y+~b*Iive&0^#2#@&wvR$1T{(0$)zhuTAp__UmJZ<<^%!s@L-1zq0 zBJc92hew3RdAGPz+9#!sbfl!?iQpT#De39-Z@vTd>30=>i%t7Sc%m!T zlODC08II7h8iD>hmE&|px7Hms^e#8`j#PT?yQ7)y!C@Vt zWviH2MlxSFpMYNgIz}J#cp!fZdGPG6B0IY08L1EkuOnkVG}iRH^1wnI>pu9 z?rU5tPaKD;h|t=4u$mNA#5Be^A4(}M2&t7ttuU-jSZ;2&#+7?BfDa`z>adyfIA?+T zPPZD>+Uqz^13e+-D6jt}M13TA47ZwlwcD;Lf7j{iC%Inqgs2O))_d+d-MduY#D1NS z;!r0-x7J}aUf{;xm8?^W@42gzZeHtk>0h{t2*1%k>C0LbSH6nr70;pc@+IQ_UanQ{ z^1J(2Kdq~B2~OzXIGsmV2(9h)56;WV-R9~ZEp(FGsz-y!yFF6E9bF+juEJJ#T#vW( z2sgQBBkj6Fdj6zGyFJos=v;S9TCzUsKAG0um&l%}&C_N@)fWW3WtrKkLj7by{HQbH7aIMEZS6066lwbyIe>pg zBYfUTQT4?LnUv5-_Y+O2towN*LdJyDR&gPTdW5RY#c_@X%JWYhZ(LQ6nrqR2RdJOu zL$Z@KWj7OYD@mxsZDUeGlI~1n^=O0JikHJCl3< zLsRgb`<_2J@~L%;G^@7&td@#4|Ln&=8s%pG15s=I54r+2)WY5tbI zGbE-1GvHwn_Z11Bat z&Ux19Kc&A)PkBtu^~ix*==7iJkI&;a8|Qt>>Hnxdo*feBT@w_G@WzF#cKT2A=jN1) z2#G8Aw3{=okDlosAGcDsDW3R9tkx&p1>*>cS&b9e8gBuf4?sfTV}+%|o* z4vlk61$X1*M)7Z&Vasx~x9C=-sg3j) zx9!vx`k~_Vsa3Rf!JR_2FfL>(`CU{nF4jM~IN5()i}PDenR5Ju6Q?&gF)=ccwY(AD z5qgl4rwPh9OWfhp8tJdx#nWPW+SxcQxy@he$Emu;Q|SNnlkJV?~0n@wb zO75EJgPQ*JChNbU#)bKBVp+`Si~O6hpj&O?jHWEakQpOQ<#H;b+>NtDDyOTsP=Dog zQS@~jl67P~Latf6Dw4`(J5{yB4` z{@fiotC@~1UO8)sE?-wqO4!Ba(ZF%HXI2gON262&_nwSu!I`}?s_MG#n{(oAVlCA` zyZ%IXb~k;>{cLtTp0Cax_aC+XBZ`xa851dAZB7ruk#mOl-4cE|rCWo;s(fhc(5md(GTv)M zgvN!XxXrTK>-KJXR#t@^Ss!|?J;-Pzmoa`?2{k`nx2`A<>qEAofRM)S%yK?T*dtIHIf$htk zNbXO$@p^A@rTN`-ty|5|xWrg#*}6AbOXAXGO!4%*5J!)5GZqfgwcO(i7gujhkLBAr z-&FK(jOH9?`WW8)aGkim{3=N;ujdEy#*)|SFR9~5ZRYOHufkP-JU`Wrr5vd*xV07y z4~p+}7cc6&`C4#|q{oXkFa* z<<7VGGACcs(W@sbM?0}GI_6iMmFYb<`Ld2aJXuGVOy-*x-OtSQ7EJk<_HLdc@v9PF zH}#tK?w%^~-z8o;?GNoe!K)#y|CD&%%uL5yI8)+T4)HUy=Q`eNvn%nNeDydTtKXZUeKIP(m29xrpGbD(z?MAA@1kt5pzoSSP`L3C>86eL$Y|)ZJ$PeC{krSoN;f3lg@Qu#AvNJB_f7*1gAk?UrEm0jB=q zAqn2|5gx*fJ$;a=TEwptTI;lB3E#>zu9Inn3%O@zc=%M!kq#QjI~c_(@uKN&h& z-?J=1FH87A(zB21AD!saCf(C#X@Xvw@RO5CyGQi}NAEw1i)#`rrPS#6BaQ%PyN4KX+LddiEw{ zUSZUEzY?q_*qd-d%%v33+ka*K_aD-^|~nh6G1R>{BY%c?lN%i(os!-h}PHW1jhg;OJF?a|C^^iB)us_VsBg zx%p2L%dTNvlwdXO>74|76Anr3i&CaQ>g*)gn{b}QJ_VAqpNe{sI4_dm&>w6_N8KOG zkrsPMx_*&<(e<73FDkVDK<4fIJm*7?&dzrDU0bVRv4^GHp?Qux?GC5jM-ne3Tk6U0 zOIjWG#9qk7@~BTev5OWsY?3p1j*czl2RTQ3MslHjF0M24oiA{`knV4mpeWx||H>14 zib_-;9rt(fUbKYDW%-Uz$1Yt=kw|sc6TY9s@RH>OMv6-LrIptdQc51fTNCWc!KT+6CkUbyqpQvGrpPx?2RskQ;4}CJiu)pI&=8u#V3-eE;XUy3 zL~6p#c@mPo7{lQnm<%&vKKQT#9)$wf2}j@^cppB8a!W3>+m@gS;4f! zjc^aFfbH-pcq>Ixp#}7TiLe&-!Us_15s~X3Vf{y8`0y0$g*V}A2tLXtf(FnPhQK{g z0MEh!_ym4|xW`1QLNn+F@4FDt0*ZfYC4=mO&xB4j;l#5WAXzKm+IwgJ2vy2%AF$d%#;Gk`5!` zarg+Xeq5w4%z)?NOGtl0WCA<~=b`eGOg}sbA40XY%oHq!t?(Lr0~OY>31K)ah8N*1 zgyPn-hHxi51h2y%kiLOi2%doVA#tNfM;Hn7;T8A}Dn2DL6qdsw_zZr9s!y|QkO2?C zI(P$qgo>L)+Cbk;tp9x&tKnt%0DgiBn{j}#umTRi_i)V?k=r2~*1}Qv5n{Kp%b^Yo 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 diff --git a/packit/dex/catalog.dex b/packit/dex/catalog.dex index 24de1cc5db95a4e5766a3386bb81f004796ff27a..27c77d65be3c2c11ddaa75e30709ae4e983a714f 100644 GIT binary patch delta 17808 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%0DgiBn{j}#umTRi_i)V?k=r2~*1}Qv5n{Kp%b^Yo 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 diff --git a/packit/dex/openfile.dex b/packit/dex/openfile.dex index 24de1cc5db95a4e5766a3386bb81f004796ff27a..27c77d65be3c2c11ddaa75e30709ae4e983a714f 100644 GIT binary patch delta 17808 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%0DgiBn{j}#umTRi_i)V?k=r2~*1}Qv5n{Kp%b^Yo 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 diff --git a/packit/dex/sfx.dex b/packit/dex/sfx.dex index 24de1cc5db95a4e5766a3386bb81f004796ff27a..27c77d65be3c2c11ddaa75e30709ae4e983a714f 100644 GIT binary patch delta 17808 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%0DgiBn{j}#umTRi_i)V?k=r2~*1}Qv5n{Kp%b^Yo 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 diff --git a/packit/meta.yml b/packit/meta.yml index 748e90f..c92aab5 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.1.1-dev.23" +version: "0.1.1-dev.24" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" From 368dcdd673264cce3bd3b8cc6634945c66e12042 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 09:08:13 +0000 Subject: [PATCH 07/46] Stage suggestion uploads with a write the volume accepts (0.1.1-dev.25) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Submitting a plugin still ended in "Failed to send", with the log naming a file the staging step could not create: suggest._do_submit task error: [Errno 13] Permission denied: '/storage/emulated/0/Android/data/org.telegram.messenger/cache/tmp....plugin' The directory itself is fine — the log export writes its own file into that exact folder and succeeds, and getStagingDir had probed it with a plain open() before handing it over. Staging then used a different call: tempfile.NamedTemporaryFile opens with O_EXCL|O_NOFOLLOW at mode 0600 and shutil.copy2 chmods the copy afterwards, and the FUSE-emulated external volume refuses those where an ordinary write goes through. So the probe passed, the real write did not, and the submit died before sending anything. Staging now goes through paths.stageFileForUpload, which creates the file the same way the probe does — plain open() and a byte copy, no metadata copy — so the check and the write can no longer disagree. If the external directory refuses it regardless, the copy falls back to the internal cache; that path is already covered by the isInternalUri hook, so the upload proceeds instead of failing. paths.py also logs through logx now. It was using android_utils.log, which never reaches latestlog.txt, so a bug report had no way to show which directory staging picked. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/suggest/fragment.py | 15 +++-------- packit/src/utils/paths.py | 45 ++++++++++++++++++++++++++++--- 3 files changed, 47 insertions(+), 15 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index c92aab5..c2b770c 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.1.1-dev.24" +version: "0.1.1-dev.25" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/suggest/fragment.py b/packit/src/ui/suggest/fragment.py index 46aa688..4d5f224 100644 --- a/packit/src/ui/suggest/fragment.py +++ b/packit/src/ui/suggest/fragment.py @@ -3513,23 +3513,16 @@ def _task(): # app cache is preferred (isInternalUri lets those through), but # some ROMs deny writes there — get_cache_dir() pointed straight # at it and the whole submit died with EACCES. - import shutil, tempfile - from ...utils.paths import getStagingDir, isInternalPath - staging_dir = getStagingDir() + from ...utils.paths import stageFileForUpload, isInternalPath def _stage_for_upload(src: str, display_name: str) -> str: suffix = "" dot = display_name.rfind(".") if dot >= 0: suffix = display_name[dot:] - tmp = tempfile.NamedTemporaryFile( - delete=False, suffix=suffix, - dir=staging_dir - ) - tmp.close() - shutil.copy2(src, tmp.name) - logx(f"suggest._task: staged {src} -> {tmp.name}", True) - return tmp.name + dst = stageFileForUpload(src, suffix) + logx(f"suggest._task: staged {src} -> {dst}", True) + return dst staged_main = None staged_extras = [] diff --git a/packit/src/utils/paths.py b/packit/src/utils/paths.py index aa9579b..682251e 100644 --- a/packit/src/utils/paths.py +++ b/packit/src/utils/paths.py @@ -1,7 +1,7 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -from android_utils import log +from packutil import logx def _filesDir() -> str: from org.telegram.messenger import ApplicationLoader @@ -68,6 +68,11 @@ def _externalCacheDir() -> str: _stagingDir = None def _isWritableDir(path: str) -> bool: + # probes with the same call stageFileForUpload uses. It used to differ from + # the real write and the mismatch cost a whole feature: the probe's plain + # open() succeeded on the emulated external volume while the actual staging + # went through tempfile + copy2, which that volume refuses — so the dir was + # declared usable and every upload then died with EACCES. if not path: return False try: @@ -79,7 +84,7 @@ def _isWritableDir(path: str) -> bool: os.remove(probe) return True except Exception as e: - log(f"paths: {path} not writable: {e}") + logx(f"paths: {path} not writable: {e}", False) return False def getStagingDir() -> str: @@ -100,9 +105,43 @@ def getStagingDir() -> str: break else: _stagingDir = _cacheDir() - log(f"paths: staging dir = {_stagingDir}") + logx(f"paths: staging dir = {_stagingDir}", True) return _stagingDir + +def stageFileForUpload(src: str, suffix: str = "") -> str: + # Copies src to a place Telegram will read and returns the new path. + # + # Plain open() + byte copy on purpose. tempfile.NamedTemporaryFile opens + # with O_EXCL|O_NOFOLLOW at mode 0600 and shutil.copy2 chmods the result + # afterwards; the FUSE-emulated external volume refuses those even in a + # directory where an ordinary write works (which is how the log export + # writes to the very same folder). That EACCES aborted the whole + # suggestion upload before anything was sent. + # + # If the external dir refuses the copy anyway, fall back to the internal + # cache — always writable, and callers hook isInternalUri for such paths. + import os + import shutil + import uuid + name = f"packit_{uuid.uuid4().hex[:12]}{suffix}" + last = None + for directory in (getStagingDir(), _cacheDir()): + dst = os.path.join(directory, name) + try: + os.makedirs(directory, exist_ok=True) + shutil.copyfile(src, dst) + logx(f"paths: staged {src} -> {dst}", True) + return dst + except Exception as e: + last = e + logx(f"paths: staging into {directory} failed: {e}", False) + try: + os.unlink(dst) + except Exception: + pass + raise last if last is not None else Exception("staging failed") + def isInternalPath(path: str) -> bool: # matches what isInternalUri() rejects: the app's own private storage return bool(path) and (str(path).startswith("/data/") or "/files/" in str(path)) From ddc53a9746c1919b5ccce67ed0bbadcd6790b8e1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 10:11:06 +0000 Subject: [PATCH 08/46] Fold the overflowing tag into "+N" instead of squeezing it (0.1.1-dev.26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A card with four tags showed the last one as a thin sliver once the search results narrowed the row: Chimera NFT keeps Elyx / Featured / Customization / Fun, and with the match badge taking the right-hand side "Fun" was laid out a few dp wide. The overflow listener asked each chip for getWidth(). By the time it runs, a row that overran has already had its last chip squeezed into the space that was left, so the widths add up to exactly the row width — the check "do the chips fit" compared the result of not fitting against itself, found no overflow, and left "+N" hidden with the squeezed chip on the card. Chips are now measured unconstrained, the same way the "+N" chip already was, so the total reflects the width they actually want. The overflowing tag is dropped and counted in "+N" as intended, which opens the profile at the full tag list. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- .../filter/tagLayoutListener.py | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index c2b770c..5a822de 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.1.1-dev.25" +version: "0.1.1-dev.26" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/PluginListActivity/filter/tagLayoutListener.py b/packit/src/ui/PluginListActivity/filter/tagLayoutListener.py index 0430c4e..995a6cd 100644 --- a/packit/src/ui/PluginListActivity/filter/tagLayoutListener.py +++ b/packit/src/ui/PluginListActivity/filter/tagLayoutListener.py @@ -6,8 +6,17 @@ def _chip_extent(child) -> int: - # laid-out width plus the trailing margin the row adds after it - w = child.getWidth() + # The width the chip WANTS, plus the trailing margin the row adds after it. + # + # Not getWidth(): once the chips overrun the row the last one is laid out + # squeezed into whatever is left, so reading its width back makes the total + # add up to exactly the row width. Overflow then looked like a perfect fit, + # "+N" stayed hidden and the squeezed chip remained on the card as a sliver + # — visible on cards with four tags once the search-match badge narrows the + # row. + w = _measure_width(child) + if w <= 0: + w = child.getWidth() try: w += child.getLayoutParams().rightMargin except Exception: @@ -16,8 +25,9 @@ def _chip_extent(child) -> int: def _measure_width(view) -> int: - # the "+N" chip starts GONE, so it has no laid-out width yet — measure it - # unconstrained to learn how much room it needs + # unconstrained measure: the natural width of a chip, ignoring how the row + # ended up laying it out (and the only way to size the "+N" chip, which + # starts GONE and therefore has no laid-out width at all) try: spec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) view.measure(spec, spec) From d20bdbdb3b7df690bce388cfd53dcca95eb0c62b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 7 Aug 2026 15:17:17 +0000 Subject: [PATCH 09/46] Count what the list holds and keep an emptied filter empty (0.1.1-dev.27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things went wrong once a filter was applied. The header kept showing the unfiltered total. It asked _is_filtered() whether anything was active, and that helper built its tag universe from the tag names on the plugins — the untagged bucket was not among them. So filtering by "Unsorted", which is 12 of the official repo's 43 plugins, tested {"__unsorted__"} < {Utility, Tweaks, ...}, came back false, and the header stayed on 43 while the list below correctly held 12. The same test also misread a selection equal to the full set, or one carrying a name no plugin uses any more. The count now comes from the two lists themselves — len(filtered) against len(plugins) — which cannot drift from whatever the filter engine did. The helper is gone rather than fixed; there is no second source of truth to keep in sync. Switching every chip in a section off silently turned the section back on: the drawer refilled an empty selection with every key each time it populated, so the filters looked like they reset themselves. Empty is now a real state and shows an empty list (the existing "no plugins" stub), while "never touched" is carried as None and still means no filter — a section opens with everything selected exactly as before. The author and app version filters also compared with a strict subset, so a selection holding every value, or a stale one, counted as no filter; they now filter unless the selection covers everything. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- .../PluginListActivity/filter/filterDrawer.py | 49 ++++++++++++------- packit/src/ui/PluginListActivity/fragment.py | 45 +++++++++++------ .../ui/PluginListActivity/helpers/utils.py | 22 --------- 4 files changed, 64 insertions(+), 54 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 5a822de..7e35f3f 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.1.1-dev.26" +version: "0.1.1-dev.27" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/PluginListActivity/filter/filterDrawer.py b/packit/src/ui/PluginListActivity/filter/filterDrawer.py index 8479eae..03a0c57 100644 --- a/packit/src/ui/PluginListActivity/filter/filterDrawer.py +++ b/packit/src/ui/PluginListActivity/filter/filterDrawer.py @@ -156,7 +156,12 @@ def __init__(self, act, content_view, plugins, selected_tags, on_apply, self.on_apply = on_apply self._is_open = False - self._current_selected = set(selected_tags) if selected_tags else set() + # None means the section was never touched: it opens with everything + # selected, which is the same thing as no filter. An empty set is a + # real choice — every chip switched off — and has to stay empty + # instead of quietly flipping back to all-on, which looked like the + # filters resetting themselves. + self._current_selected = set(selected_tags) if selected_tags is not None else None self._tags_summary = {} self._tag_rows = {} # tag_name -> (row_view, border_drawable) self._tags_expanded = False @@ -164,17 +169,17 @@ def __init__(self, act, content_view, plugins, selected_tags, on_apply, self._authors_summary = {} self._author_rows = {} self._authors_expanded = False - self._current_authors = set(selected_authors) if selected_authors else set() + self._current_authors = set(selected_authors) if selected_authors is not None else None self._app_versions_summary = {} self._app_version_rows = {} self._app_versions_expanded = False - self._current_app_versions = set(selected_app_versions) if selected_app_versions else set() + self._current_app_versions = set(selected_app_versions) if selected_app_versions is not None else None # saved filter: set of "saved" and/or "unsaved", both active by default self._saved_rows = {} self._saved_expanded = False - self._current_saved = set(selected_saved) if selected_saved else {"saved", "unsaved"} + self._current_saved = set(selected_saved) if selected_saved is not None else None self._overlay = None self._drawer = None @@ -649,11 +654,16 @@ def on_reset(v): self._refresh_rows() def on_apply(v): + # a section that never populated stays None (= untouched), so it + # keeps meaning "no filter" instead of "nothing selected" + def _snapshot(sel): + return set(sel) if sel is not None else None + self.on_apply( - set(self._current_selected), - set(self._current_authors), - set(self._current_app_versions), - set(self._current_saved), + _snapshot(self._current_selected), + _snapshot(self._current_authors), + _snapshot(self._current_app_versions), + _snapshot(self._current_saved), ) self.close() @@ -837,7 +847,7 @@ def _populate_generic(self, section_key): self._authors_list.removeAllViews() self._author_rows.clear() self._authors_summary = _collect_authors(self.plugins) - if not self._current_authors: + if self._current_authors is None: self._current_authors = set(self._authors_summary.keys()) summary = self._authors_summary current_sel = self._current_authors @@ -858,7 +868,7 @@ def _populate_generic(self, section_key): } except Exception: pass - if not self._current_saved: + if self._current_saved is None: self._current_saved = {"saved", "unsaved"} for key, label in saved_items.items(): is_sel = key in self._current_saved @@ -883,7 +893,7 @@ def handler(v): self._app_versions_list.removeAllViews() self._app_version_rows.clear() self._app_versions_summary = _collect_app_versions(self.plugins) - if not self._current_app_versions: + if self._current_app_versions is None: self._current_app_versions = set(self._app_versions_summary.keys()) summary = self._app_versions_summary current_sel = self._current_app_versions @@ -918,7 +928,7 @@ def _populate_tags(self): self._tag_rows.clear() self._tags_summary = filterEngine.collect_tags(self.plugins) - if not self._current_selected: + if self._current_selected is None: self._current_selected = set(self._tags_summary.keys()) for tag_name, count in self._tags_summary.items(): @@ -953,14 +963,19 @@ def handler(v): logx(f"SortDrawer._populate_tags error: {e}", False) def _refresh_rows(self): + # a section can still be None here if its populate failed + tags = self._current_selected or set() + authors = self._current_authors or set() + versions = self._current_app_versions or set() + saved = self._current_saved or set() for tag_name, (row, border, name_tv) in self._tag_rows.items(): - self._update_row_style(border, name_tv, tag_name in self._current_selected) + self._update_row_style(border, name_tv, tag_name in tags) for key, (row, border, name_tv) in self._author_rows.items(): - self._update_row_style(border, name_tv, key in self._current_authors) + self._update_row_style(border, name_tv, key in authors) for key, (row, border, name_tv) in self._app_version_rows.items(): - self._update_row_style(border, name_tv, key in self._current_app_versions) + self._update_row_style(border, name_tv, key in versions) for key, (row, border, name_tv) in self._saved_rows.items(): - self._update_row_style(border, name_tv, key in self._current_saved) + self._update_row_style(border, name_tv, key in saved) def _register_back_callback(self): try: @@ -992,7 +1007,7 @@ def _unregister_back_callback(self): def open(self, selected_tags, selected_authors=None, selected_app_versions=None, selected_saved=None): try: - self._current_selected = set(selected_tags) if selected_tags else set() + self._current_selected = set(selected_tags) if selected_tags is not None else None if selected_authors is not None: self._current_authors = set(selected_authors) if selected_app_versions is not None: diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index e88ece2..205cb17 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -112,7 +112,7 @@ from .helpers.utils import ( _count_active_repos, _plural_form, _format_plural, _build_stats_label, - _build_plugin_count_label, _is_filtered, _parse_version, _check_app_version, + _build_plugin_count_label, _parse_version, _check_app_version, _filter_unavailable, ) @@ -434,10 +434,13 @@ def __init__(self, install_ui, title, plugins, show_loading_initial=False, repo_ self.is_loading = False self.scroll_listener = None self.current_sort_type = "alpha_az" - self.selected_tags = set() - self.selected_authors = set() - self.selected_app_versions = set() - self.selected_saved = {"saved", "unsaved"} + # None = untouched (no filter). A set is what the drawer applied, + # and an empty one means "nothing selected" — which shows nothing, + # not everything. + self.selected_tags = None + self.selected_authors = None + self.selected_app_versions = None + self.selected_saved = None self._active_drawer = None self.batch_size = 10 self._ai_result_active = False @@ -680,35 +683,45 @@ def build_list_with_sort(self, sort_type: str, q=None): scored.sort(key=lambda x: x[0]) filtered = [p for _, p in scored] - if self.selected_tags: - filtered = tag_mod.filter_by_tags(filtered, self.selected_tags) + # None = section never filtered. An empty selection is deliberate + # and matches nothing — the drawer no longer rewrites it to "all". + if self.selected_tags is not None: + filtered = tag_mod.filter_by_tags(filtered, self.selected_tags) if self.selected_tags else [] - if self.selected_authors: + if self.selected_authors is not None: all_authors = set( str(p.get("author") or "").strip() for p in self.plugins if str(p.get("author") or "").strip() and str(p.get("author") or "").strip().lower() != "unknown" ) - # no filter if all selected - if self.selected_authors < all_authors: + # everything selected is the same as no filter; anything less + # filters (">=" and not "<": a selection holding a stale name + # is still a filter, a strict-subset test called it none) + if not self.selected_authors: + filtered = [] + elif not (self.selected_authors >= all_authors): filtered = [ p for p in filtered if str(p.get("author") or "").strip() in self.selected_authors ] - if self.selected_app_versions: + if self.selected_app_versions is not None: all_versions = set( str(p.get("app_version") or "").strip() for p in self.plugins if str(p.get("app_version") or "").strip() and str(p.get("app_version") or "").strip().lower() != "unknown" ) - if self.selected_app_versions < all_versions: + if not self.selected_app_versions: + filtered = [] + elif not (self.selected_app_versions >= all_versions): filtered = [ p for p in filtered if str(p.get("app_version") or "").strip() in self.selected_app_versions ] - if hasattr(self, 'selected_saved') and self.selected_saved != {"saved", "unsaved"}: + if getattr(self, "selected_saved", None) is not None and not self.selected_saved: + filtered = [] + elif getattr(self, "selected_saved", None) is not None and self.selected_saved != {"saved", "unsaved"}: try: from ..PluginActivity.fragment import _read_saved_plugins saved_ids = set(_read_saved_plugins()) @@ -732,7 +745,11 @@ def build_list_with_sort(self, sort_type: str, q=None): self.filtered_plugins = filtered if hasattr(self, 'subtitle'): total = len(self.plugins) - if _is_filtered(self): + # straight from the two lists. Asking a helper whether a filter + # "is active" meant the header could disagree with what the list + # actually holds: its tag universe left out the untagged bucket, + # so filtering by "Unsorted" (12 of 43) kept the header on 43. + if len(filtered) != total: self.subtitle.setText(f"{len(filtered)}/{_build_plugin_count_label(total)}") else: self.subtitle.setText(_build_plugin_count_label(total)) diff --git a/packit/src/ui/PluginListActivity/helpers/utils.py b/packit/src/ui/PluginListActivity/helpers/utils.py index 0d68c9b..80d9554 100644 --- a/packit/src/ui/PluginListActivity/helpers/utils.py +++ b/packit/src/ui/PluginListActivity/helpers/utils.py @@ -74,28 +74,6 @@ def _build_plugin_count_label(plugin_count: int) -> str: except Exception: return strings("plugin_many", plugin_count) -def _is_filtered(self_obj) -> bool: - # true if any filter reduces the full plugin set - all_tags = set() - all_authors = set() - all_versions = set() - for p in self_obj.plugins: - for t in (p.get("tags") or []): - if isinstance(t, list) and t: - all_tags.add(t[0]) - a = str(p.get("author") or "").strip() - if a and a.lower() != "unknown": - all_authors.add(a) - v = str(p.get("app_version") or "").strip() - if v and v.lower() != "unknown": - all_versions.add(v) - - tags_filtered = bool(self_obj.selected_tags) and self_obj.selected_tags < all_tags - authors_filtered = bool(self_obj.selected_authors) and self_obj.selected_authors < all_authors - versions_filtered = bool(self_obj.selected_app_versions) and self_obj.selected_app_versions < all_versions - saved_filtered = hasattr(self_obj, 'selected_saved') and self_obj.selected_saved != {"saved", "unsaved"} - return tags_filtered or authors_filtered or versions_filtered or saved_filtered - def _parse_version(v_str): try: return tuple(int(x) for x in str(v_str).strip().split(".")) From e62bf0802713efdbb1f2453b0c9deab8880d035f Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 8 Aug 2026 09:31:13 +0000 Subject: [PATCH 10/46] Version 0.1.1 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packit/meta.yml b/packit/meta.yml index 7e35f3f..8fc02bc 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.1.1-dev.27" +version: "0.1.1" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" From bb713ea58eadd247e4be76b6a35e21c1a12b21e9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:08:29 +0000 Subject: [PATCH 11/46] Rewrite the sources screen as a fragment with repository cards (0.1.2-dev.1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The screen was a settings list: seven rows per repository, an icon picked by name out of the host's R.drawable catalogue through a sub-fragment listing several hundred of them, and editing done through inline inputs. It carried no information about what a repository actually holds. It is a fragment now, one card per repository: avatar, name, maintainer, a switch, a status chip and — where the cached repomap happens to carry the lists — counts. Telegram and source links sit as buttons on the card, everything else (edit, copy link, share, delete) moved into its overflow menu, and the six bulk actions that used to hide behind "Дополнительно" are behind the button next to the counter. Cards fade in staggered, tapping one flips its switch, a disabled repository goes outlined and dim. The three fields the developer just added to repometa are what makes this possible: rm_icon is now an image url, so the avatar is downloaded, cached on disk and in memory, and drawn over a monogram that stands in until it arrives — repository icons no longer come from R.drawable at all. rm_telegram and rm_source became the two buttons. The repo=add deeplink sheet accepts both spellings of rm_icon, since older repomaps still put a drawable name there. Adding and editing use the dialog the api-key screen already had: dimmed overlay, card that springs in, outlined field, one accent button. Its overlay, back handling, keyboard tracking and animations are imported from AddKeyDialog rather than copied. What is new is that a bad link is answered in place — scheme, duplicate and every error addRepositoryWithUrl can return are localized and shown under the field with the dialog still open, instead of dismissing first and dropping an english bulletin afterwards. Screens no longer rebuilt by setRepositories' rebuildAllItems() call, which only ever reached the settings list, listen through a small registry instead, so a deeplink adding a source repaints an open screen. Every action re-resolves its repository by id before touching it: updateAllCaches drops entries by index on startup and the old code held indexes across that. SettingsActivity/repos.py and icons.py are gone, along with their five dead string keys. Adds the "retry" key the plugin catalog has been asking for in four locales without ever finding it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 52 +- packit/locales/strings_de.json | 42 +- packit/locales/strings_en.json | 48 +- packit/locales/strings_ru.json | 52 +- packit/meta.yml | 2 +- packit/src/MainActivity.py | 11 +- packit/src/RepositoryManager.py | 7 + packit/src/SettingsActivity/icons.py | 233 -------- packit/src/SettingsActivity/repos.py | 735 ------------------------ packit/src/deeplinks/repo.py | 19 +- packit/src/ui/ReposActivity/__init__.py | 40 ++ packit/src/ui/ReposActivity/actions.py | 310 ++++++++++ packit/src/ui/ReposActivity/addSheet.py | 483 ++++++++++++++++ packit/src/ui/ReposActivity/card.py | 274 +++++++++ packit/src/ui/ReposActivity/fragment.py | 466 +++++++++++++++ packit/src/ui/ReposActivity/repoIcon.py | 263 +++++++++ packit/src/utils/imagePool.py | 99 ++++ packit/src/utils/paths.py | 10 + 18 files changed, 2119 insertions(+), 1027 deletions(-) delete mode 100644 packit/src/SettingsActivity/icons.py delete mode 100644 packit/src/SettingsActivity/repos.py create mode 100644 packit/src/ui/ReposActivity/__init__.py create mode 100644 packit/src/ui/ReposActivity/actions.py create mode 100644 packit/src/ui/ReposActivity/addSheet.py create mode 100644 packit/src/ui/ReposActivity/card.py create mode 100644 packit/src/ui/ReposActivity/fragment.py create mode 100644 packit/src/ui/ReposActivity/repoIcon.py create mode 100644 packit/src/utils/imagePool.py diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 1647c18..a0df5c5 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -19,9 +19,7 @@ "contributors": "Удзельнікі", "max_repositories_allowed": "Максімум 10 рэпазіторыяў. Расслабься.", "max_buttons_allowed": "Максімум 4 кнопкі. Расслабься.", - "fill_previous_repository": "Спачатку запоўні папярэдні рэпазіторый. Сур'ёзна.", "add_repository": "Дадаць рэпазіторый", - "repository_form": "Рэпазіторый #{0}", "delete_repository_title": "Выдаліць", "delete_repository_message": "Ты ўпэўнены, што хочаш выдаліць гэты рэпазіторый? Назад шляху няма.", "delete_button": "Выдаліць", @@ -33,9 +31,6 @@ "achiev_unlocked": "Дасягненне разблакавана!", "repo_link_copied": "Скапіявана ў буфер! Магія.", "failed_to_copy": "Не атрымалася скапіяваць :(", - "repo_icon_text": "Іконка: {0}", - "repo_icon_not_selected": "Іконка: не выбрана", - "repo_enabled": "Уключана", "repo_name": "Назва", "repo_url": "URL канфігурацыі", "invalid_repository": "Няправільны рэпазіторый", @@ -99,7 +94,6 @@ "file_not_found": "Файл не знойдзены :(", "error_opening_settings": "Памылка адкрыцця налад: {0}", "error_generic": "Памылка: {0}", - "icon_selected": "Іконка \"{0}\" выбрана! Крута.", "arrows_navigation": "Стрэлкі і навігацыя", "attachments_media": "Укладанні і медыя", "avatar_profile": "Аватар і профіль", @@ -827,8 +821,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 — гатова. Інкогніта актывавана, акуляры надзеты, ніхто нічога не бачыў.", + "achiev_title_secret_opsec": "Аперацыйная бяспека", + "achiev_hint_secret_opsec": "Ты ўсталяваў opsec прывілеяваным спосабам: sudo packit install opsec.", "tags_section_title": "Тэгі", "apply_button": "Ужыць", "authors_section_title": "Аўтары", @@ -1136,8 +1130,6 @@ "role_founder": "Заснавальнік", "achiev_title_secret_connect_is_bullshit": "Усе мы ненавідзім Connect", "achiev_hint_secret_connect_is_bullshit": "Малююць багіню Умі Асанагі, не ведаючы анатомію чалавека... Гарэць ім у пекле.", - "achiev_title_secret_opsec": "Аперацыйная бяспека", - "achiev_hint_secret_opsec": "Ты ўсталяваў opsec прывілеяваным спосабам: sudo packit install opsec.", "pp_closed_source_moderated": "Закрыты зыходны код, але з мадэрацыяй, таму вы ў бяспецы.", "beta_build_title": "Бэта-зборка", "beta_build_msg": "Вы не тэсціроўшчык, вам не варта было ўсталёўваць PackIt.", @@ -1149,10 +1141,6 @@ "pp_no_license": "Няма ліцэнзіі", "pp_languages": "Мовы", "pp_clients": "Кліенты", - "pp_closed_source_moderated": "Закрыты зыходны код, але з мадэрацыяй, таму вы ў бяспецы.", - "beta_build_title": "Бэта-зборка", - "beta_build_msg": "Вы не тэсціроўшчык, вам не варта было ўсталёўваць PackIt.", - "delete_packit": "Выдаліць PackIt", "debug_menu": "Меню адладкі", "debug_menu_desc": "Меню для адладкі", "debug_logs": "Логі адладкі", @@ -1183,5 +1171,39 @@ "bi_root_no": "Не", "bi_app_version": "Версія праграмы", "bi_app_package": "Пакет праграмы", - "plus_sponsor": "+ Спонсар" + "plus_sponsor": "+ Спонсар", + "repo_card_status_ok": "Абноўлены", + "repo_card_status_stale": "Кэш састарэў", + "repo_card_status_missing": "Не загружаны", + "repo_card_status_disabled": "Адключаны", + "repo_card_plugins": "{0} плагінаў", + "repo_card_icons": "{0} набораў", + "repo_edit": "Змяніць", + "repo_copy_link": "Скапіяваць спасылку", + "repos_updating": "Абнаўленне крыніц…", + "repo_add_sheet_title": "Новая крыніца", + "repo_add_sheet_subtitle": "Устаўце спасылку на repomap.json", + "repo_add_field_url": "Спасылка", + "repo_sheet_edit_title": "Змяніць крыніцу", + "repo_sheet_edit_sub": "Назва і спасылка", + "repo_err_empty": "Увядзіце спасылку", + "repo_err_scheme": "Спасылка мусіць пачынацца з https://", + "repo_err_duplicate": "Гэтая крыніца ўжо дададзена", + "repo_err_not_found": "Файл не знойдзены", + "repo_err_forbidden": "Доступ забаронены", + "repo_err_rate_limited": "Занадта шмат запытаў, паспрабуйце пазней", + "repo_err_redirect": "Спасылка перанакіроўвае — патрэбна простая спасылка на файл", + "repo_err_timeout": "Час чакання скончыўся", + "repo_err_server": "Памылка на баку сервера", + "repo_err_http": "Сервер адказаў: {0}", + "repo_err_json": "Файл не з'яўляецца карэктным JSON", + "repo_err_meta": "У файле няма блока repometa", + "repo_err_rid": "У repometa няма rm_rid", + "repo_err_name": "У repometa няма rm_name", + "repo_err_cache": "Не атрымалася захаваць кэш", + "repo_err_network": "Няма злучэння", + "repo_err_unknown": "Невядомая памылка: {0}", + "repos_empty_title": "Пакуль пуста", + "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", + "retry": "Паўтарыць" } diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index b1b0cd4..d35e888 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -19,9 +19,7 @@ "contributors": "Mitwirkende", "max_repositories_allowed": "Maximal 10 Repositorys zulässig. Kühlen.", "max_buttons_allowed": "Maximal 4 Tasten zulässig. Kühlen.", - "fill_previous_repository": "Füllen Sie zuerst das vorherige Repository aus. Ja, wirklich.", "add_repository": "Repository hinzufügen", - "repository_form": "Repository Nr.{0}", "delete_repository_title": "Löschen", "delete_repository_message": "Sind Sie sicher, dass Sie dieses Repository löschen möchten? Keine Rücknahme.", "delete_button": "Löschen", @@ -33,9 +31,6 @@ "achiev_unlocked": "Erfolg freigeschaltet!", "repo_link_copied": "In die Zwischenablage kopiert! Magie.", "failed_to_copy": "Kopieren fehlgeschlagen :(", - "repo_icon_text": "Symbol: {0}", - "repo_icon_not_selected": "Symbol: nicht ausgewählt", - "repo_enabled": "Ermöglicht", "repo_name": "Name", "repo_url": "Konfigurations-URL", "invalid_repository": "Ungültiges Repository", @@ -99,7 +94,6 @@ "file_not_found": "Datei nicht gefunden :(", "error_opening_settings": "Fehler beim Öffnen der Einstellungen: {0}", "error_generic": "Fehler: {0}", - "icon_selected": "Symbol „{0}“ ausgewählt! Schick.", "arrows_navigation": "Pfeile und Navigation", "attachments_media": "Anhänge und Medien", "avatar_profile": "Avatar & Profil", @@ -1177,5 +1171,39 @@ "bi_root_no": "Nein", "bi_app_version": "App-Version", "bi_app_package": "App-Paket", - "plus_sponsor": "+ Sponsor" + "plus_sponsor": "+ Sponsor", + "repo_card_status_ok": "Aktuell", + "repo_card_status_stale": "Cache veraltet", + "repo_card_status_missing": "Nicht geladen", + "repo_card_status_disabled": "Deaktiviert", + "repo_card_plugins": "{0} Plugins", + "repo_card_icons": "{0} Icon-Sets", + "repo_edit": "Bearbeiten", + "repo_copy_link": "Link kopieren", + "repos_updating": "Quellen werden aktualisiert…", + "repo_add_sheet_title": "Neue Quelle", + "repo_add_sheet_subtitle": "Link zur repomap.json einfügen", + "repo_add_field_url": "Link", + "repo_sheet_edit_title": "Quelle bearbeiten", + "repo_sheet_edit_sub": "Name und Link", + "repo_err_empty": "Link eingeben", + "repo_err_scheme": "Der Link muss mit https:// beginnen", + "repo_err_duplicate": "Diese Quelle ist bereits hinzugefügt", + "repo_err_not_found": "Datei nicht gefunden", + "repo_err_forbidden": "Zugriff verweigert", + "repo_err_rate_limited": "Zu viele Anfragen, später erneut versuchen", + "repo_err_redirect": "Der Link leitet weiter — direkter Dateilink nötig", + "repo_err_timeout": "Zeitüberschreitung", + "repo_err_server": "Serverfehler", + "repo_err_http": "Server antwortete: {0}", + "repo_err_json": "Die Datei ist kein gültiges JSON", + "repo_err_meta": "Die Datei hat keinen repometa-Block", + "repo_err_rid": "repometa hat kein rm_rid", + "repo_err_name": "repometa hat kein rm_name", + "repo_err_cache": "Cache konnte nicht gespeichert werden", + "repo_err_network": "Keine Verbindung", + "repo_err_unknown": "Unbekannter Fehler: {0}", + "repos_empty_title": "Noch nichts da", + "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", + "retry": "Erneut versuchen" } diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 04eafdf..84ac697 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -19,9 +19,7 @@ "contributors": "Contributors", "max_repositories_allowed": "Maximum of 10 repositories allowed. Chill.", "max_buttons_allowed": "Maximum of 4 buttons allowed. Chill.", - "fill_previous_repository": "Fill in the previous repository first. Yes, really.", "add_repository": "Add Repository", - "repository_form": "Repository #{0}", "delete_repository_title": "Delete", "delete_repository_message": "Are you sure you want to delete this repository? No takebacks.", "delete_button": "Delete", @@ -33,9 +31,6 @@ "achiev_unlocked": "Achievement Unlocked!", "repo_link_copied": "Copied to clipboard! Magic.", "failed_to_copy": "Failed to copy :(", - "repo_icon_text": "Icon: {0}", - "repo_icon_not_selected": "Icon: not selected", - "repo_enabled": "Enabled", "repo_name": "Name", "repo_url": "Config URL", "invalid_repository": "Invalid repository", @@ -99,7 +94,6 @@ "file_not_found": "File not found :(", "error_opening_settings": "Error opening settings: {0}", "error_generic": "Error: {0}", - "icon_selected": "Icon \"{0}\" selected! Fancy.", "arrows_navigation": "Arrows & Navigation", "attachments_media": "Attachments & Media", "avatar_profile": "Avatar & Profile", @@ -827,8 +821,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.", + "achiev_title_secret_opsec": "Operational security", + "achiev_hint_secret_opsec": "You installed opsec the privileged way: sudo packit install opsec.", "tags_section_title": "Tags", "apply_button": "Apply", "authors_section_title": "Authors", @@ -1136,8 +1130,6 @@ "role_founder": "Founder", "achiev_title_secret_connect_is_bullshit": "We all hate Connect", "achiev_hint_secret_connect_is_bullshit": "Drawing goddess Umi Asanagi without knowing human anatomy... May they burn in hell.", - "achiev_title_secret_opsec": "Operational security", - "achiev_hint_secret_opsec": "You installed opsec the privileged way: sudo packit install opsec.", "pp_section_about": "About the project", "pp_source_code": "Source code", "pp_not_provided": "Not provided", @@ -1179,5 +1171,39 @@ "bi_root_no": "No", "bi_app_version": "App version", "bi_app_package": "App package", - "plus_sponsor": "+ Sponsor" + "plus_sponsor": "+ Sponsor", + "repo_card_status_ok": "Up to date", + "repo_card_status_stale": "Cache is stale", + "repo_card_status_missing": "Not loaded", + "repo_card_status_disabled": "Disabled", + "repo_card_plugins": "{0} plugins", + "repo_card_icons": "{0} icon packs", + "repo_edit": "Edit", + "repo_copy_link": "Copy link", + "repos_updating": "Refreshing repositories…", + "repo_add_sheet_title": "New source", + "repo_add_sheet_subtitle": "Paste a link to repomap.json", + "repo_add_field_url": "Link", + "repo_sheet_edit_title": "Edit source", + "repo_sheet_edit_sub": "Name and link", + "repo_err_empty": "Enter a link", + "repo_err_scheme": "The link must start with https://", + "repo_err_duplicate": "This source is already added", + "repo_err_not_found": "File not found", + "repo_err_forbidden": "Access denied", + "repo_err_rate_limited": "Too many requests, try again later", + "repo_err_redirect": "The link redirects — use a direct file link", + "repo_err_timeout": "Request timed out", + "repo_err_server": "Server error", + "repo_err_http": "The server answered: {0}", + "repo_err_json": "The file is not valid JSON", + "repo_err_meta": "The file has no repometa block", + "repo_err_rid": "repometa has no rm_rid", + "repo_err_name": "repometa has no rm_name", + "repo_err_cache": "Could not save the cache", + "repo_err_network": "No connection", + "repo_err_unknown": "Unknown error: {0}", + "repos_empty_title": "Nothing here yet", + "repos_empty_text": "Add a source to install plugins", + "retry": "Retry" } diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index aa0d7bb..5e82f6d 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -19,9 +19,7 @@ "contributors": "Участники", "max_repositories_allowed": "Максимум 10 репозиториев разрешено :(", "max_buttons_allowed": "Максимум 4 кнопки. Полегче, этого достаточно.", - "fill_previous_repository": "Сначала заполните предыдущий репозиторий. Пожалуйста", "add_repository": "Добавить репозиторий", - "repository_form": "Репозиторий №{0}", "delete_repository_title": "Удалить репозиторий", "delete_repository_message": "Вы уверены, что хотите удалить репозиторий?", "delete_button": "Удалить", @@ -33,9 +31,6 @@ "achiev_unlocked": "Достижение разблокировано!", "repo_link_copied": "Уже в буфер обмене!", "failed_to_copy": "Буфер отказался… ну что ж, бывает", - "repo_icon_text": "Иконка: {0}", - "repo_icon_not_selected": "Иконка: не выбрана", - "repo_enabled": "Включено", "repo_name": "Название", "repo_url": "Ссылка", "invalid_repository": "Неверный репозиторий", @@ -99,7 +94,6 @@ "file_not_found": "Файл не найден :(", "error_opening_settings": "Ошибка открытия настроек: {0}", "error_generic": "Ошибка: {0}", - "icon_selected": "Иконка \"{0}\" выбрана!", "arrows_navigation": "Стрелки и навигация", "attachments_media": "Вложения и медиа", "avatar_profile": "Аватар и профиль", @@ -827,8 +821,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 — готово. Инкогнито активировано, очки надеты, никто ничего не видел.", + "achiev_title_secret_opsec": "Операционная безопасность", + "achiev_hint_secret_opsec": "Ты установил opsec привилегированным способом: sudo packit install opsec.", "tags_section_title": "Теги", "apply_button": "Применить", "authors_section_title": "Авторы", @@ -1136,8 +1130,6 @@ "role_founder": "Основатель", "achiev_title_secret_connect_is_bullshit": "Все мы ненавидим Connect", "achiev_hint_secret_connect_is_bullshit": "Рисуют богиню Уми Асанаги не зная анатомию человека... Гореть им в аду.", - "achiev_title_secret_opsec": "Операционная безопасность", - "achiev_hint_secret_opsec": "Ты установил opsec привилегированным способом: sudo packit install opsec.", "pp_closed_source_moderated": "Закрытый исходный код, но с модерацией, поэтому вы в безопасности.", "beta_build_title": "Бета-сборка", "beta_build_msg": "Вы не тестировщик, вам не следовало устанавливать PackIt.", @@ -1149,10 +1141,6 @@ "pp_no_license": "Нет лицензии", "pp_languages": "Языки", "pp_clients": "Клиенты", - "pp_closed_source_moderated": "Закрытый исходный код, но с модерацией, поэтому вы в безопасности.", - "beta_build_title": "Бета-сборка", - "beta_build_msg": "Вы не тестировщик, вам не следовало устанавливать PackIt.", - "delete_packit": "Удалить PackIt", "debug_menu": "Меню отладки", "debug_menu_desc": "Меню для отладки", "debug_logs": "Логи отладки", @@ -1183,5 +1171,39 @@ "bi_root_no": "Нет", "bi_app_version": "Версия приложения", "bi_app_package": "Пакет приложения", - "plus_sponsor": "+ Спонсор" + "plus_sponsor": "+ Спонсор", + "repo_card_status_ok": "Обновлён", + "repo_card_status_stale": "Кэш устарел", + "repo_card_status_missing": "Не загружен", + "repo_card_status_disabled": "Отключён", + "repo_card_plugins": "{0} плагинов", + "repo_card_icons": "{0} наборов", + "repo_edit": "Изменить", + "repo_copy_link": "Копировать ссылку", + "repos_updating": "Обновление источников…", + "repo_add_sheet_title": "Новый источник", + "repo_add_sheet_subtitle": "Вставьте ссылку на repomap.json", + "repo_add_field_url": "Ссылка", + "repo_sheet_edit_title": "Изменить источник", + "repo_sheet_edit_sub": "Название и ссылка", + "repo_err_empty": "Введите ссылку", + "repo_err_scheme": "Ссылка должна начинаться с https://", + "repo_err_duplicate": "Этот источник уже добавлен", + "repo_err_not_found": "Файл не найден", + "repo_err_forbidden": "Доступ запрещён", + "repo_err_rate_limited": "Слишком много запросов, попробуйте позже", + "repo_err_redirect": "Ссылка перенаправляет — нужна прямая ссылка на файл", + "repo_err_timeout": "Истекло время ожидания", + "repo_err_server": "Ошибка на стороне сервера", + "repo_err_http": "Сервер ответил: {0}", + "repo_err_json": "Файл не является корректным JSON", + "repo_err_meta": "В файле нет блока repometa", + "repo_err_rid": "В repometa нет rm_rid", + "repo_err_name": "В repometa нет rm_name", + "repo_err_cache": "Не удалось сохранить кэш", + "repo_err_network": "Нет соединения", + "repo_err_unknown": "Неизвестная ошибка: {0}", + "repos_empty_title": "Пока пусто", + "repos_empty_text": "Добавьте источник, чтобы ставить плагины", + "retry": "Повторить" } diff --git a/packit/meta.yml b/packit/meta.yml index 9d57fb0..a139668 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.1.1-rel" +version: "0.1.2-dev.1" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/MainActivity.py b/packit/src/MainActivity.py index 253a1b0..456221a 100644 --- a/packit/src/MainActivity.py +++ b/packit/src/MainActivity.py @@ -8,7 +8,6 @@ except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, metainfo failed: {e}") from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from .SettingsActivity.repos import RepositoriesSettings from .SettingsActivity.deeplinks import DeeplinksSettings from .SettingsActivity.settings import OtherSettings from .SettingsActivity.docs import DocumentationSettings @@ -71,7 +70,6 @@ class SettingsBuilder: def __init__(self, repoManager, plugin): self.repoManager = repoManager self.plugin = plugin - self.repositoriesSettings = RepositoriesSettings(repoManager) self.deeplinksSettings = DeeplinksSettings() self.otherSettings = OtherSettings(plugin.chatUI, plugin) self.documentationSettings = DocumentationSettings() @@ -201,6 +199,13 @@ def _check_updates(self, view): logx(f"MainActivity: _check_updates error: {e}", False) + def _open_repositories(self, view): + try: + from .ui.ReposActivity import show_repos_fragment + show_repos_fragment(self.repoManager) + except Exception as e: + logx(f"MainActivity: _open_repositories error: {e}", False) + def _install_icons(self, view): try: install_icons_ui = InstallIconsUI(self.plugin) @@ -290,7 +295,7 @@ def buildMainSettings(self): Text( text=strings.repositories, icon="msg_folders", - create_sub_fragment=self.repositoriesSettings.build, + on_click=self._open_repositories, link_alias="repositories" ), diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 2ae6526..b398016 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -51,6 +51,13 @@ def setRepositories(self, repos): fragment.rebuildAllItems() except Exception: pass + # the sources screen is a plain fragment with no adapter, so + # rebuildAllItems never reaches it — it listens here instead + try: + from .ui.ReposActivity import notify_repos_changed + notify_repos_changed() + except Exception: + pass def _fetch_and_save_repomap(self, url: str) -> dict | None: """Fetch repomap.json from url, save to packit/{rm_rid}.json, return repometa dict.""" diff --git a/packit/src/SettingsActivity/icons.py b/packit/src/SettingsActivity/icons.py deleted file mode 100644 index 31e4d46..0000000 --- a/packit/src/SettingsActivity/icons.py +++ /dev/null @@ -1,233 +0,0 @@ -# pyright: reportMissingImports=false -# SPDX-License-Identifier: GPL-3.0-or-later - -from ui.settings import Header, Text, Divider -from ui.bulletin import BulletinHelper -try: - from org.telegram.messenger import AndroidUtilities -except Exception as e: - import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from android_utils import run_on_ui_thread -from hook_utils import find_class -from client_utils import get_last_fragment -try: - from elyx import strings -except Exception as e: - import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() - - -class IconSelector: - def __init__(self, repoManager, on_icon_selected_callback): - self.repoManager = repoManager - self.on_icon_selected_callback = on_icon_selected_callback - - def _refresh_settings_page(self): - def action(): - fragment = get_last_fragment() - if fragment and hasattr(fragment, "rebuildAllFragments"): - fragment.rebuildAllFragments(True) - run_on_ui_thread(action) - - def _copy_text(self, text_to_copy: str, message: str): - if AndroidUtilities.addToClipboard(text_to_copy): - BulletinHelper.show_success(message) - - def _select_icon(self, icon_name: str): - self.on_icon_selected_callback(icon_name) - BulletinHelper.show_success(strings.icon_selected.format(icon_name)) - def close_fragment(): - fragment = get_last_fragment() - if fragment and hasattr(fragment, "finishFragment"): - fragment.finishFragment() - run_on_ui_thread(close_fragment) - - def build(self): - try: - icon_categories = { - strings.arrows_navigation: [ - "arrow_more_solar", "msg_go", "msg_go_up", "preview_arrow_left", "preview_arrow_right", - "ic_ab_back", "ic_ab_other", "ic_ab_search", "ic_go" - ], - strings.attachments_media: [ - "attach_audio", "attach_contact", "attach_file", "attach_gallery", "attach_gif", - "attach_location", "attach_poll", "attach_send_solar", "attach_video", "attach_voice", - "msg_attach", "msg_audio", "msg_video", "msg_photo", "msg_gallery", "msg_gif", - "msg_file", "msg_doc", "msg_image", "msg_media" - ], - strings.avatar_profile: [ - "avatar_add", "avatar_delete", "avatar_edit", "msg_photo_add", "msg_photo_crop", - "msg_photo_curve", "msg_photo_text_regular", "msg_photo_text2", "profile_calls", - "profile_newmsg", "profile_settings", "profile_share", "profile_video" - ], - strings.calls_voice: [ - "calls_accept", "calls_decline", "calls_flip", "calls_mute", "calls_videocall", - "msg_call", "msg_videocall", "msg_videoplay", "msg_voice", "msg_voip", - "voip_accept", "voip_decline", "voip_group_add_user", "voip_group_invite", - "voip_group_leave", "voip_group_link", "voip_speaker" - ], - strings.chat_actions: [ - "chats_archive", "chats_delete", "chats_delivered", "chats_error", "chats_markread", - "chats_mute", "chats_pin", "chats_read", "chats_sending", "chats_sent", - "chats_unarchive", "chats_unmute", "chats_unpin", "msg_chat", "msg_group", - "msg_channel", "msg_bot", "msg_user", "msg_contacts" - ], - strings.data_network: [ - "data_network", "data_roaming", "data_wifi", "msg_filled_data_calls", - "msg_filled_data_files", "msg_filled_datausage", "msg_filled_storageusage" - ], - strings.dialog_creation: [ - "dialogs_add", "dialogs_bot", "dialogs_broadcast", "dialogs_contacts", - "dialogs_group", "dialogs_newchannel", "dialogs_newgroup", "dialogs_newsecret", - "dialogs_proxy", "dialogs_search", "dialogs_settings" - ], - strings.fab_actions: [ - "fab_add", "fab_camera", "fab_compose_small_solar", "fab_done", "fab_edit", - "msg_add_file", "msg_addbot", "msg_addfolder", "msg_addphoto" - ], - strings.files_storage: [ - "files_storage", "gift_unpack", "msg_file", "msg_doc", "msg_folders_bots", - "msg_folders_channels", "msg_folders_groups", "msg_folders_requests", - "msg_folder", "msg_removefolder" - ], - strings.input_interface: [ - "ic_attach_document", "ic_attach_gallery", "ic_attach_location", "ic_attach_music", - "ic_attach_poll", "ic_attach_video", "input_bot1", "input_bot1_remix", - "input_clear", "input_emoji", "input_schedule_solar", "input_send", "input_sticker", - "msg_input", "msg_instant", "msg_select", "msg_select_between" - ], - strings.location_maps: [ - "location_current", "location_send", "location_zoom_in", "location_zoom_out", - "msg_location", "msg_map" - ], - strings.menu_items: [ - "menu_account", "menu_add", "menu_archive", "menu_attach", "menu_back", - "menu_block", "menu_broadcast", "menu_calls", "menu_camera", "menu_cancel", - "menu_channel", "menu_chat", "menu_clear", "menu_close", "menu_contacts", - "menu_copy", "menu_create", "menu_crop", "menu_delete", "menu_done", - "menu_download", "menu_edit", "menu_emoji", "menu_end", "menu_exit", - "menu_export", "menu_fave", "menu_feature_premium", "menu_file", "menu_filter", - "menu_flag", "menu_folder", "menu_forward", "menu_gallery", "menu_gif", - "menu_group", "menu_help", "menu_hide", "menu_home", "menu_import", - "menu_info", "menu_intro_solar", "menu_invite", "menu_join", "menu_leave", - "menu_link", "menu_location", "menu_lock", "menu_logout", "menu_love", - "menu_map", "menu_mic", "menu_more", "menu_mute", "menu_new", "menu_next", - "menu_night", "menu_notifications", "menu_open", "menu_pause", "menu_phone", - "menu_photo", "menu_pin", "menu_play", "menu_plus", "menu_poll", - "menu_premium", "menu_premium_clock", "menu_premium_location", "menu_premium_star", - "menu_preview", "menu_previous", "menu_privacy", "menu_profile", "menu_qr", - "menu_question", "menu_quiz", "menu_read", "menu_redo", "menu_refresh", - "menu_remove", "menu_reorder", "menu_repeat", "menu_reply", "menu_report", - "menu_restart", "menu_restore", "menu_rotate", "menu_save", "menu_scan", - "menu_search", "menu_security", "menu_select_quote_solar", "menu_send", - "menu_settings", "menu_share", "menu_silent", "menu_skip", "menu_sort", - "menu_spam", "menu_star", "menu_stats", "menu_sticker", "menu_stop", - "menu_storage", "menu_stories", "menu_switch", "menu_sync", "menu_theme", - "menu_undo", "menu_unmute", "menu_unpin", "menu_unlock", "menu_unread", - "menu_update", "menu_upload", "menu_user", "menu_video", "menu_voice", - "menu_wallet", "menu_warning", "menu_zoom" - ], - strings.message_content: [ - "msg_archive", "msg_autodelete_1d", "msg_autodelete_1m", "msg_autodelete_1w", - "msg_autodelete_badge2", "msg_block", "msg_broadcast", "msg_calendar", - "msg_calendar2", "msg_clock", "msg_code", "msg_colors", "msg_comment", - "msg_copy_filled", "msg_day", "msg_discussion", "msg_draft", "msg_draw", - "msg_font", "msg_games", "msg_gift_premium", "msg_header_draw", - "msg_header_share", "msg_help", "msg_history", "msg_info", "msg_invite", - "msg_join", "msg_leave", "msg_level", "msg_link2", "msg_link_1", "msg_link_2", - "msg_list", "msg_live", "msg_log", "msg_love", "msg_mention", "msg_menu", - "msg_message2", "msg_month", "msg_move", "msg_msgbubble2", "msg_music", - "msg_name", "msg_new", "msg_new_group", "msg_new_private", "msg_new_secret", - "msg_newphone", "msg_news", "msg_panel_forward", "msg_panel_reply", - "msg_payment_provider", "msg_phone", "msg_players", "msg_plugins", - "msg_poll", "msg_premium", "msg_preview", "msg_question", "msg_quote", - "msg_quiz", "msg_rate_down", "msg_reactions_filled", "msg_recent", - "msg_recents", "msg_record", "msg_saved", "msg_scheduled", "msg_secret", - "msg_send", "msg_separated", "msg_settings_art", "msg_settings_ny", - "msg_share", "msg_share_filled", "msg_shareout", "msg_sound", "msg_spam", - "msg_speed", "msg_start", "msg_stats", "msg_sticker", "msg_stories", - "msg_stories_add", "msg_stories_archive", "msg_stories_closefriends", - "msg_stories_my", "msg_stories_stealth", "msg_ton", "msg_topic_create", - "msg_translate", "msg_unarchive", "msg_user", "msg_video", "msg_view", - "msg_wallpaper", "msg_watch", "msg_wave", "msg_work" - ], - strings.message_controls: [ - "msg_check", "msg_check2", "msg_clear", "msg_clear_recent", "msg_close", - "msg_customize", "msg_delete", "msg_delete_solar", "msg_done", "msg_download", - "msg_download_settings", "msg_edit", "msg_emoji", "msg_empty", "msg_error", - "msg_fave", "msg_favorite", "msg_filled_blocked_solar", "msg_filled_menu_channels", - "msg_filled_menu_groups", "msg_filled_menu_users", "msg_filled_shareout", - "msg_filled_storageusage", "msg_filter", "msg_flag", "msg_flash", "msg_flip", - "msg_folder", "msg_folders_bots", "msg_folders_channels", "msg_folders_groups", - "msg_folders_requests", "msg_forward", "msg_gift_premium", "msg_hide", - "msg_image", "msg_input", "msg_instant", "msg_location", "msg_lock", - "msg_map", "msg_media", "msg_mini_autodelete_empty", "msg_mini_customize", - "msg_mute", "msg_night", "msg_night_auto", "msg_no_sound", "msg_notifications", - "msg_online", "msg_open", "msg_openin", "msg_openprofile", "msg_pause", - "msg_pin", "msg_pin_mini", "msg_play", "msg_privacy", "msg_profile", - "msg_proxy", "msg_qrcode", "msg_qrcode_mini", "msg_read", "msg_rear_camera", - "msg_redo", "msg_refresh", "msg_remix", "msg_remove", "msg_removefolder", - "msg_replace", "msg_reply", "msg_reply_small", "msg_report", "msg_restore", - "msg_retry", "msg_rotate", "msg_save", "msg_search", "msg_security", - "msg_select", "msg_select_between", "msg_settings", "msg_silent", - "msg_speed", "msg_star", "msg_status_edit", "msg_status_set", "msg_stop", - "msg_stopwatch", "msg_unmute", "msg_unpin", "msg_unlock", "msg_unvote", - "msg_update", "msg_upload", "msg_videocall", "msg_videoplay", "msg_voice", - "msg_voip", "msg_warning", "msg_zoom" - ], - strings.passcode_security: [ - "passcode_delete", "passcode_fingerprint", "passcode_logo", "ic_block_user", - "ic_lock", "ic_lock_white", "ic_unblock_user" - ], - strings.player_controls: [ - "player_next", "player_pause", "player_play", "player_prev", "player_repeat", - "player_shuffle", "preview_play", "msg_play", "msg_pause", "msg_stop" - ], - strings.stickers: [ - "stickers_add", "stickers_check", "stickers_delete", "stickers_fave", - "stickers_menu", "msg_sticker", "input_sticker" - ], - strings.stories: [ - "stories_circle", "stories_seen", "stories_unseen", "msg_stories", - "msg_stories_add", "msg_stories_archive", "msg_stories_closefriends", - "msg_stories_my", "msg_stories_stealth", "menu_stories" - ], - strings.themes: [ - "theme_auto", "theme_dark", "theme_day", "theme_light", "theme_night", - "msg_night", "msg_night_auto", "msg_brightness_high", "msg_brightness_low" - ], - strings.ui_elements: [ - "tooltip_arrow", "undo_redo", "undo_undo", "window_close", "ic_comment", - "ic_delete", "ic_done", "ic_menu_more", "ic_mute", "ic_notifications", - "ic_pin", "ic_send", "ic_unmute" - ] - } - - try: - R_drawable = find_class("org.telegram.messenger.R$drawable") - def filter_icons(icon_list): - return [icon for icon in icon_list if getattr(R_drawable, icon, 0) != 0] - except Exception: - def filter_icons(icon_list): - return icon_list - - settings_list = [] - - for category_name, category_icons in icon_categories.items(): - filtered_icons = filter_icons(category_icons) - if filtered_icons: - settings_list.append(Header(text=category_name)) - for icon_name in filtered_icons: - settings_list.append(Text( - text=icon_name, - icon=icon_name, - on_click=lambda view, name=icon_name: self._select_icon(name) - )) - settings_list.append(Divider()) - - settings_list.append(Divider()) - return settings_list - - except Exception as e: - return [Header(text=strings.error_header), Text(text=strings.failed_to_load_icons.format(e))] \ No newline at end of file diff --git a/packit/src/SettingsActivity/repos.py b/packit/src/SettingsActivity/repos.py deleted file mode 100644 index 2a7e846..0000000 --- a/packit/src/SettingsActivity/repos.py +++ /dev/null @@ -1,735 +0,0 @@ -# pyright: reportMissingImports=false -# SPDX-License-Identifier: GPL-3.0-or-later - -from packutil import logx -from ..utils.bulletins import factory as _pbf -from ui.settings import Header, Input, Divider, Switch, Text -try: - from elyx import strings, settings -except Exception as e: - import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from client_utils import get_last_fragment -from ui.bulletin import BulletinHelper -from ui.alert import AlertDialogBuilder -from .icons import IconSelector - -from hook_utils import find_class -try: - from org.telegram.messenger import R as R_tg -except Exception as e: - import android_utils as _au; _au.log(f"import org.telegram.messenger import R as R_tg failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() - -BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") - - -def _showAddRepoDialog(context, repoManager): - try: - from android.text import InputType - from android.content import DialogInterface - from android.view import View - from android.widget import ScrollView, LinearLayout, TextView, FrameLayout, ImageView - from android.view import Gravity - from android.util import TypedValue - from java import dynamic_proxy - from org.telegram.ui.ActionBar import AlertDialog, Theme - from org.telegram.ui.Components import EditTextBoldCursor, OutlineTextContainerView, RLottieImageView, LayoutHelper, CircularProgressDrawable - from org.telegram.messenger import AndroidUtilities - from client_utils import run_on_queue - from android_utils import run_on_ui_thread, OnClickListener - - dp = AndroidUtilities.dp - - builder = AlertDialog.Builder(context) - - frameLayout = FrameLayout(context) - builder.setView(frameLayout) - - scrollView = ScrollView(context) - scrollView.setFillViewport(True) - frameLayout.addView(scrollView, LayoutHelper.createFrame(-1, -1)) - - linear = LinearLayout(context) - linear.setOrientation(LinearLayout.VERTICAL) - linear.setGravity(Gravity.CENTER_HORIZONTAL) - scrollView.addView(linear, LayoutHelper.createFrame(-1, -2, Gravity.TOP)) - - try: - anim = RLottieImageView(context) - anim.setAnimation(R_tg.raw.shared_link_enter, 100, 100) - anim.playAnimation() - linear.addView(anim, LayoutHelper.createLinear(100, 100, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0)) - except Exception as e: - logx(f"repos: add repo dialog anim error: {e}", False) - - titleView = TextView(context) - titleView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)) - titleView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18) - titleView.setGravity(Gravity.CENTER_HORIZONTAL) - titleView.setTypeface(AndroidUtilities.bold()) - titleView.setText(str(strings.add_repository)) - linear.addView(titleView, LayoutHelper.createFrame(-2, -2, Gravity.CENTER_HORIZONTAL, 24, 8, 24, 0)) - - outlineView = OutlineTextContainerView(context) - outlineView.setText(str(strings.repo_url)) - outlineView.animateSelection(1, False) - linear.addView(outlineView, LayoutHelper.createLinear(-1, -2, Gravity.CENTER_HORIZONTAL, 24, 24, 24, 16)) - - inputField = EditTextBoldCursor(context) - inputField.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 18) - inputField.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)) - inputField.setHintTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteHintText)) - inputField.setBackground(None) - inputField.setSingleLine(True) - inputField.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI) - inputField.setCursorColor(Theme.getColor(Theme.key_windowBackgroundWhiteInputFieldActivated)) - inputField.setCursorWidth(1.5) - padding = dp(16) - inputField.setPadding(padding, padding, padding, padding) - outlineView.addView(inputField, LayoutHelper.createFrame(-1, -2)) - outlineView.attachEditText(inputField) - - class _FocusListener(dynamic_proxy(View.OnFocusChangeListener)): - def onFocusChange(self, v, hasFocus): - outlineView.animateSelection(1 if hasFocus else 0) - - inputField.setOnFocusChangeListener(_FocusListener()) - - # button: LinearLayout with TextView inside, same pattern as installUi details button - doneBtn = LinearLayout(context) - doneBtn.setOrientation(LinearLayout.HORIZONTAL) - doneBtn.setGravity(Gravity.CENTER) - doneBtn.setBackground(Theme.createSimpleSelectorRoundRectDrawable( - dp(6), - Theme.getColor(Theme.key_featuredStickers_addButton), - Theme.getColor(Theme.key_featuredStickers_addButtonPressed) - )) - doneBtn.setClickable(True) - doneBtn.setFocusable(True) - - btnLabel = TextView(context) - btnLabel.setText(str(strings.add_repository)) - btnLabel.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16) - btnLabel.setGravity(Gravity.CENTER) - btnLabel.setTextColor(Theme.getColor(Theme.key_featuredStickers_buttonText)) - doneBtn.addView(btnLabel, LayoutHelper.createLinear(-2, -2, Gravity.CENTER)) - - linear.addView(doneBtn, LayoutHelper.createFrame(-1, 44, Gravity.TOP, 30, 0, 30, 16)) - - dialog = builder.create() - - def _setLoading(isLoading): - try: - doneBtn.setEnabled(not isLoading) - doneBtn.removeAllViews() - if isLoading: - color = Theme.getColor(Theme.key_featuredStickers_buttonText) - spinnerDrawable = CircularProgressDrawable(color) - try: - spinnerDrawable.size = float(dp(20)) - spinnerDrawable.thickness = float(dp(2)) - except Exception: - pass - spinnerView = ImageView(context) - spinnerView.setImageDrawable(spinnerDrawable) - spinnerView.setScaleType(ImageView.ScaleType.CENTER) - doneBtn.addView(spinnerView, LayoutHelper.createLinear(20, 20, Gravity.CENTER)) - else: - doneBtn.addView(btnLabel, LayoutHelper.createLinear(-2, -2, Gravity.CENTER)) - except Exception as e: - logx(f"repos: _setLoading error: {e}", False) - - def onAdd(): - url = str(inputField.getText()).strip() - if not url: - return - AndroidUtilities.hideKeyboard(inputField) - run_on_ui_thread(lambda: _setLoading(True)) - - def task(): - repometa, reason = repoManager.addRepositoryWithUrl(url) - - def onDone(): - dialog.dismiss() - if reason is not None: - try: - BulletinHelper.show_error(f"{strings.invalid_repository}: {reason}") - except Exception as e: - logx(f"repos: add repo error bulletin error: {e}", False) - else: - try: - frag = get_last_fragment() - container = frag.getParentActivity().getWindow().getDecorView() - resourceProvider = frag.getResourceProvider() - _pbf(container, resourceProvider).createSimpleBulletin( - R_tg.raw.shared_link_enter, - str(strings.repository_added) - ).show() - if frag and hasattr(frag, "rebuildAllItems"): - frag.rebuildAllItems() - except Exception as e: - logx(f"repos: add repo success bulletin error: {e}", False) - - run_on_ui_thread(onDone) - - run_on_queue(task) - - doneBtn.setOnClickListener(OnClickListener(lambda v: onAdd())) - - class _DismissListener(dynamic_proxy(DialogInterface.OnDismissListener)): - def onDismiss(self, d): - AndroidUtilities.hideKeyboard(inputField) - - class _ShowListener(dynamic_proxy(DialogInterface.OnShowListener)): - def onShow(self, d): - inputField.requestFocus() - AndroidUtilities.showKeyboard(inputField) - - dialog.setOnDismissListener(_DismissListener()) - dialog.setOnShowListener(_ShowListener()) - dialog.show() - except Exception as e: - logx(f"repos: _showAddRepoDialog error: {e}", False) - - -class RepositoriesSettings: - def __init__(self, repoManager): - self.repoManager = repoManager - - def build(self): - repos = self.repoManager.getRepositories() - - if not repos: - self.repoManager.addRepository(isFirst=True) - repos = self.repoManager.getRepositories() - try: - fragment = get_last_fragment() - if fragment and hasattr(fragment, "rebuildAllItems"): - fragment.rebuildAllItems() - except Exception as e: - logx(f"{e}", False) - - def add_new_repository(view): - repos = self.repoManager.getRepositories() - if len(repos) >= 10: - try: - BulletinHelper.show_error(strings.max_repositories_allowed) - except Exception as e: - logx(f"{e}", False) - return - - try: - frag = get_last_fragment() - ctx = frag.getParentActivity() if frag else None - if not ctx: - return - _showAddRepoDialog(ctx, self.repoManager) - except Exception as e: - logx(f"repos: add_new_repository error: {e}", False) - - def restore_default_repository(view): - repos = self.repoManager.getRepositories() - if len(repos) >= 10: - try: - logx("Default repository restore failed: max limit reached (10)", True) - BulletinHelper.show_error(strings.max_repositories_allowed) - except Exception as e: - logx(f"{e}", False) - return - - self.repoManager.restoreDefaultRepository() - try: - BulletinHelper.show_success(strings.default_repo_restored) - except Exception as e: - logx(f"{e}", False) - - def reset_repositories(view): - repos = self.repoManager.getRepositories() - if len(repos) <= 1: - try: - frag = get_last_fragment() - act = frag.getParentActivity() if frag else None - if not act: - return - - builder = AlertDialogBuilder(act) - builder.set_title(strings.easter_egg_title) - builder.set_message(strings.easter_egg_reset_message) - builder.set_positive_button(strings.close_button, lambda b, w: b.dismiss()) - builder.show() - except Exception as e: - logx(f"{e}", False) - return - - try: - frag = get_last_fragment() - act = frag.getParentActivity() if frag else None - if not act: - return - - builder = AlertDialogBuilder(act) - builder.set_title(strings.reset_repositories_title) - builder.set_message(strings.reset_repositories_message) - - def on_yes(b, w): - self.repoManager.resetRepositories() - try: - frag = get_last_fragment() - container = frag.getParentActivity().getWindow().getDecorView() - resourceProvider = frag.getResourceProvider() - _pbf(container, resourceProvider).createSimpleBulletin(R_tg.raw.group_pip_delete_icon, strings.repositories_reset).show() - except Exception as e: - logx(f"{e}", False) - - builder.set_positive_button(strings.reset_button, on_yes) - builder.set_negative_button(strings.close_button, lambda b, w: b.dismiss()) - try: - builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) - except Exception as e: - logx(f"{e}", False) - builder.show() - except Exception as e: - logx(f"{e}", False) - - def clear_all_except_first(view): - repos = self.repoManager.getRepositories() - if len(repos) <= 1: - try: - frag = get_last_fragment() - act = frag.getParentActivity() if frag else None - if not act: - return - - builder = AlertDialogBuilder(act) - builder.set_title(strings.easter_egg_title) - builder.set_message(strings.easter_egg_clear_message) - builder.set_positive_button(strings.close_button, lambda b, w: b.dismiss()) - builder.show() - except Exception as e: - logx(f"{e}", False) - return - - try: - frag = get_last_fragment() - act = frag.getParentActivity() if frag else None - if not act: - return - - builder = AlertDialogBuilder(act) - builder.set_title(strings.clear_all_title) - builder.set_message(strings.clear_all_message) - - def on_yes(b, w): - self.repoManager.clearAllExceptFirst() - try: - frag = get_last_fragment() - container = frag.getParentActivity().getWindow().getDecorView() - resourceProvider = frag.getResourceProvider() - _pbf(container, resourceProvider).createSimpleBulletin(R_tg.raw.utyan_cache, strings.repositories_cleared).show() - except Exception as e: - logx(f"{e}", False) - - builder.set_positive_button(strings.clear_button, on_yes) - builder.set_negative_button(strings.close_button, lambda b, w: b.dismiss()) - try: - builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) - except Exception as e: - logx(f"{e}", False) - builder.show() - except Exception as e: - logx(f"{e}", False) - - def update_repositories(view): - from client_utils import run_on_queue - from android_utils import run_on_ui_thread - import requests - import json - import os - try: - from org.telegram.messenger import ApplicationLoader - except Exception as e: - import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() - - def task(): - try: - repos = self.repoManager.getRepositories() - from ..utils.paths import getReposCacheDir - cache_dir = getReposCacheDir() - os.makedirs(cache_dir, exist_ok=True) - changed = False - to_remove = [] - seen_rids = set() - - for i, repo in enumerate(repos): - url = (repo.get("url") or "").strip() - if not url: - continue - try: - r = requests.get(url, timeout=10) - if r.status_code != 200: - logx(f"update_repositories: HTTP {r.status_code} for {url}", True) - continue - data = r.json() - repometa = data.get("repometa") - rm_rid = repometa.get("rm_rid") if repometa else None - - if not repometa or not rm_rid: - logx(f"update_repositories: no repometa for '{url}', removing repo", True) - to_remove.append(i) - changed = True - continue - - if rm_rid in seen_rids: - logx(f"update_repositories: duplicate rm_rid='{rm_rid}', removing repo", True) - to_remove.append(i) - changed = True - continue - seen_rids.add(rm_rid) - - if repo.get("id") != rm_rid: - repos[i]["id"] = rm_rid - changed = True - logx(f"update_repositories: set id='{rm_rid}' for repo '{repo.get('name')}'", True) - - cache_path = os.path.join(cache_dir, f"{rm_rid}.json") - with open(cache_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - logx(f"update_repositories: updated cache for '{rm_rid}'", True) - except Exception as e: - logx(f"update_repositories: error for {url}: {e}", False) - - for i in sorted(to_remove, reverse=True): - repos.pop(i) - - if changed: - self.repoManager.setRepositories(repos) - - run_on_ui_thread(lambda: BulletinHelper.show_success(strings.update_repos_success)) - except Exception as e: - logx(f"update_repositories: task error: {e}", False) - - run_on_queue(task) - - isActionsCollapsed = settings.get("actions_collapsed", True) - actionsCollapseIcon = "msg_go_up" if not isActionsCollapsed else "arrow_more_solar" - - def toggle_actions_collapsed(view): - current = settings.get("actions_collapsed", True) - settings.set("actions_collapsed", not current, reload_settings=True) - - def export_repositories(view): - repos = self.repoManager.getRepositories() - links = [] - for repo in repos: - name = repo.get('name', '').strip() - url = repo.get('url', '').strip() - icon = repo.get('icon', '').strip() - if not url: - continue - links.append(f"tg://packit?repo=add&name={name}&link={url}&icon={icon}") - - if not links: - BulletinHelper.show_error(strings.no_repositories_to_export) - return - - share_text = "\n\n".join(links) - - try: - from java import jclass, dynamic_proxy - from hook_utils import find_class - from android_utils import run_on_ui_thread - - frag = get_last_fragment() - if not frag: - return - act = frag.getParentActivity() - if not act: - return - - ShareAlert = find_class("org.telegram.ui.Components.ShareAlert") - ShareDelegateClass = jclass("org.telegram.ui.Components.ShareAlert$ShareAlertDelegate") - _fragment = frag - - class ShareDelegate(dynamic_proxy(ShareDelegateClass)): - def __init__(self): - super().__init__() - - def didShare(self): - def _show_bulletin(): - try: - from org.telegram.messenger import R as R_tg - BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") - container = _fragment.getParentActivity().getWindow().getDecorView() - rp = _fragment.getResourceProvider() - _pbf(container, rp).createSimpleBulletin(R_tg.raw.voip_invite, strings.repositories_exported).show() - except Exception as _be: - logx(f"repos.export_repositories.ShareDelegate.didShare: {_be}", True) - run_on_ui_thread(_show_bulletin) - - def didCopy(self): - return False - - share_alert = ShareAlert( - act, - None, - share_text, - True, - share_text, - False - ) - share_alert.setDelegate(ShareDelegate()) - frag.showDialog(share_alert) - except Exception as e: - logx(f"Export failed: {e}", False) - BulletinHelper.show_error(strings.failed_to_copy) - - def toggle_all_repositories(view): - repos = self.repoManager.getRepositories() - anyEnabled = any(r.get('enabled', True) for r in repos) - for repo in repos: - repo['enabled'] = not anyEnabled - self.repoManager.setRepositories(repos) - - anyEnabled = any(r.get('enabled', True) for r in repos) - toggleAllText = strings.disable_all_repositories if anyEnabled else strings.enable_all_repositories - - actionItems = [ - Text( - text=strings.update_repositories, - icon="msg_retry", - on_click=update_repositories, - link_alias="update_repos" - ), - Text( - text=strings.export_repositories, - icon="msg_share", - on_click=export_repositories, - link_alias="export_repos" - ), - Text( - text=toggleAllText, - icon="msg_customize", - on_click=toggle_all_repositories, - link_alias="toggle_all_repos" - ), - Text( - text=strings.restore_default_repository, - icon="msg_reset", - on_click=restore_default_repository, - link_alias="restore_repo" - ), - Text( - text=strings.clear_all_except_first, - icon="msg_clear", - red=True, - on_click=clear_all_except_first, - link_alias="clear_all" - ), - Text( - text=strings.reset_repositories, - icon="msg_delete", - red=True, - on_click=reset_repositories, - link_alias="reset_repo" - ), - ] - - settingsList = [ - Header(text=strings.repositories), - Text( - text=strings.add_repository, - icon="msg_add", - accent=True, - on_click=add_new_repository, - link_alias="new_repo" - ), - Text( - text=strings.additional_actions, - icon=actionsCollapseIcon, - accent=True, - on_click=toggle_actions_collapsed - ), - *(actionItems if not isActionsCollapsed else []), - Divider() - ] - - def makeOnChange(field, i): - return lambda value: self.repoManager.updateRepoField(i, field, value) - - def makeOnRemove(i): - def show_confirm_dialog(view): - try: - frag = get_last_fragment() - act = frag.getParentActivity() if frag else None - if not act: - return - - builder = AlertDialogBuilder(act) - builder.set_title(strings.delete_repository_title) - builder.set_message(strings.delete_repository_message) - - def on_yes(b, w): - self.repoManager.removeRepository(i) - - builder.set_positive_button(strings.delete_button, on_yes) - builder.set_negative_button(strings.close_button, lambda b, w: b.dismiss()) - try: - builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) - except Exception as e: - logx(f"{e}", False) - builder.show() - except Exception as e: - logx(f"{e}", False) - self.repoManager.removeRepository(i) - - return show_confirm_dialog - - def makeOnShare(i): - def share_repository(view): - current_repos = self.repoManager.getRepositories() - if i >= len(current_repos): - BulletinHelper.show_error(strings.failed_to_copy) - return - repo = current_repos[i] - name = repo.get('name', '').strip() - url = repo.get('url', '').strip() - icon = repo.get('icon', '').strip() - - share_url = f"tg://packit?repo=add&name={name}&link={url}&icon={icon}" - - try: - from java import jclass, dynamic_proxy - from android_utils import run_on_ui_thread - - frag = get_last_fragment() - if not frag: - return - act = frag.getParentActivity() - if not act: - return - - ShareAlert = find_class("org.telegram.ui.Components.ShareAlert") - ShareDelegateClass = jclass("org.telegram.ui.Components.ShareAlert$ShareAlertDelegate") - _fragment = frag - - class ShareDelegate(dynamic_proxy(ShareDelegateClass)): - def __init__(self): - super().__init__() - - def didShare(self): - def _show_bulletin(): - try: - from org.telegram.messenger import R as R_tg - BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") - container = _fragment.getParentActivity().getWindow().getDecorView() - rp = _fragment.getResourceProvider() - _pbf(container, rp).createSimpleBulletin(R_tg.raw.voip_invite, strings.repo_link_copied).show() - except Exception as _be: - logx(f"repos.ShareDelegate.didShare: {_be}", True) - run_on_ui_thread(_show_bulletin) - - def didCopy(self): - return False - - share_alert = ShareAlert( - act, - None, - share_url, - True, - share_url, - False - ) - share_alert.setDelegate(ShareDelegate()) - frag.showDialog(share_alert) - except Exception as e: - logx(f"Share failed: {e}", False) - BulletinHelper.show_error(strings.failed_to_copy) - - return share_repository - - def makeOnToggleCollapse(i): - def toggle(view): - repos = self.repoManager.getRepositories() - if i < len(repos): - repos[i]['collapsed'] = not repos[i].get('collapsed', False) - self.repoManager.setRepositories(repos) - return toggle - - def makeOnSelectIcon(i): - def open_icon_selector(): - def on_icon_selected(icon_name): - self.repoManager.updateRepoField(i, 'icon', icon_name) - - icon_selector = IconSelector(self.repoManager, on_icon_selected) - settings_list = icon_selector.build() - return settings_list - - return open_icon_selector - - for idx, repo in enumerate(repos): - isCollapsed = repo.get("collapsed", False) - isEnabled = repo.get("enabled", True) - collapseIcon = "msg_go_up" if not isCollapsed else "arrow_more_solar" - headerText = strings.repository_form.format(idx + 1) - settingsList.append(Text( - text=headerText, - icon=collapseIcon, - accent=isEnabled, - on_click=makeOnToggleCollapse(idx) - )) - - if not isCollapsed: - current_icon = repo.get('icon', '') - icon_text = strings.repo_icon_text.format(current_icon) if current_icon else strings.repo_icon_not_selected - key_suffix = repo['id'] if repo.get('id') else f"idx_{idx}" - settingsList.extend([ - Switch( - key=f"repo_enabled_{key_suffix}", - text=strings.repo_enabled, - default=repo.get("enabled", True), - icon="msg_customize", - on_change=makeOnChange("enabled", idx) - ), - Input( - key=f"repo_name_{key_suffix}", - text=strings.repo_name, - default=repo.get("name", ""), - icon="msg_edit", - on_change=makeOnChange("name", idx) - ), - Input( - key=f"repo_url_{key_suffix}", - text=strings.repo_url, - default=repo.get("url", ""), - icon="msg_link", - on_change=makeOnChange("url", idx) - ), - Text( - text=icon_text, - icon="msg_folders", - create_sub_fragment=makeOnSelectIcon(idx) - ) - ]) - - settingsList.extend([ - Text( - text=strings.share_repository, - icon="msg_share", - accent=True, - on_click=makeOnShare(idx) - ) - ]) - - if len(repos) > 1: - settingsList.append(Text( - text=strings.remove_repository, - icon="msg_filled_blocked_solar", - red=True, - on_click=makeOnRemove(idx) - )) - - settingsList.append(Divider()) - - return settingsList \ No newline at end of file diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index 015d3b1..a176ed9 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -160,15 +160,20 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, icon, repoManager): linear.setOrientation(LinearLayout.VERTICAL) frame.addView(linear) - # icon centered + # icon centered — rm_icon is an image url in current repomaps and a + # R.drawable name in older ones, so both have to work here try: icon_view = ImageView(act) - icon_id = getattr(R_tg.drawable, rm_icon, 0) - if not icon_id: - icon_id = getattr(R_tg.drawable, "msg_folders", 0) - if icon_id: - icon_view.setImageResource(icon_id) - icon_view.setColorFilter(Theme.getColor(Theme.key_featuredStickers_addButton)) + if str(rm_icon).lower().startswith(("http://", "https://")): + from ..ui.ReposActivity.repoIcon import load_url_into + load_url_into(icon_view, rm_icon, 48) + else: + icon_id = getattr(R_tg.drawable, rm_icon, 0) + if not icon_id: + icon_id = getattr(R_tg.drawable, "msg_folders", 0) + if icon_id: + icon_view.setImageResource(icon_id) + icon_view.setColorFilter(Theme.getColor(Theme.key_featuredStickers_addButton)) linear.addView(icon_view, LayoutHelper.createLinear(48, 48, Gravity.CENTER_HORIZONTAL, 0, 20, 0, 0)) except Exception as e: logx(f"repo deeplink: icon error: {e}", False) diff --git a/packit/src/ui/ReposActivity/__init__.py b/packit/src/ui/ReposActivity/__init__.py new file mode 100644 index 0000000..1c477c0 --- /dev/null +++ b/packit/src/ui/ReposActivity/__init__.py @@ -0,0 +1,40 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Live screens listen here so a repository list changed anywhere else — a +# deeplink adding a source, the startup cache refresh dropping a dead one — +# repaints the open screen. RepositoryManager.setRepositories used to poke +# fragment.rebuildAllItems(), which only ever worked for the settings-list +# screen this one replaces. + +_delegates = [] + + +def register(delegate): + if delegate not in _delegates: + _delegates.append(delegate) + + +def unregister(delegate): + try: + _delegates.remove(delegate) + except ValueError: + pass + + +def notify_repos_changed(): + if not _delegates: + return + from android_utils import run_on_ui_thread + for delegate in list(_delegates): + try: + run_on_ui_thread(delegate.reload) + except Exception: + pass + + +def show_repos_fragment(repoManager): + # imported lazily: the fragment pulls in a good chunk of the ui package and + # nothing needs it until the row is actually tapped + from .fragment import show_repos_fragment as _show + return _show(repoManager) diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/ReposActivity/actions.py new file mode 100644 index 0000000..0d55078 --- /dev/null +++ b/packit/src/ui/ReposActivity/actions.py @@ -0,0 +1,310 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Everything the Sources screen can do to a repository. +# +# The behaviour is the one the settings-list screen had — same confirmations, +# same bulletins, same easter eggs — only the entry points moved: per-card +# actions into the card's overflow menu, the bulk ones into the menu behind the +# summary row. The one cleanup: the screen no longer carries its own copy of +# RepositoryManager.updateAllCaches. + +from packutil import logx + +from android_utils import run_on_ui_thread +from client_utils import get_last_fragment +from hook_utils import find_class + +try: + from elyx import strings +except Exception as e: + import android_utils as _au; _au.log(f"repos actions: import elyx strings failed: {e}") +try: + from ui.alert import AlertDialogBuilder + from ui.bulletin import BulletinHelper +except Exception as e: + import android_utils as _au; _au.log(f"repos actions: import ui helpers failed: {e}") +try: + from org.telegram.messenger import R as R_tg +except Exception as e: + import android_utils as _au; _au.log(f"repos actions: import R failed: {e}") + +from ...utils.bulletins import factory as _pbf +from ..contextMenu import show_plugin_context_menu +from . import notify_repos_changed + + +def _bulletin(raw_name: str, text): + try: + frag = get_last_fragment() + container = frag.getParentActivity().getWindow().getDecorView() + rp = frag.getResourceProvider() + _pbf(container, rp).createSimpleBulletin(getattr(R_tg.raw, raw_name), str(text)).show() + except Exception as e: + logx(f"repos actions: bulletin error: {e}", True) + + +def open_url(act, url: str): + if not url: + return + try: + from android.net import Uri + from org.telegram.messenger.browser import Browser + Browser.openUrl(act, Uri.parse(url)) + except Exception as e: + logx(f"repos actions: open_url error: {e}", False) + try: + from android.content import Intent + from android.net import Uri + act.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(url))) + except Exception as e2: + logx(f"repos actions: open_url fallback error: {e2}", False) + + +def copy_link(repo: dict): + url = str(repo.get("url") or "").strip() + try: + from org.telegram.messenger import AndroidUtilities + if url and AndroidUtilities.addToClipboard(url): + _bulletin("voip_invite", strings.repo_link_copied) + return + except Exception as e: + logx(f"repos actions: copy_link error: {e}", False) + BulletinHelper.show_error(str(strings.failed_to_copy)) + + +def share_repository(act, repo: dict): + # the deeplink the other client will resolve back into a repository + from urllib.parse import quote + name = quote(str(repo.get("name") or "").strip(), safe="") + url = quote(str(repo.get("url") or "").strip(), safe="") + icon = quote(str(repo.get("icon") or "").strip(), safe="") + share_url = f"tg://packit?repo=add&name={name}&link={url}&icon={icon}" + try: + from java import jclass, dynamic_proxy + frag = get_last_fragment() + if not frag or not act: + return + ShareAlert = find_class("org.telegram.ui.Components.ShareAlert") + ShareDelegateClass = jclass("org.telegram.ui.Components.ShareAlert$ShareAlertDelegate") + + class ShareDelegate(dynamic_proxy(ShareDelegateClass)): + def __init__(self): + super().__init__() + + def didShare(self): + run_on_ui_thread(lambda: _bulletin("voip_invite", strings.repo_link_copied)) + + def didCopy(self): + return False + + alert = ShareAlert(act, None, share_url, True, share_url, False) + alert.setDelegate(ShareDelegate()) + frag.showDialog(alert) + except Exception as e: + logx(f"repos actions: share error: {e}", False) + BulletinHelper.show_error(str(strings.failed_to_copy)) + + +def delete_repository(act, delegate, repo: dict): + try: + builder = AlertDialogBuilder(act) + builder.set_title(str(strings.delete_repository_title)) + builder.set_message(str(strings.delete_repository_message)) + + def on_yes(b, w): + idx, _ = delegate._index_of(repo) + if idx < 0: + delegate.reload() + return + delegate.repoManager.removeRepository(idx) + delegate.reload() + + builder.set_positive_button(str(strings.delete_button), on_yes) + builder.set_negative_button(str(strings.close_button), lambda b, w: b.dismiss()) + try: + builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) + except Exception as e: + logx(f"repos actions: red button error: {e}", True) + builder.show() + except Exception as e: + logx(f"repos actions: delete error: {e}", False) + + +def refresh_all(delegate): + _bulletin("shared_link_enter", strings.repos_updating) + + def _done(): + run_on_ui_thread(lambda: (_bulletin("shared_link_enter", strings.update_repos_success), + delegate.reload())) + + try: + delegate.repoManager.updateAllCaches(on_complete=_done) + except Exception as e: + logx(f"repos actions: refresh_all error: {e}", False) + + +def export_repositories(act, delegate): + repos = delegate.repoManager.getRepositories() + from urllib.parse import quote + links = [] + for repo in repos: + url = str(repo.get("url") or "").strip() + if not url: + continue + name = quote(str(repo.get("name") or "").strip(), safe="") + icon = quote(str(repo.get("icon") or "").strip(), safe="") + links.append(f"tg://packit?repo=add&name={name}&link={quote(url, safe='')}&icon={icon}") + if not links: + BulletinHelper.show_error(str(strings.no_repositories_to_export)) + return + text = "\n\n".join(links) + try: + from org.telegram.messenger import AndroidUtilities + if AndroidUtilities.addToClipboard(text): + _bulletin("voip_invite", strings.repositories_exported) + return + except Exception as e: + logx(f"repos actions: export error: {e}", False) + BulletinHelper.show_error(str(strings.failed_to_copy)) + + +def toggle_all(delegate): + repos = delegate.repoManager.getRepositories() + if not repos: + return + target = not any(r.get("enabled", True) for r in repos) + for repo in repos: + repo["enabled"] = target + delegate.repoManager.setRepositories(repos) + delegate.reload() + + +def restore_default(delegate): + repos = delegate.repoManager.getRepositories() + if len(repos) >= 10: + BulletinHelper.show_error(str(strings.max_repositories_allowed)) + return + delegate.repoManager.restoreDefaultRepository() + BulletinHelper.show_success(str(strings.default_repo_restored)) + delegate.reload() + + +def _easter_egg(act, message): + try: + builder = AlertDialogBuilder(act) + builder.set_title(str(strings.easter_egg_title)) + builder.set_message(str(message)) + builder.set_positive_button(str(strings.close_button), lambda b, w: b.dismiss()) + builder.show() + except Exception as e: + logx(f"repos actions: easter egg error: {e}", False) + + +def clear_all_except_first(act, delegate): + repos = delegate.repoManager.getRepositories() + if len(repos) <= 1: + _easter_egg(act, strings.easter_egg_clear_message) + return + try: + builder = AlertDialogBuilder(act) + builder.set_title(str(strings.clear_all_title)) + builder.set_message(str(strings.clear_all_message)) + + def on_yes(b, w): + delegate.repoManager.clearAllExceptFirst() + _bulletin("group_pip_delete_icon", strings.repositories_cleared) + delegate.reload() + + builder.set_positive_button(str(strings.clear_button), on_yes) + builder.set_negative_button(str(strings.close_button), lambda b, w: b.dismiss()) + try: + builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) + except Exception: + pass + builder.show() + except Exception as e: + logx(f"repos actions: clear all error: {e}", False) + + +def reset_repositories(act, delegate): + repos = delegate.repoManager.getRepositories() + if len(repos) <= 1: + _easter_egg(act, strings.easter_egg_reset_message) + return + try: + builder = AlertDialogBuilder(act) + builder.set_title(str(strings.reset_repositories_title)) + builder.set_message(str(strings.reset_repositories_message)) + + def on_yes(b, w): + delegate.repoManager.resetRepositories() + _bulletin("group_pip_delete_icon", strings.repositories_reset) + delegate.reload() + + builder.set_positive_button(str(strings.reset_button), on_yes) + builder.set_negative_button(str(strings.close_button), lambda b, w: b.dismiss()) + try: + builder.make_button_red(AlertDialogBuilder.BUTTON_POSITIVE) + except Exception: + pass + builder.show() + except Exception as e: + logx(f"repos actions: reset error: {e}", False) + + +def add_repository(act, delegate): + repos = delegate.repoManager.getRepositories() + if len(repos) >= 10: + BulletinHelper.show_error(str(strings.max_repositories_allowed)) + return + from .addSheet import show_add_repo_dialog + show_add_repo_dialog(act, delegate) + + +def edit_repository(act, delegate, repo: dict): + from .addSheet import show_edit_repo_dialog + show_edit_repo_dialog(act, delegate, repo) + + +def show_card_menu(act, delegate, repo: dict, anchor): + repos = delegate.repoManager.getRepositories() + items = [ + {"icon": "msg_edit", "text": str(strings.repo_edit), + "action": lambda: edit_repository(act, delegate, repo)}, + {"icon": "msg_copy", "text": str(strings.repo_copy_link), + "action": lambda: copy_link(repo)}, + {"icon": "msg_share", "text": str(strings.share_repository), + "action": lambda: share_repository(act, repo)}, + {"icon": "msg_delete", "text": str(strings.remove_repository), "red": True, + "show": len(repos) > 1, + "action": lambda: delete_repository(act, delegate, repo)}, + ] + try: + show_plugin_context_menu(anchor.getRootView(), anchor, items) + except Exception as e: + logx(f"repos actions: card menu error: {e}", False) + + +def show_bulk_menu(act, delegate, anchor): + repos = delegate.repoManager.getRepositories() + any_enabled = any(r.get("enabled", True) for r in repos) + items = [ + {"icon": "msg_retry", "text": str(strings.update_repositories), + "action": lambda: refresh_all(delegate)}, + {"icon": "msg_share", "text": str(strings.export_repositories), + "action": lambda: export_repositories(act, delegate)}, + {"icon": "msg_customize", + "text": str(strings.disable_all_repositories if any_enabled else strings.enable_all_repositories), + "action": lambda: toggle_all(delegate)}, + {"icon": "msg_reset", "text": str(strings.restore_default_repository), + "action": lambda: restore_default(delegate)}, + {"icon": "msg_clear", "text": str(strings.clear_all_except_first), "red": True, + "action": lambda: clear_all_except_first(act, delegate)}, + {"icon": "msg_delete", "text": str(strings.reset_repositories), "red": True, + "action": lambda: reset_repositories(act, delegate)}, + ] + try: + show_plugin_context_menu(anchor.getRootView(), anchor, items) + except Exception as e: + logx(f"repos actions: bulk menu error: {e}", False) diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/ReposActivity/addSheet.py new file mode 100644 index 0000000..8519c9d --- /dev/null +++ b/packit/src/ui/ReposActivity/addSheet.py @@ -0,0 +1,483 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Adding and editing a source. +# +# Built in the shape of the api-key dialog (SettingsActivity/service/ +# AddKeyDialog.py): dimmed overlay, a card that springs in, bold title, gray +# subtitle, outlined field, one accent button. That dialog's overlay, back +# handling, keyboard tracking and animations are imported rather than copied — +# there is no reason for a second implementation of any of them. +# +# What this one adds: a field can refuse to submit and say why in place (the +# old add dialog dismissed itself and then dropped a bulletin, so a typo cost +# you the whole form), the button carries a spinner while the repomap is being +# fetched, and the same dialog serves editing. + +from packutil import logx +import ctypes + +from android_utils import run_on_ui_thread, OnClickListener +from client_utils import get_last_fragment, run_on_queue +from java import dynamic_proxy + +try: + from elyx import strings +except Exception as e: + import android_utils as _au; _au.log(f"repos dialog: import elyx strings failed: {e}") +try: + from org.telegram.ui.ActionBar import Theme + from org.telegram.ui.Components import LayoutHelper, EditTextBoldCursor, OutlineTextContainerView + from org.telegram.messenger import AndroidUtilities, R as R_tg +except Exception as e: + import android_utils as _au; _au.log(f"repos dialog: import telegram classes failed: {e}") + +from ...SettingsActivity.service.AddKeyDialog import ( + _register_back_cb, _unregister_back_cb, _animate_in, _animate_out, + _attach_keyboard_listener, _detach_keyboard_listener, +) +from ...utils.bulletins import factory as _pbf + +# addRepositoryWithUrl answers in lowercase english; the user gets their own +# language and, where possible, a hint at what to do about it +_REASONS = { + "file not found": "repo_err_not_found", + "forbidden": "repo_err_forbidden", + "unauthorized": "repo_err_forbidden", + "rate limited, try again later": "repo_err_rate_limited", + "redirected": "repo_err_redirect", + "permanently redirected": "repo_err_redirect", + "temporarily redirected": "repo_err_redirect", + "see other": "repo_err_redirect", + "request timeout": "repo_err_timeout", + "gateway timeout": "repo_err_timeout", + "server error": "repo_err_server", + "bad gateway": "repo_err_server", + "service unavailable": "repo_err_server", + "resource gone": "repo_err_not_found", + "invalid json": "repo_err_json", + "missing repometa": "repo_err_meta", + "missing rm_rid": "repo_err_rid", + "missing rm_name": "repo_err_name", + "cache write failed": "repo_err_cache", +} + + +def _c(color: int) -> int: + return ctypes.c_int32(color).value + + +def _theme(key: str, fallback: int = 0): + try: + return Theme.getColor(getattr(Theme, key)) + except Exception: + return fallback + + +def _s(key: str, fallback: str = "") -> str: + try: + return str(strings[key]) + except Exception: + return fallback + + +def _localize_reason(reason: str) -> str: + text = str(reason or "").strip() + low = text.lower() + key = _REASONS.get(low) + if key: + return _s(key, text) + if "connection" in low or "max retries" in low or "resolve" in low: + return _s("repo_err_network", text) + return _s("repo_err_http", "{0}").replace("{0}", text) + + +def _make_field(act, label: str, hint: str, value: str, uri: bool): + from android.util import TypedValue + from android.text import InputType, TextUtils + from android.view import View + + dp = AndroidUtilities.dp + outline = OutlineTextContainerView(act) + outline.setText(label) + outline.animateSelection(0, False) + outline.setClipChildren(True) + outline.setClipToPadding(True) + + edit = EditTextBoldCursor(act) + edit.setHint(hint) + edit.setHintTextColor(_theme("key_windowBackgroundWhiteGrayText")) + edit.setTextColor(_theme("key_windowBackgroundWhiteBlackText")) + edit.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + edit.setBackground(None) + edit.setSingleLine(True) + edit.setHorizontallyScrolling(True) + edit.setInputType( + InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI if uri else InputType.TYPE_CLASS_TEXT + ) + if value: + edit.setText(value) + try: + edit.setSelection(len(value)) + except Exception: + pass + try: + edit.setCursorColor(_theme("key_featuredStickers_addButton")) + edit.setCursorWidth(1.5) + except Exception: + pass + edit.setPadding(dp(16), dp(14), dp(16), dp(14)) + try: + edit.setEllipsize(TextUtils.TruncateAt.END) + except Exception: + pass + + class _FocusListener(dynamic_proxy(View.OnFocusChangeListener)): + def onFocusChange(self, v, hasFocus): + outline.animateSelection(1 if hasFocus else 0) + + edit.setOnFocusChangeListener(_FocusListener()) + outline.addView(edit, LayoutHelper.createFrame(-1, -2)) + outline.attachEditText(edit) + return outline, edit + + +def _show_form_dialog(act, title: str, subtitle: str, fields: list, button_text: str, on_submit): + """ + fields — [{"label","hint","value","uri"}] + on_submit(values: list[str], ui) — ui.error(text) / ui.loading(bool) / ui.dismiss() + """ + try: + from android.widget import LinearLayout, TextView, FrameLayout + from android.view import Gravity, ViewGroup + from android.util import TypedValue + from android.graphics.drawable import GradientDrawable + + dp = AndroidUtilities.dp + decor = act.getWindow().getDecorView() + accent = _theme("key_featuredStickers_addButton") + + overlay_ref = [None] + back_cb_ref = [None] + kb_listener_ref = [None] + orig_mode_ref = [None] + busy = [False] + + overlay = FrameLayout(act) + overlay_ref[0] = overlay + overlay.setBackgroundColor(_c(0x99000000)) + overlay.setClickable(True) + overlay.setFocusable(True) + + card = LinearLayout(act) + card.setOrientation(LinearLayout.VERTICAL) + card.setClickable(True) + card.setFocusable(True) + card.setOnClickListener(OnClickListener(lambda v: None)) + card_bg = GradientDrawable() + card_bg.setShape(GradientDrawable.RECTANGLE) + card_bg.setCornerRadius(dp(16)) + card_bg.setColor(_theme("key_dialogBackground")) + card.setBackground(card_bg) + card.setPadding(dp(20), dp(24), dp(20), dp(20)) + + card_lp = FrameLayout.LayoutParams(-1, -2) + card_lp.gravity = Gravity.CENTER + card_lp.leftMargin = dp(32) + card_lp.rightMargin = dp(32) + overlay.addView(card, card_lp) + + def _dismiss(on_end=None): + _unregister_back_cb(back_cb_ref[0]) + back_cb_ref[0] = None + _detach_keyboard_listener(act, decor, kb_listener_ref[0], orig_mode_ref[0]) + kb_listener_ref[0] = None + orig_mode_ref[0] = None + _animate_out(overlay_ref, card, decor, on_end=on_end) + + def _dismiss_from_overlay(v): + if busy[0]: + return + try: + AndroidUtilities.hideKeyboard(edits[0]) + except Exception: + pass + _dismiss() + + overlay.setOnClickListener(OnClickListener(_dismiss_from_overlay)) + + title_tv = TextView(act) + title_tv.setText(title) + title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17) + title_tv.setTextColor(_theme("key_dialogTextBlack")) + title_tv.setGravity(Gravity.CENTER_HORIZONTAL) + try: + title_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + card.addView(title_tv, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 6)) + + subtitle_tv = TextView(act) + subtitle_tv.setText(subtitle) + subtitle_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + subtitle_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + subtitle_tv.setGravity(Gravity.CENTER_HORIZONTAL) + card.addView(subtitle_tv, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 20)) + + edits = [] + for i, spec in enumerate(fields): + outline, edit = _make_field( + act, spec.get("label", ""), spec.get("hint", ""), + spec.get("value", ""), bool(spec.get("uri")) + ) + card.addView(outline, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 10)) + edits.append(edit) + + error_tv = TextView(act) + error_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + error_tv.setTextColor(_theme("key_text_RedRegular", _c(0xFFEC5044))) + error_tv.setGravity(Gravity.CENTER_HORIZONTAL) + error_tv.setVisibility(8) # GONE + card.addView(error_tv, LayoutHelper.createLinear(-1, -2, 4, 0, 4, 6)) + + button_box = FrameLayout(act) + button_box.setClickable(True) + button_box.setFocusable(True) + try: + button_box.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + dp(12), accent, _theme("key_featuredStickers_addButtonPressed", accent))) + except Exception: + pass + + button_tv = TextView(act) + button_tv.setText(button_text) + button_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + button_tv.setGravity(Gravity.CENTER) + button_tv.setPadding(dp(16), dp(14), dp(16), dp(14)) + button_tv.setTextColor(_theme("key_featuredStickers_buttonText")) + try: + button_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + button_box.addView(button_tv, FrameLayout.LayoutParams(-1, -2)) + + spinner_holder = FrameLayout(act) + spinner_holder.setVisibility(8) + button_box.addView(spinner_holder, FrameLayout.LayoutParams(-1, -1)) + card.addView(button_box, LayoutHelper.createLinear(-1, -2, 0, 6, 0, 0)) + + class _Ui: + def error(self, text): + def _apply(): + try: + if not text: + error_tv.setVisibility(8) + return + error_tv.setText(str(text)) + error_tv.setVisibility(0) + error_tv.setAlpha(0.0) + error_tv.animate().alpha(1.0).setDuration(160).start() + except Exception as e: + logx(f"repos dialog: error paint failed: {e}", False) + run_on_ui_thread(_apply) + + def loading(self, value): + busy[0] = bool(value) + + def _apply(): + try: + button_box.setEnabled(not value) + button_tv.setAlpha(0.35 if value else 1.0) + if value and spinner_holder.getChildCount() == 0: + try: + from ..PluginListActivity.helpers.uiHelpers import create_circular_loading + spin = create_circular_loading(act, 20) + spinner_holder.addView(spin, FrameLayout.LayoutParams( + AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) + except Exception as e: + logx(f"repos dialog: spinner unavailable: {e}", True) + spinner_holder.setVisibility(0 if value else 8) + except Exception as e: + logx(f"repos dialog: loading paint failed: {e}", False) + run_on_ui_thread(_apply) + + def dismiss(self, on_end=None): + def _apply(): + try: + AndroidUtilities.hideKeyboard(edits[0] if edits else None) + except Exception: + pass + _dismiss(on_end=on_end) + run_on_ui_thread(_apply) + + ui = _Ui() + + def _submit(v): + if busy[0]: + return + values = [] + for edit in edits: + try: + values.append(str(edit.getText()).strip()) + except Exception: + values.append("") + ui.error(None) + try: + on_submit(values, ui) + except Exception as e: + logx(f"repos dialog: submit error: {e}", False) + ui.loading(False) + ui.error(_s("repo_err_unknown", "{0}").replace("{0}", str(e))) + + button_box.setOnClickListener(OnClickListener(_submit)) + + overlay.setAlpha(0.0) + card.setAlpha(0.0) + card.setScaleX(0.92) + card.setScaleY(0.92) + + decor.addView(overlay, ViewGroup.LayoutParams(-1, -1)) + back_cb_ref[0] = _register_back_cb(act, lambda: None if busy[0] else _dismiss()) + + listener, orig_mode = _attach_keyboard_listener(act, decor, card) + kb_listener_ref[0] = listener + orig_mode_ref[0] = orig_mode + + def _open(): + if edits: + edits[0].requestFocus() + _animate_in(overlay, card, on_end=lambda: ( + AndroidUtilities.showKeyboard(edits[0]) if edits else None)) + + run_on_ui_thread(_open) + except Exception as e: + logx(f"repos dialog: show error: {e}", False) + + +def _normalize_url(url: str) -> str: + text = str(url or "").strip() + if not text: + return "" + if text.startswith("http://") or text.startswith("https://"): + return text + if "://" in text: + return text # some other scheme, let the validator complain + return "https://" + text + + +def show_add_repo_dialog(act, delegate): + def _submit(values, ui): + url = _normalize_url(values[0]) + if not url: + ui.error(_s("repo_err_empty", "Enter a link")) + return + if not url.startswith(("http://", "https://")): + ui.error(_s("repo_err_scheme", "The link must start with https://")) + return + repos = delegate.repoManager.getRepositories() + if any(str(r.get("url") or "").strip() == url for r in repos): + ui.error(_s("repo_err_duplicate", "Already added")) + return + + ui.loading(True) + + def _task(): + repometa, reason = delegate.repoManager.addRepositoryWithUrl(url) + + def _done(): + ui.loading(False) + if reason: + ui.error(_localize_reason(reason)) + return + ui.dismiss(on_end=lambda: _added(delegate)) + + run_on_ui_thread(_done) + + run_on_queue(_task) + + _show_form_dialog( + act, + _s("repo_add_sheet_title", "New source"), + _s("repo_add_sheet_subtitle", "Paste a link to repomap.json"), + [{"label": _s("repo_add_field_url", "Link"), "hint": "https://…/repomap.json", + "value": "", "uri": True}], + str(strings.add_repository), + _submit, + ) + + +def _added(delegate): + try: + frag = get_last_fragment() + container = frag.getParentActivity().getWindow().getDecorView() + rp = frag.getResourceProvider() + _pbf(container, rp).createSimpleBulletin( + R_tg.raw.shared_link_enter, str(strings.repository_added)).show() + except Exception as e: + logx(f"repos dialog: added bulletin error: {e}", True) + try: + delegate.reload() + except Exception: + pass + + +def show_edit_repo_dialog(act, delegate, repo: dict): + def _submit(values, ui): + name = values[0] + url = _normalize_url(values[1]) if len(values) > 1 else "" + if not url: + ui.error(_s("repo_err_empty", "Enter a link")) + return + if not url.startswith(("http://", "https://")): + ui.error(_s("repo_err_scheme", "The link must start with https://")) + return + + idx, repos = delegate._index_of(repo) + if idx < 0: + ui.error(_s("repo_err_unknown", "{0}").replace("{0}", "gone")) + return + if any(i != idx and str(r.get("url") or "").strip() == url for i, r in enumerate(repos)): + ui.error(_s("repo_err_duplicate", "Already added")) + return + + changed_url = str(repo.get("url") or "").strip() != url + if name != str(repo.get("name") or ""): + delegate.repoManager.updateRepoField(idx, "name", name) + if not changed_url: + ui.dismiss(on_end=delegate.reload) + return + + # a new url is a new repomap: validate it before it replaces the old one + ui.loading(True) + + def _task(): + repometa, reason = delegate.repoManager.addRepositoryWithUrl(url) + + def _done(): + ui.loading(False) + if reason: + ui.error(_localize_reason(reason)) + return + # addRepositoryWithUrl appended a fresh entry; drop the old one + fresh_idx, _ = delegate._index_of(repo) + if fresh_idx >= 0: + delegate.repoManager.removeRepository(fresh_idx) + ui.dismiss(on_end=delegate.reload) + + run_on_ui_thread(_done) + + run_on_queue(_task) + + _show_form_dialog( + act, + _s("repo_sheet_edit_title", "Edit source"), + _s("repo_sheet_edit_sub", "Name and link"), + [ + {"label": str(strings.repo_name), "hint": str(strings.repo_name), + "value": str(repo.get("name") or ""), "uri": False}, + {"label": str(strings.repo_url), "hint": "https://…/repomap.json", + "value": str(repo.get("url") or ""), "uri": True}, + ], + _s("save_button", "Save"), + _submit, + ) diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py new file mode 100644 index 0000000..7a98e10 --- /dev/null +++ b/packit/src/ui/ReposActivity/card.py @@ -0,0 +1,274 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# One repository card. +# +# Deliberately not the plugin card: that one is a filled surface with a 18dp +# radius, this is an outlined 16dp container so the two lists read as different +# places. Everything the card shows comes from the repomap already sitting in +# reposCache — the card never touches the network. + +from packutil import logx +import ctypes + +from android.widget import LinearLayout, TextView, FrameLayout, ImageView +from android.view import View, Gravity +from android.util import TypedValue +from android.graphics.drawable import GradientDrawable +from android_utils import OnClickListener + +try: + from org.telegram.messenger import AndroidUtilities + from org.telegram.ui.ActionBar import Theme + from org.telegram.ui.Components import LayoutHelper +except Exception as e: + import android_utils as _au; _au.log(f"repos card: import telegram classes failed: {e}") + AndroidUtilities = None + Theme = None + LayoutHelper = None + +try: + from elyx import strings +except Exception as e: + import android_utils as _au; _au.log(f"repos card: import elyx strings failed: {e}") + +from . import repoIcon +from ..PluginListActivity.helpers.uiHelpers import ( + make_info_chip, apply_press_scale_on_target, resolve_icon, +) + + +def _c(color: int) -> int: + return ctypes.c_int32(color).value + + +def _alpha(color: int, a: int) -> int: + return _c((a << 24) | (color & 0xFFFFFF)) + + +def _theme(key: str, fallback: int = 0): + try: + return Theme.getColor(getattr(Theme, key)) + except Exception: + return fallback + + +def _round_icon_button(ctx, icon_name: str, tint: int, on_click, size_dp: int = 36): + btn = FrameLayout(ctx) + btn.setClickable(True) + btn.setFocusable(True) + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(size_dp) / 2)) + bg.setColor(_alpha(tint, 0x14)) + try: + btn.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + AndroidUtilities.dp(size_dp) // 2, _alpha(tint, 0x14), _alpha(tint, 0x28) + )) + except Exception: + btn.setBackground(bg) + + iv = ImageView(ctx) + icon_id = resolve_icon(icon_name) + if icon_id: + iv.setImageResource(icon_id) + iv.setScaleType(ImageView.ScaleType.CENTER_INSIDE) + try: + iv.setColorFilter(tint) + except Exception: + pass + btn.addView(iv, FrameLayout.LayoutParams( + AndroidUtilities.dp(18), AndroidUtilities.dp(18), Gravity.CENTER + )) + btn.setOnClickListener(OnClickListener(lambda v: on_click())) + apply_press_scale_on_target(btn, btn) + return btn + + +def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): + """ + repo — the stored dict (id / name / url / enabled) + info — read off the ui thread from reposCache: maintainer, telegram, + source, plugins, icons, status ("ok"/"stale"/"missing") + callbacks — on_toggle(bool), on_menu(anchor), on_open(url) + """ + enabled = bool(repo.get("enabled", True)) + accent = repoIcon.accent_for(repo) + + card = LinearLayout(ctx) + card.setOrientation(LinearLayout.VERTICAL) + card.setPadding(*(AndroidUtilities.dp(16),) * 4) + card.setClickable(True) + card.setFocusable(True) + try: + surface = _theme("key_windowBackgroundWhite") + outline = _theme("key_divider") + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(16))) + bg.setColor(surface if enabled else _alpha(surface, 0x80)) + bg.setStroke(AndroidUtilities.dp(1), _alpha(outline, 0xFF if enabled else 0x66)) + card.setBackground(bg) + except Exception as e: + logx(f"repos card: background error: {e}", False) + + # ---- header: avatar | name + maintainer | switch + header = LinearLayout(ctx) + header.setOrientation(LinearLayout.HORIZONTAL) + header.setGravity(Gravity.CENTER_VERTICAL) + + icon_view = repoIcon.build_icon_view(ctx, repo, 48, 14) + header.addView(icon_view, LayoutHelper.createLinear(48, 48, Gravity.CENTER_VERTICAL, 0, 0, 12, 0)) + + col = LinearLayout(ctx) + col.setOrientation(LinearLayout.VERTICAL) + + name_tv = TextView(ctx) + name_tv.setText(str(repo.get("name") or strings.unnamed)) + name_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17) + name_tv.setSingleLine(True) + name_tv.setTextColor(_theme("key_windowBackgroundWhiteBlackText")) + try: + name_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + try: + name_tv.setTypeface(AndroidUtilities.bold()) + except Exception: + pass + col.addView(name_tv, LayoutHelper.createLinear(-1, -2)) + + sub = str(info.get("maintainer") or "").strip() or _host_of(repo.get("url")) + if sub: + sub_tv = TextView(ctx) + sub_tv.setText(sub) + sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + sub_tv.setSingleLine(True) + sub_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + col.addView(sub_tv, LayoutHelper.createLinear(-1, -2, 0, 2, 0, 0)) + + header.addView(col, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) + + switch = _build_switch(ctx, enabled) + if switch is not None: + header.addView(switch, LayoutHelper.createLinear(37, 20, Gravity.CENTER_VERTICAL, 8, 0, 0, 0)) + + card.addView(header, LayoutHelper.createLinear(-1, -2)) + + # ---- chips: status and what the repository carries + chips = LinearLayout(ctx) + chips.setOrientation(LinearLayout.HORIZONTAL) + chips.setGravity(Gravity.CENTER_VERTICAL) + + status = "disabled" if not enabled else str(info.get("status") or "ok") + status_text, status_key = { + "ok": (getattr(strings, "repo_card_status_ok", "OK"), "key_avatar_backgroundGreen"), + "stale": (getattr(strings, "repo_card_status_stale", "Stale"), "key_windowBackgroundWhiteGrayText"), + "missing": (getattr(strings, "repo_card_status_missing", "Not loaded"), "key_text_RedBold"), + "disabled": (getattr(strings, "repo_card_status_disabled", "Disabled"), "key_windowBackgroundWhiteGrayText"), + }.get(status, (status, "key_windowBackgroundWhiteGrayText")) + chips.addView(make_info_chip(ctx, str(status_text), status_key), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + + plugins = info.get("plugins") + if isinstance(plugins, int): + chips.addView( + make_info_chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), + "key_windowBackgroundWhiteBlueText"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + icons_n = info.get("icons") + if isinstance(icons_n, int): + chips.addView( + make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), + "key_avatar_backgroundViolet"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + + card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) + + # ---- footer: telegram / source, overflow on the right + footer = LinearLayout(ctx) + footer.setOrientation(LinearLayout.HORIZONTAL) + footer.setGravity(Gravity.CENTER_VERTICAL) + + tg_url = str(info.get("telegram") or "").strip() + src_url = str(info.get("source") or "").strip() + on_open = callbacks.get("on_open") or (lambda _u: None) + + if tg_url: + footer.addView( + _round_icon_button(ctx, "msg_channel", accent, lambda u=tg_url: on_open(u)), + LayoutHelper.createLinear(36, 36, 0, 0, 8, 0)) + if src_url: + footer.addView( + _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), + LayoutHelper.createLinear(36, 36, 0, 0, 8, 0)) + + spacer = View(ctx) + footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) + + on_menu = callbacks.get("on_menu") + menu_btn = _round_icon_button( + ctx, "ic_ab_other", _theme("key_windowBackgroundWhiteGrayText"), + lambda: on_menu(menu_holder[0]) if on_menu else None + ) + menu_holder = [menu_btn] + footer.addView(menu_btn, LayoutHelper.createLinear(36, 36)) + + card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) + + # tapping the card flips the switch — it is the only stateful control here, + # everything else lives behind explicit buttons + on_toggle = callbacks.get("on_toggle") + state = {"enabled": enabled} + + def _toggle(_v=None): + state["enabled"] = not state["enabled"] + try: + if switch is not None: + switch.setChecked(state["enabled"], True) + except Exception: + pass + if on_toggle: + on_toggle(state["enabled"]) + + card.setOnClickListener(OnClickListener(_toggle)) + apply_press_scale_on_target(card, card) + if not enabled: + try: + icon_view.setAlpha(0.55) + col.setAlpha(0.55) + chips.setAlpha(0.55) + except Exception: + pass + return card + + +def _build_switch(ctx, checked: bool): + # the host's own switch, the one it draws in its plugin cards + try: + from org.telegram.ui.Components import Switch as TgSwitch + sw = TgSwitch(ctx) + sw.setChecked(checked, False) + try: + sw.setColors( + "key_switchTrack", "key_switchTrackChecked", + "key_switchThumb", "key_switchThumbChecked", + ) + except Exception: + pass + # taps are handled by the whole card, the switch only reflects state + sw.setClickable(False) + sw.setFocusable(False) + return sw + except Exception as e: + logx(f"repos card: switch unavailable ({e}), falling back to a chip", True) + return None + + +def _host_of(url) -> str: + try: + text = str(url or "") + if "://" in text: + text = text.split("://", 1)[1] + return text.split("/", 1)[0] + except Exception: + return "" diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py new file mode 100644 index 0000000..c36897c --- /dev/null +++ b/packit/src/ui/ReposActivity/fragment.py @@ -0,0 +1,466 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# The "Sources" screen. +# +# Replaces the settings-list version, where every repository took seven rows and +# its icon was picked out of the host's R.drawable catalogue. Here a repository +# is one card that already knows what it carries, and the icon comes from the +# repomap itself (repometa.rm_icon). +# +# Everything the cards show is read from reposCache off the ui thread; the +# screen makes no network call of its own. + +from packutil import logx +import ctypes +import json +import os +import time + +from java import dynamic_proxy +from android_utils import run_on_ui_thread, OnClickListener +from client_utils import get_last_fragment, run_on_queue + +try: + from elyx import strings +except Exception as e: + import android_utils as _au; _au.log(f"repos fragment: import elyx strings failed: {e}") +try: + from android.widget import FrameLayout, LinearLayout, TextView, ImageView, ScrollView + from android.view import View, Gravity + from android.util import TypedValue + from android.graphics.drawable import GradientDrawable +except Exception as e: + import android_utils as _au; _au.log(f"repos fragment: import android widgets failed: {e}") +try: + from org.telegram.ui.ActionBar import Theme + from org.telegram.ui.Components import LayoutHelper + from org.telegram.messenger import AndroidUtilities, R as R_tg +except Exception as e: + import android_utils as _au; _au.log(f"repos fragment: import telegram classes failed: {e}") +try: + from com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment +except Exception as e: + import android_utils as _au; _au.log(f"repos fragment: import UniversalFragment failed: {e}") + +from . import register, unregister +from .card import make_repo_card +from ..viewUtils import applyFontToTree +from ...utils.paths import getRepoCachePath + +_STALE_AFTER = 24 * 60 * 60 + + +def _c(color: int) -> int: + return ctypes.c_int32(color).value + + +def _alpha(color: int, a: int) -> int: + return _c((a << 24) | (color & 0xFFFFFF)) + + +def _theme(key: str, fallback: int = 0): + try: + return Theme.getColor(getattr(Theme, key)) + except Exception: + return fallback + + +def read_repo_info(repo: dict) -> dict: + """Everything the card needs, straight out of the cached repomap.""" + info = {"maintainer": "", "telegram": "", "source": "", + "plugins": None, "icons": None, "status": "missing"} + repo_id = str(repo.get("id") or "") + if not repo_id: + return info + path = getRepoCachePath(repo_id) + try: + if not os.path.isfile(path): + return info + with open(path, "r", encoding="utf-8") as f: + cached = json.load(f) + except Exception as e: + logx(f"repos: cache unreadable for '{repo_id}': {e}", True) + return info + + meta = cached.get("repometa") or {} + info["maintainer"] = str(meta.get("rm_maintainer") or "") + info["telegram"] = str(meta.get("rm_telegram") or "") + info["source"] = str(meta.get("rm_source") or "") + try: + age = time.time() - os.path.getmtime(path) + info["status"] = "stale" if age > _STALE_AFTER else "ok" + except Exception: + info["status"] = "ok" + + # a repomap that is itself the plugin list carries the count; the usual + # shape only points at it by url, and the screen does not go online to count + plugins = cached.get("plugins") + if isinstance(plugins, list): + info["plugins"] = len(plugins) + icons = cached.get("icons") + if isinstance(icons, list): + info["icons"] = len(icons) + return info + + +class ReposFragment(dynamic_proxy(UniversalFragment.UniversalFragmentDelegate)): + def __init__(self, repoManager): + super().__init__() + self.repoManager = repoManager + self._root = None + self._list = None + self._summary = None + self._alive = [True] + self._fragment = [None] + self._first_build = True + + # ---------------------------------------------------------------- delegate + def onFragmentCreate(self, *_): + register(self) + + def onFragmentDestroy(self, *_): + self._alive[0] = False + unregister(self) + try: + if self._root is not None: + parent = self._root.getParent() + if parent is not None: + parent.removeView(self._root) + self._root = None + except Exception as e: + logx(f"repos fragment: onFragmentDestroy error: {e}", False) + + def beforeCreateView(self): + try: + if self._root is not None: + parent = self._root.getParent() + if parent is not None: + parent.removeView(self._root) + self._root = None + except Exception as e: + logx(f"repos fragment: view cleanup error: {e}", False) + + frag = get_last_fragment() + act = frag.getParentActivity() if frag else None + if not act: + return None + + try: + root = FrameLayout(act) + root.setBackgroundColor(_theme("key_windowBackgroundGray")) + + scroll = ScrollView(act) + scroll.setVerticalScrollBarEnabled(False) + try: + scroll.setFillViewport(True) + except Exception: + pass + + content = LinearLayout(act) + content.setOrientation(LinearLayout.VERTICAL) + content.setPadding(AndroidUtilities.dp(12), AndroidUtilities.dp(8), + AndroidUtilities.dp(12), AndroidUtilities.dp(96)) + + content.addView(self._build_summary_row(act), LayoutHelper.createLinear(-1, -2)) + + self._list = LinearLayout(act) + self._list.setOrientation(LinearLayout.VERTICAL) + content.addView(self._list, LayoutHelper.createLinear(-1, -2)) + + scroll.addView(content, ScrollView.LayoutParams(-1, -2)) + root.addView(scroll, FrameLayout.LayoutParams(-1, -1)) + root.addView(self._build_add_button(act), LayoutHelper.createFrame( + -2, -2, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0, 0, 20)) + + self._root = root + run_on_ui_thread(lambda: self.reload(), 30) + return root + except Exception as e: + logx(f"repos fragment: beforeCreateView error: {e}", False) + return None + + def afterCreateView(self, v): + return None + + def getTitle(self): + try: + return str(strings.repositories) + except Exception: + return "Repositories" + + def onBackPressed(self): + return True + + def fillItems(self, items, adapter): + pass + + def onClick(self, item, view, pos, x, y): + pass + + def onLongClick(self, item, view, pos, x, y): + return False + + def onMenuItemClick(self, mid): + if mid == -1: + try: + frag = self._fragment[0] or get_last_fragment() + if frag: + frag.finishFragment() + except Exception: + pass + + # ------------------------------------------------------------------- build + def reload(self): + # cards are rebuilt wholesale: the list is capped at ten entries, so + # diffing would cost more than it saves + if not self._alive[0] or self._list is None: + return + frag = get_last_fragment() + act = frag.getParentActivity() if frag else None + if not act: + return + repos = self.repoManager.getRepositories() + + def _work(): + infos = [read_repo_info(r) for r in repos] + + def _paint(): + if not self._alive[0] or self._list is None: + return + try: + self._render(act, repos, infos) + except Exception as e: + logx(f"repos fragment: render error: {e}", False) + + run_on_ui_thread(_paint) + + run_on_queue(_work) + + def _render(self, act, repos, infos): + self._list.removeAllViews() + + self._summary.setText(self._summary_text(len(repos))) + + if not repos: + self._list.addView(self._build_empty_state(act), LayoutHelper.createLinear(-1, -2)) + self._first_build = False + applyFontToTree(self._root) + return + + for idx, repo in enumerate(repos): + info = infos[idx] if idx < len(infos) else {} + card = make_repo_card(act, repo, info, self._callbacks_for(act, repo)) + lp = LayoutHelper.createLinear(-1, -2, 0, 0, 0, 8) + self._list.addView(card, lp) + if self._first_build: + self._reveal(card, idx) + + self._first_build = False + applyFontToTree(self._root) + + def _summary_text(self, count: int) -> str: + try: + from ..PluginListActivity.helpers.utils import _format_plural + return str(_format_plural(count, strings.repo_one, strings.repo_few, + strings.repo_many, strings["plural_type"])) + except Exception: + return f"{count}" + + def _reveal(self, card, idx: int): + # md3 enter: fade plus a short rise, staggered down the list + try: + card.setAlpha(0.0) + card.setTranslationY(float(AndroidUtilities.dp(12))) + card.animate().alpha(1.0).translationY(0.0).setStartDelay(idx * 45).setDuration(220).start() + except Exception: + pass + + # --------------------------------------------------------------- callbacks + def _index_of(self, repo: dict): + # never trust a captured index: updateAllCaches drops repositories by + # index while the screen is open + repos = self.repoManager.getRepositories() + repo_id = repo.get("id") + if repo_id: + for i, r in enumerate(repos): + if r.get("id") == repo_id: + return i, repos + url = repo.get("url") + if url: + for i, r in enumerate(repos): + if r.get("url") == url: + return i, repos + return -1, repos + + def _callbacks_for(self, act, repo): + from . import actions + + def _on_toggle(value): + idx, _ = self._index_of(repo) + if idx < 0: + self.reload() + return + repo["enabled"] = value + self.repoManager.updateRepoField(idx, "enabled", value) + + def _on_menu(anchor): + actions.show_card_menu(act, self, repo, anchor) + + def _on_open(url): + actions.open_url(act, url) + + return {"on_toggle": _on_toggle, "on_menu": _on_menu, "on_open": _on_open} + + # ------------------------------------------------------------------ pieces + def _build_summary_row(self, act): + # the bulk actions the old screen kept under "Дополнительно" live behind + # the button on the right of this row + row = LinearLayout(act) + row.setOrientation(LinearLayout.HORIZONTAL) + row.setGravity(Gravity.CENTER_VERTICAL) + row.setPadding(AndroidUtilities.dp(6), 0, 0, AndroidUtilities.dp(8)) + + self._summary = TextView(act) + self._summary.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + self._summary.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + row.addView(self._summary, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) + + from .card import _round_icon_button + + def _menu(): + from . import actions + actions.show_bulk_menu(act, self, menu_btn) + + menu_btn = _round_icon_button( + act, "msg_customize", _theme("key_windowBackgroundWhiteGrayText"), _menu, 34) + row.addView(menu_btn, LayoutHelper.createLinear(34, 34, Gravity.CENTER_VERTICAL)) + return row + + def _build_add_button(self, act): + accent = _theme("key_featuredStickers_addButton") + btn = LinearLayout(act) + btn.setOrientation(LinearLayout.HORIZONTAL) + btn.setGravity(Gravity.CENTER_VERTICAL) + btn.setPadding(AndroidUtilities.dp(18), AndroidUtilities.dp(14), + AndroidUtilities.dp(20), AndroidUtilities.dp(14)) + btn.setClickable(True) + btn.setFocusable(True) + try: + btn.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + AndroidUtilities.dp(28), accent, + _theme("key_featuredStickers_addButtonPressed", accent))) + btn.setElevation(float(AndroidUtilities.dp(10))) + except Exception: + pass + + icon = ImageView(act) + try: + icon.setImageResource(getattr(R_tg.drawable, "msg_add")) + icon.setColorFilter(_theme("key_featuredStickers_buttonText")) + except Exception: + pass + btn.addView(icon, LayoutHelper.createLinear(20, 20, Gravity.CENTER_VERTICAL, 0, 0, 8, 0)) + + label = TextView(act) + try: + label.setText(str(strings.add_repository)) + except Exception: + label.setText("Add") + label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) + label.setTextColor(_theme("key_featuredStickers_buttonText")) + try: + label.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + btn.addView(label, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL)) + + def _add(v): + from . import actions + actions.add_repository(act, self) + + btn.setOnClickListener(OnClickListener(_add)) + try: + from ..PluginListActivity.helpers.uiHelpers import apply_press_scale + apply_press_scale(btn) + except Exception: + pass + return btn + + def _build_empty_state(self, act): + box = LinearLayout(act) + box.setOrientation(LinearLayout.VERTICAL) + box.setGravity(Gravity.CENTER) + box.setPadding(0, AndroidUtilities.dp(64), 0, AndroidUtilities.dp(24)) + + icon = ImageView(act) + try: + icon.setImageResource(getattr(R_tg.drawable, "msg_folders")) + icon.setColorFilter(_alpha(_theme("key_windowBackgroundWhiteGrayText"), 0x66)) + except Exception: + pass + box.addView(icon, LayoutHelper.createLinear(56, 56, Gravity.CENTER_HORIZONTAL, 0, 0, 0, 14)) + + title = TextView(act) + try: + title.setText(str(strings.repos_empty_title)) + except Exception: + title.setText("No repositories") + title.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16) + title.setGravity(Gravity.CENTER) + title.setTextColor(_theme("key_windowBackgroundWhiteBlackText")) + try: + title.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + box.addView(title, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_HORIZONTAL)) + + sub = TextView(act) + try: + sub.setText(str(strings.repos_empty_text)) + except Exception: + sub.setText("") + sub.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + sub.setGravity(Gravity.CENTER) + sub.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + box.addView(sub, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_HORIZONTAL, 24, 6, 24, 0)) + return box + + +def show_repos_fragment(repoManager): + try: + frag = get_last_fragment() + if not frag: + return + delegate = ReposFragment(repoManager) + new_frag = UniversalFragment(delegate) + frag.presentFragment(new_frag) + delegate._fragment[0] = new_frag + + def _setup(attempt=0): + # the action bar is null for a few frames after presentFragment + try: + action_bar = new_frag.getActionBar() + if not action_bar: + if attempt < 10: + run_on_ui_thread(lambda: _setup(attempt + 1), 120) + return + new_frag.setTitle(str(strings.repositories), False, 0) + action_bar.setBackgroundColor(_theme("key_windowBackgroundGray")) + back_icon = getattr(R_tg.drawable, "ic_ab_back", 0) + if back_icon: + action_bar.setBackButtonImage(back_icon) + action_bar.setBackButtonContentDescription("Back") + back_button = action_bar.getBackButton() + if back_button: + def _on_back(v): + f = get_last_fragment() + if f: + f.finishFragment() + back_button.setOnClickListener(OnClickListener(_on_back)) + except Exception as e: + logx(f"repos fragment: actionbar setup error: {e}", False) + + _setup() + except Exception as e: + logx(f"repos fragment: show_repos_fragment error: {e}", False) diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py new file mode 100644 index 0000000..2e8390f --- /dev/null +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -0,0 +1,263 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Repository avatar. +# +# repomap declares the icon as a plain image url (repometa.rm_icon), so there is +# nothing to look up in R.drawable any more — the file is downloaded once, kept +# on disk and in memory, and drawn over a monogram that stands in until it +# arrives (and stays for repositories that declare no icon at all). +# +# The view is a FrameLayout of two layers, monogram below and bitmap above, +# because a Drawable subclass would have to be proxied into java just to paint +# one letter. Late answers are dropped by tag, the same guard utils/stickers.py +# uses, so a card reused for another repository cannot inherit its avatar. + +from packutil import logx +import ctypes +from collections import OrderedDict + +from android.widget import FrameLayout, TextView, ImageView +from android.view import Gravity +from android.util import TypedValue +from android.graphics.drawable import GradientDrawable +from android_utils import run_on_ui_thread + +try: + from org.telegram.messenger import AndroidUtilities + from org.telegram.ui.ActionBar import Theme +except Exception as e: + import android_utils as _au; _au.log(f"repoIcon: import telegram classes failed: {e}") + AndroidUtilities = None + Theme = None + +from ...utils import imagePool +from ...utils.paths import getRepoIconCachePath, getRepoIconCacheDir, getRepoCachePath + +_MEM_CAP = 64 +_mem = OrderedDict() +_mem_lock = None + +_PALETTE = ( + "key_avatar_backgroundBlue", + "key_avatar_backgroundViolet", + "key_avatar_backgroundGreen", + "key_avatar_backgroundOrange", + "key_avatar_backgroundPink", + "key_avatar_backgroundCyan", + "key_avatar_backgroundRed", +) + + +def _c(color: int) -> int: + # java setColor(int) rejects python ints >= 0x80000000 + return ctypes.c_int32(color).value + + +def _alpha(color: int, a: int) -> int: + return _c((a << 24) | (color & 0xFFFFFF)) + + +def _lock(): + global _mem_lock + if _mem_lock is None: + import threading + _mem_lock = threading.Lock() + return _mem_lock + + +def _seed(repo: dict) -> int: + key = str(repo.get("id") or repo.get("url") or repo.get("name") or "") + total = 0 + for ch in key: + total = (total * 31 + ord(ch)) & 0xFFFFFFFF + return total + + +def accent_for(repo: dict) -> int: + # deterministic colour so a repository keeps its look between launches + try: + name = _PALETTE[_seed(repo) % len(_PALETTE)] + return Theme.getColor(getattr(Theme, name)) + except Exception: + try: + return Theme.getColor(Theme.key_featuredStickers_addButton) + except Exception: + return _c(0xFF2AABEE) + + +def _letter(repo: dict) -> str: + for ch in str(repo.get("name") or ""): + if ch.isalnum(): + return ch.upper() + return "?" + + +def icon_url_for(repo: dict): + # rm_icon out of the cached repomap; anything that is not an http(s) link is + # ignored — older repositories put an R.drawable name there + try: + repo_id = str(repo.get("id") or "") + if not repo_id: + return None + import json + import os + path = getRepoCachePath(repo_id) + if not os.path.isfile(path): + return None + with open(path, "r", encoding="utf-8") as f: + cached = json.load(f) + url = str((cached.get("repometa") or {}).get("rm_icon") or "").strip() + return url if url.lower().startswith(("http://", "https://")) else None + except Exception as e: + logx(f"repoIcon: icon_url_for error: {e}", True) + return None + + +def _load_bitmap(url: str, px: int): + # memory -> disk -> network, decoded to a px-sized bitmap + with _lock(): + bmp = _mem.get(url) + if bmp is not None: + _mem.move_to_end(url) + return bmp + + import os + path = getRepoIconCachePath(url) + data = None + try: + if os.path.isfile(path): + with open(path, "rb") as f: + data = f.read() + except Exception: + data = None + + if not data: + data = imagePool.fetch(url) + if not data: + return None + try: + os.makedirs(getRepoIconCacheDir(), exist_ok=True) + with open(path, "wb") as f: + f.write(data) + except Exception as e: + logx(f"repoIcon: cache write failed: {e}", True) + + bmp = imagePool.decode(data, px, imagePool.looks_like_svg(url, data)) + if bmp is None: + # a corrupted cache entry would keep failing forever + try: + os.unlink(path) + except Exception: + pass + return None + with _lock(): + _mem[url] = bmp + while len(_mem) > _MEM_CAP: + _mem.popitem(last=False) + return bmp + + +def load_url_into(image_view, url: str, size_dp: int = 48): + # for callers that already have their own ImageView (the repo=add deeplink + # sheet), no monogram layer involved + if not url: + return + size_px = AndroidUtilities.dp(size_dp) + want = f"packit_repoicon_url_{abs(hash(url))}" + try: + image_view.setTag(want) + except Exception: + pass + + def _task(): + bmp = _load_bitmap(url, size_px) + if bmp is None: + return + + def _apply(): + try: + if str(image_view.getTag() or "") != want: + return + image_view.setImageBitmap(bmp) + try: + image_view.setColorFilter(None) + except Exception: + pass + except Exception as e: + logx(f"repoIcon: url bind error: {e}", False) + + run_on_ui_thread(_apply) + + imagePool.submit(_task) + + +def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14): + # monogram now, real icon when it arrives + size_px = AndroidUtilities.dp(size_dp) + accent = accent_for(repo) + + holder = FrameLayout(ctx) + + mono = TextView(ctx) + mono.setText(_letter(repo)) + mono.setGravity(Gravity.CENTER) + mono.setTextSize(TypedValue.COMPLEX_UNIT_DIP, max(12, int(size_dp * 0.42))) + mono.setTextColor(_alpha(accent, 0xFF)) + try: + mono.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + try: + mono.setTypeface(AndroidUtilities.bold()) + except Exception: + pass + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(radius_dp))) + bg.setColor(_alpha(accent, 0x1C)) + mono.setBackground(bg) + holder.addView(mono, FrameLayout.LayoutParams(size_px, size_px)) + + image = ImageView(ctx) + image.setScaleType(ImageView.ScaleType.CENTER_CROP) + image.setVisibility(8) # GONE + try: + image.setClipToOutline(True) + image.setBackground(bg.getConstantState().newDrawable().mutate()) + except Exception: + pass + holder.addView(image, FrameLayout.LayoutParams(size_px, size_px)) + + url = None + try: + url = repo.get("_icon_url") # resolved by the caller when it read the cache + except Exception: + url = None + + want = f"packit_repoicon_{_seed(repo)}" + holder.setTag(want) + + def _task(): + target = url if url else icon_url_for(repo) + if not target: + return + bmp = _load_bitmap(target, size_px) + if bmp is None: + return + + def _apply(): + try: + if str(holder.getTag() or "") != want: + return + image.setImageBitmap(bmp) + image.setVisibility(0) # VISIBLE + image.setAlpha(0.0) + image.animate().alpha(1.0).setDuration(160).start() + mono.setVisibility(8) + except Exception as e: + logx(f"repoIcon: bind error: {e}", False) + + run_on_ui_thread(_apply) + + imagePool.submit(_task) + return holder diff --git a/packit/src/utils/imagePool.py b/packit/src/utils/imagePool.py new file mode 100644 index 0000000..a4aa6e6 --- /dev/null +++ b/packit/src/utils/imagePool.py @@ -0,0 +1,99 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Shared worker pool and decoder for remote images. +# +# One thread per image starves the CPU and the network the moment a screen +# opens a dozen of them at once — the icon catalog learned that the hard way and +# grew a small fixed pool. Repository icons need exactly the same thing, so the +# pool and the decode step live here instead of being copied a second time. + +from packutil import logx +import threading + +_WORKERS = 4 +_queue = None +_queue_lock = threading.Lock() + + +def submit(task): + # runs task() on one of the pool threads; tasks are served in order + global _queue + with _queue_lock: + if _queue is None: + import queue + _queue = queue.Queue() + + def _worker(): + while True: + fn = _queue.get() + try: + fn() + except Exception as e: + logx(f"imagePool: worker error: {e}", True) + finally: + _queue.task_done() + + for _ in range(_WORKERS): + threading.Thread(target=_worker, daemon=True).start() + _queue.put(task) + + +def fetch(url: str, timeout: int = 15): + # downloads the bytes of an image, or None + try: + import requests + r = requests.get(url, timeout=timeout, headers={ + "User-Agent": "PackIt/1.0 (Android; github.com/shareui/packit)" + }) + if r.status_code != 200: + logx(f"imagePool: HTTP {r.status_code} for {url}", True) + return None + return r.content + except Exception as e: + logx(f"imagePool: fetch error for {url}: {e}", False) + return None + + +def decode(data, px: int, is_svg: bool = False): + # bytes -> Bitmap scaled for a px-sized slot, or None + if not data: + return None + from hook_utils import find_class + try: + if is_svg: + SVG = find_class("com.caverock.androidsvg.SVG") + ByteArrayInputStream = find_class("java.io.ByteArrayInputStream") + Bitmap = find_class("android.graphics.Bitmap") + Canvas = find_class("android.graphics.Canvas") + svg = SVG.getFromInputStream(ByteArrayInputStream(data)) + # force the render size; the viewBox scales to it + svg.setDocumentWidth(px) + svg.setDocumentHeight(px) + bmp = Bitmap.createBitmap(px, px, Bitmap.Config.ARGB_8888) + svg.renderToCanvas(Canvas(bmp)) + return bmp + + BitmapFactory = find_class("android.graphics.BitmapFactory") + opts = BitmapFactory.Options() + opts.inJustDecodeBounds = True + BitmapFactory.decodeByteArray(data, 0, len(data), opts) + if opts.outWidth > 0 and opts.outHeight > 0 and px > 0: + opts.inSampleSize = max(1, min(opts.outWidth // px, opts.outHeight // px)) + opts.inJustDecodeBounds = False + return BitmapFactory.decodeByteArray(data, 0, len(data), opts) + except Exception as e: + logx(f"imagePool: decode error: {e}", False) + return None + + +def looks_like_svg(url: str, data=None) -> bool: + try: + if str(url).lower().split("?", 1)[0].endswith(".svg"): + return True + if data: + head = bytes(data[:256]).lstrip() + return head.startswith(b" str: def getClassesCachePath() -> str: return _filesDir() + "/packit/.cache/classes/icons.json" +def getRepoIconCacheDir() -> str: + return _filesDir() + "/packit/.cache/repoIcons" + +def getRepoIconCachePath(url: str) -> str: + # repomap declares rm_icon as a plain image url, so the file name is a hash + # of it: the url can be any length and carries query strings + import hashlib + digest = hashlib.sha1(str(url).encode("utf-8", "ignore")).hexdigest()[:20] + return getRepoIconCacheDir() + f"/{digest}.img" + def getKeysDir() -> str: return _filesDir() + "/packit/.secret/keys" From 7f0bbde17e4f7d472ce6c2d1d247baa5fe68c8c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:18:16 +0000 Subject: [PATCH 12/46] Keep the share link resolvable and the url inside its field (0.1.2-dev.2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The share deeplink stopped working. I had started percent-encoding the whole query when the actions moved over, so a shared repository arrived as link=https%3A%2F%2Fraw.githubusercontent.com%2F… — the plugin's own parser decodes that fine (urlparse + parse_qs), but the link has to survive the telegram client first, and it no longer resolved there. The link goes back to the plain form that worked, and only the name is escaped, for the five characters that would otherwise end or split the query. The url in the edit dialog also drew across the field's border. Two reasons: the cursor was placed at the end of the text, which scrolls a long url until its tail shows, and a scrolled single-line TextView paints over its own padding. The cursor now stays at the start — the beginning of a url is the part worth reading anyway — and the inset moved to a wrapper that clips to it, so the text cannot leave the outline no matter how far it is scrolled. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/actions.py | 27 +++++++++++++++---------- packit/src/ui/ReposActivity/addSheet.py | 20 +++++++++++++++--- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index a139668..9534adf 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.1.2-dev.1" +version: "0.1.2-dev.2" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/ReposActivity/actions.py index 0d55078..5b8914c 100644 --- a/packit/src/ui/ReposActivity/actions.py +++ b/packit/src/ui/ReposActivity/actions.py @@ -73,13 +73,22 @@ def copy_link(repo: dict): BulletinHelper.show_error(str(strings.failed_to_copy)) +def _share_link(repo: dict) -> str: + # The link goes into a telegram message, so it has to survive that client's + # url detection: percent-encoding it whole made the message text look like + # a link but stopped resolving. Only the name is escaped, and only for the + # characters that would otherwise end or split the query. + name = str(repo.get("name") or "").strip() + for ch, esc in (("%", "%25"), ("&", "%26"), ("=", "%3D"), ("#", "%23"), (" ", "%20")): + name = name.replace(ch, esc) + url = str(repo.get("url") or "").strip() + icon = str(repo.get("icon") or "").strip() + return f"tg://packit?repo=add&name={name}&link={url}&icon={icon}" + + def share_repository(act, repo: dict): # the deeplink the other client will resolve back into a repository - from urllib.parse import quote - name = quote(str(repo.get("name") or "").strip(), safe="") - url = quote(str(repo.get("url") or "").strip(), safe="") - icon = quote(str(repo.get("icon") or "").strip(), safe="") - share_url = f"tg://packit?repo=add&name={name}&link={url}&icon={icon}" + share_url = _share_link(repo) try: from java import jclass, dynamic_proxy frag = get_last_fragment() @@ -146,15 +155,11 @@ def _done(): def export_repositories(act, delegate): repos = delegate.repoManager.getRepositories() - from urllib.parse import quote links = [] for repo in repos: - url = str(repo.get("url") or "").strip() - if not url: + if not str(repo.get("url") or "").strip(): continue - name = quote(str(repo.get("name") or "").strip(), safe="") - icon = quote(str(repo.get("icon") or "").strip(), safe="") - links.append(f"tg://packit?repo=add&name={name}&link={quote(url, safe='')}&icon={icon}") + links.append(_share_link(repo)) if not links: BulletinHelper.show_error(str(strings.no_repositories_to_export)) return diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/ReposActivity/addSheet.py index 8519c9d..8f42a46 100644 --- a/packit/src/ui/ReposActivity/addSheet.py +++ b/packit/src/ui/ReposActivity/addSheet.py @@ -117,8 +117,11 @@ def _make_field(act, label: str, hint: str, value: str, uri: bool): ) if value: edit.setText(value) + # cursor stays at the start: putting it at the end scrolls a long url so + # that its tail is what you see, and the beginning is the part worth + # reading try: - edit.setSelection(len(value)) + edit.setSelection(0) except Exception: pass try: @@ -126,7 +129,7 @@ def _make_field(act, label: str, hint: str, value: str, uri: bool): edit.setCursorWidth(1.5) except Exception: pass - edit.setPadding(dp(16), dp(14), dp(16), dp(14)) + edit.setPadding(dp(4), dp(14), dp(4), dp(14)) try: edit.setEllipsize(TextUtils.TruncateAt.END) except Exception: @@ -137,7 +140,18 @@ def onFocusChange(self, v, hasFocus): outline.animateSelection(1 if hasFocus else 0) edit.setOnFocusChangeListener(_FocusListener()) - outline.addView(edit, LayoutHelper.createFrame(-1, -2)) + + # A scrolled single-line TextView paints over its own padding, so a long url + # ran out from under the outline and over the border. The inset lives on a + # wrapper that clips to it instead, which the text cannot escape. + from android.widget import FrameLayout as _FrameLayout + holder = _FrameLayout(act) + holder.setPadding(dp(12), 0, dp(12), 0) + holder.setClipToPadding(True) + holder.setClipChildren(True) + holder.addView(edit, LayoutHelper.createFrame(-1, -2)) + + outline.addView(holder, LayoutHelper.createFrame(-1, -2)) outline.attachEditText(edit) return outline, edit From 4f3fe8ad7141ba689edd290f0e6f212de073cf56 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:22:11 +0000 Subject: [PATCH 13/46] Let the host's switch keep its own colours (0.1.2-dev.3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The toggle on a repository card came out square and in the wrong palette. It is already the client's own switch, the widget every settings row draws, and its colours are set from the theme by its constructor — but I called setColors("key_switchTrack", …) on top of that, which hands Theme.getColor the name of the constant rather than its value. Nothing resolved, so the track and thumb fell back to whatever the default palette had. Dropping the call is the whole fix: the switch now draws exactly like the one next to any other setting in the client. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 9534adf..82ac1fb 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.1.2-dev.2" +version: "0.1.2-dev.3" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 7a98e10..4437e52 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -243,24 +243,24 @@ def _toggle(_v=None): def _build_switch(ctx, checked: bool): - # the host's own switch, the one it draws in its plugin cards + # The host's own switch — the same widget every settings row and the client's + # own plugin cards draw, so it matches the rest of the app for free. + # + # Its colours are already set by the constructor from the theme. Passing + # setColors("key_switchTrack", …) looked right but hands Theme.getColor the + # name of the constant instead of its value, so nothing resolved and the + # track came out in whatever the fallback palette had — which is what made + # the toggle look square and off-colour. try: from org.telegram.ui.Components import Switch as TgSwitch sw = TgSwitch(ctx) sw.setChecked(checked, False) - try: - sw.setColors( - "key_switchTrack", "key_switchTrackChecked", - "key_switchThumb", "key_switchThumbChecked", - ) - except Exception: - pass # taps are handled by the whole card, the switch only reflects state sw.setClickable(False) sw.setFocusable(False) return sw except Exception as e: - logx(f"repos card: switch unavailable ({e}), falling back to a chip", True) + logx(f"repos card: switch unavailable: {e}", False) return None From 7a8403456aa5fefa25abec18f74b7f2e10e0d7cb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:25:09 +0000 Subject: [PATCH 14/46] Accept a repomap with a trailing comma (0.1.2-dev.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Нет метаданных" on the official repository, and it is not the deeplink: that message is only reached after the file has been fetched successfully. configs/repomap.json carries a trailing comma after rm_icon, so json.loads throws, repometa stays None, and every path reports the file as having no metadata. Adding the repository fails the same way, with "invalid json", and the startup refresh quietly keeps the stale cache. A repomap is written by hand and a trailing comma is the mistake people make — javascript and python both take it, json does not. So parse strictly first, and only when that fails strip trailing commas and try once more. The stripping walks the text rather than running a regex over it, because a pattern like ",(\s*[}\]])" also eats the comma inside a string such as "a, }" and would silently change data; strings are skipped, and anything still broken raises as before. When the fallback fires it is logged, so the malformed file stays visible in a bug report. Applied to every place a fetched repository file is parsed: the manager's add and refresh paths, and the repo=add / update deeplinks. The real fix is still one character in the repository itself. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/RepositoryManager.py | 7 +-- packit/src/deeplinks/repo.py | 5 ++- packit/src/deeplinks/update.py | 5 ++- packit/src/utils/jsonx.py | 76 +++++++++++++++++++++++++++++++++ 5 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 packit/src/utils/jsonx.py diff --git a/packit/meta.yml b/packit/meta.yml index 82ac1fb..9e3899d 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.1.2-dev.3" +version: "0.1.2-dev.4" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index b398016..380ae5b 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -5,6 +5,7 @@ from .utils.netQueue import run_serial_io import os import json +from .utils import jsonx as _jsonx import requests from client_utils import get_last_fragment, run_on_queue try: @@ -66,7 +67,7 @@ def _fetch_and_save_repomap(self, url: str) -> dict | None: if r.status_code != 200: logx(f"repom: failed to fetch repomap from '{url}': HTTP {r.status_code}", True) return None - data = r.json() + data = _jsonx.loads(r.text) repometa = data.get("repometa") if not repometa: logx(f"repom: no 'repometa' key in response from '{url}'", True) @@ -158,7 +159,7 @@ def addRepositoryWithUrl(self, url: str): # validate try: with open(temp_path, "r", encoding="utf-8") as f: - data = json.load(f) + data = _jsonx.loads(f.read()) except Exception as e: logx(f"repom: addRepositoryWithUrl: json parse error: {e}", False) self._cleanup_temp_dir() @@ -352,7 +353,7 @@ def task(): if r.status_code != 200: logx(f"updateAllCaches: HTTP {r.status_code} for {url}", True) continue - data = r.json() + data = _jsonx.loads(r.text) repometa = data.get("repometa") rm_rid = repometa.get("rm_rid") if repometa else None diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index a176ed9..a55c7c9 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -35,6 +35,7 @@ from urllib.parse import urlparse, parse_qs import requests import json +from ..utils import jsonx as _jsonx import os BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") @@ -97,7 +98,7 @@ def fetch_task(): try: response = requests.get(link, timeout=10) if response.status_code == 200: - data = response.json() + data = _jsonx.loads(response.text) repometa = data.get("repometa") if repometa and repometa.get("rm_rid"): @@ -116,7 +117,7 @@ def fetch_task(): try: pr = requests.get(plugins_url, timeout=10) if pr.status_code == 200: - pdata = pr.json() + pdata = _jsonx.loads(pr.text) plugins = pdata.get("plugins", []) pluginCount = len(plugins) if isinstance(plugins, (list, dict)) else 0 except Exception as e: diff --git a/packit/src/deeplinks/update.py b/packit/src/deeplinks/update.py index 452616a..9cf935f 100644 --- a/packit/src/deeplinks/update.py +++ b/packit/src/deeplinks/update.py @@ -18,6 +18,7 @@ from urllib.parse import urlparse, parse_qs import requests import json +from ..utils import jsonx as _jsonx import os @@ -45,7 +46,7 @@ def task(): if r.status_code != 200: logx(f"update deeplink: HTTP {r.status_code} for {url}", True) continue - data = r.json() + data = _jsonx.loads(r.text) repometa = data.get("repometa") rmRid = repometa.get("rm_rid") if repometa else None @@ -110,7 +111,7 @@ def task(): logx(f"update deeplink: HTTP {r.status_code} for {url}", True) run_on_ui_thread(lambda: BulletinHelper.show_error(str(strings("dl_update_repo_http_error", code=r.status_code)))) return - data = r.json() + data = _jsonx.loads(r.text) repometa = data.get("repometa") rmRid = repometa.get("rm_rid") if repometa else None diff --git a/packit/src/utils/jsonx.py b/packit/src/utils/jsonx.py new file mode 100644 index 0000000..7df8d72 --- /dev/null +++ b/packit/src/utils/jsonx.py @@ -0,0 +1,76 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Forgiving parser for repository files. +# +# A repomap.json is written by hand, and a trailing comma before a closing +# brace is the mistake people make — javascript and python both accept it, +# json does not. The official repository shipped one and every client then +# refused to add or refresh it, reporting "no metadata" for a file that was +# otherwise perfectly fine. +# +# So: parse strictly, and only if that fails strip trailing commas and try +# once more. The stripping walks the text instead of running a regex over it, +# because a naive pattern also eats the comma in a string like "a, }" and +# would quietly change data. + +from packutil import logx +import json + + +def _strip_trailing_commas(text: str) -> str: + out = [] + in_string = False + escaped = False + pending = -1 # index in `out` of a comma waiting to see what follows + + for ch in text: + if in_string: + out.append(ch) + if escaped: + escaped = False + elif ch == "\\": + escaped = True + elif ch == '"': + in_string = False + continue + + if ch == '"': + pending = -1 + in_string = True + out.append(ch) + continue + + if ch == ",": + pending = len(out) + out.append(ch) + continue + + if ch in " \t\r\n": + out.append(ch) + continue + + if ch in "}]" and pending >= 0: + out[pending] = "" # the comma was trailing after all + pending = -1 + out.append(ch) + + return "".join(out) + + +def loads(text): + """json.loads that tolerates trailing commas. Raises like json.loads does.""" + try: + return json.loads(text) + except ValueError as strict_error: + repaired = _strip_trailing_commas(text) + if repaired == text: + raise + value = json.loads(repaired) + logx(f"jsonx: accepted a file with trailing commas ({strict_error})", False) + return value + + +def loads_response(response): + """Same, for a requests response.""" + return loads(response.text) From b9cc1162fc1c687b20fde69c7548952c44b78bdf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:37:40 +0000 Subject: [PATCH 15/46] Stop the layout helper from weighting fixed-size views (0.1.2-dev.5) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The switch still drew as a square. It is the client's own widget and its draw is fixed: Switch.onDraw centres a 31x14dp rounded track and a 20dp thumb inside whatever size the view was measured at. Nothing about it can become rectangular — unless the view is narrower than the track, in which case the pill is clipped by the view bounds and what is left has square ends. That is what was happening. The cause is an overload. LayoutHelper.createLinear(37, 20, gravity, l, t, r, b) has a twin taking a float weight in the third position, and the call was resolving to it: the switch got weight 16 (the value of Gravity.CENTER_VERTICAL) instead of a gravity. In a row whose text column is already weighted, the overflow is then shared out by weight and the switch — holding almost all of it — is squeezed below its 31dp track. Every fixed-size child in a weighted row now builds its LayoutParams explicitly, so no call can pick the weight overload: the switch, the avatar, the telegram/source/overflow buttons, the actions button in the header and the icon inside the add button. The avatar was one measurement away from the same fate. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 29 ++++++++++++++++++++----- packit/src/ui/ReposActivity/fragment.py | 14 +++++++++--- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 9e3899d..ba279e3 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.1.2-dev.4" +version: "0.1.2-dev.5" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 4437e52..d79e7d3 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -118,7 +118,10 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): header.setGravity(Gravity.CENTER_VERTICAL) icon_view = repoIcon.build_icon_view(ctx, repo, 48, 14) - header.addView(icon_view, LayoutHelper.createLinear(48, 48, Gravity.CENTER_VERTICAL, 0, 0, 12, 0)) + icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(48), AndroidUtilities.dp(48)) + icon_lp.gravity = Gravity.CENTER_VERTICAL + icon_lp.rightMargin = AndroidUtilities.dp(12) + header.addView(icon_view, icon_lp) col = LinearLayout(ctx) col.setOrientation(LinearLayout.VERTICAL) @@ -150,7 +153,18 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): switch = _build_switch(ctx, enabled) if switch is not None: - header.addView(switch, LayoutHelper.createLinear(37, 20, Gravity.CENTER_VERTICAL, 8, 0, 0, 0)) + # Explicit params, not LayoutHelper.createLinear(37, 20, gravity, …): + # that call has a (w, h, float weight, …) twin, and picking it gives the + # switch a weight instead of a gravity. In a row that already has a + # weighted column the switch then absorbs the overflow and is measured + # narrower than the 31dp track Switch.onDraw centres in it, so the track + # is clipped by the view bounds — which is what turned the pill into a + # rectangle with square corners. + sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(20)) + sw_lp.gravity = Gravity.CENTER_VERTICAL + sw_lp.leftMargin = AndroidUtilities.dp(10) + switch.setMinimumWidth(AndroidUtilities.dp(37)) + header.addView(switch, sw_lp) card.addView(header, LayoutHelper.createLinear(-1, -2)) @@ -193,14 +207,19 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): src_url = str(info.get("source") or "").strip() on_open = callbacks.get("on_open") or (lambda _u: None) + def _btn_lp(right_margin_dp=8): + lp = LinearLayout.LayoutParams(AndroidUtilities.dp(36), AndroidUtilities.dp(36)) + lp.rightMargin = AndroidUtilities.dp(right_margin_dp) + return lp + if tg_url: footer.addView( _round_icon_button(ctx, "msg_channel", accent, lambda u=tg_url: on_open(u)), - LayoutHelper.createLinear(36, 36, 0, 0, 8, 0)) + _btn_lp()) if src_url: footer.addView( _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), - LayoutHelper.createLinear(36, 36, 0, 0, 8, 0)) + _btn_lp()) spacer = View(ctx) footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) @@ -211,7 +230,7 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): lambda: on_menu(menu_holder[0]) if on_menu else None ) menu_holder = [menu_btn] - footer.addView(menu_btn, LayoutHelper.createLinear(36, 36)) + footer.addView(menu_btn, _btn_lp(0)) card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index c36897c..6f41645 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -334,7 +334,9 @@ def _menu(): menu_btn = _round_icon_button( act, "msg_customize", _theme("key_windowBackgroundWhiteGrayText"), _menu, 34) - row.addView(menu_btn, LayoutHelper.createLinear(34, 34, Gravity.CENTER_VERTICAL)) + menu_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(34), AndroidUtilities.dp(34)) + menu_lp.gravity = Gravity.CENTER_VERTICAL + row.addView(menu_btn, menu_lp) return row def _build_add_button(self, act): @@ -360,7 +362,10 @@ def _build_add_button(self, act): icon.setColorFilter(_theme("key_featuredStickers_buttonText")) except Exception: pass - btn.addView(icon, LayoutHelper.createLinear(20, 20, Gravity.CENTER_VERTICAL, 0, 0, 8, 0)) + icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20)) + icon_lp.gravity = Gravity.CENTER_VERTICAL + icon_lp.rightMargin = AndroidUtilities.dp(8) + btn.addView(icon, icon_lp) label = TextView(act) try: @@ -399,7 +404,10 @@ def _build_empty_state(self, act): icon.setColorFilter(_alpha(_theme("key_windowBackgroundWhiteGrayText"), 0x66)) except Exception: pass - box.addView(icon, LayoutHelper.createLinear(56, 56, Gravity.CENTER_HORIZONTAL, 0, 0, 0, 14)) + stub_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(56), AndroidUtilities.dp(56)) + stub_lp.gravity = Gravity.CENTER_HORIZONTAL + stub_lp.bottomMargin = AndroidUtilities.dp(14) + box.addView(icon, stub_lp) title = TextView(act) try: From ad381b2ff21e84986ae021ada3f65291823958e2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:39:35 +0000 Subject: [PATCH 16/46] Set the switch up the way the client's own plugin card does (0.1.2-dev.6) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PluginCell, the card the client draws for every installed plugin, builds its toggle like this: Switch s = new Switch(context); s.setColors(Theme.key_switchTrack, Theme.key_switchTrackChecked, Theme.key_windowBackgroundWhite, Theme.key_windowBackgroundWhite); s.setFocusable(false); addView(s, LayoutHelper.createFrame(37, 40, ...)); Two things I had wrong. The colours: the thumb is key_windowBackgroundWhite for both states, which is what gives the knob its usual look — I had left it to the constructor's defaults. And the box is 37x40, not 37x20: onDraw centres a 14dp track and a 20dp thumb circle in the view, so at 20dp tall the circle fills the height exactly and its top and bottom are shaved off by the view bounds. Square edges, in other words — the same flattening the width squeeze was causing at the sides. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 22 ++++++++++++++-------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index ba279e3..dd21356 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.1.2-dev.5" +version: "0.1.2-dev.6" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index d79e7d3..5de36b9 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -160,7 +160,7 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): # narrower than the 31dp track Switch.onDraw centres in it, so the track # is clipped by the view bounds — which is what turned the pill into a # rectangle with square corners. - sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(20)) + sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(40)) sw_lp.gravity = Gravity.CENTER_VERTICAL sw_lp.leftMargin = AndroidUtilities.dp(10) switch.setMinimumWidth(AndroidUtilities.dp(37)) @@ -262,17 +262,23 @@ def _toggle(_v=None): def _build_switch(ctx, checked: bool): - # The host's own switch — the same widget every settings row and the client's - # own plugin cards draw, so it matches the rest of the app for free. + # Set up exactly the way the client sets up the switch in its own plugin + # card (PluginCell): the same colour keys, and the same 37x40 box. # - # Its colours are already set by the constructor from the theme. Passing - # setColors("key_switchTrack", …) looked right but hands Theme.getColor the - # name of the constant instead of its value, so nothing resolved and the - # track came out in whatever the fallback palette had — which is what made - # the toggle look square and off-colour. + # The height matters. Switch.onDraw centres a 14dp track and then a 20dp + # thumb circle at the middle of the view, so at a 20dp-tall box the circle + # spans the full height and its top and bottom are shaved off by the view + # bounds — which is most of what made the toggle look square. try: from org.telegram.ui.Components import Switch as TgSwitch sw = TgSwitch(ctx) + try: + sw.setColors( + Theme.key_switchTrack, Theme.key_switchTrackChecked, + Theme.key_windowBackgroundWhite, Theme.key_windowBackgroundWhite, + ) + except Exception as e: + logx(f"repos card: switch colors unavailable: {e}", True) sw.setChecked(checked, False) # taps are handled by the whole card, the switch only reflects state sw.setClickable(False) From 3aafcbbdde45209e0d80c60c0c925e3e5440a047 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:41:22 +0000 Subject: [PATCH 17/46] Harmonize the repository palette with the system one (0.1.2-dev.7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The client carries MonetUtils for this. On Android 12+ it pulls a colour towards the system palette — MaterialColors.harmonize against system_accent1_600 — and it is how the client keeps its own fixed colours from clashing with a Monet theme; Theme.refreshMonetColors() re-runs it when the user changes the wallpaper overlay. The repository avatar picks its colour deterministically from the id, so on a Monet theme those seven fixed hues were the one thing on the screen not following the wallpaper. They go through MonetUtils.harmonize now, cached per colour, and fall back to the raw value below Android 12 or when the class is absent. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/repoIcon.py | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index dd21356..48e5252 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.1.2-dev.6" +version: "0.1.2-dev.7" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index 2e8390f..a8cd426 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -74,11 +74,32 @@ def _seed(repo: dict) -> int: return total +_harmonized = {} + + +def _harmonize(color: int) -> int: + # The client ships MonetUtils for exactly this: on Android 12+ it pulls a + # colour towards the system palette (MaterialColors.harmonize against + # system_accent1_600), which is what keeps a fixed palette from clashing + # with a Monet theme. Below 12, and on themes without it, it hands the + # colour back unchanged. + if color in _harmonized: + return _harmonized[color] + result = color + try: + from com.exteragram.messenger.utils.ui import MonetUtils + result = _c(int(MonetUtils.harmonize(color))) + except Exception: + result = color + _harmonized[color] = result + return result + + def accent_for(repo: dict) -> int: # deterministic colour so a repository keeps its look between launches try: name = _PALETTE[_seed(repo) % len(_PALETTE)] - return Theme.getColor(getattr(Theme, name)) + return _harmonize(Theme.getColor(getattr(Theme, name))) except Exception: try: return Theme.getColor(Theme.key_featuredStickers_addButton) From 45e8b2f79935abb8b367ea721a7cbb6a3a10fae6 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:45:42 +0000 Subject: [PATCH 18/46] Use the client's loading button in the add dialog (0.1.2-dev.8) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The spinner was drawn on top of the label, so "Добавить репозиторий" stayed readable underneath it — my own overlay of a FrameLayout, a TextView and a CircularProgressDrawable stacked in the same box. The client already has the button for this. ButtonWithCounterView.setLoading animates the label out and the spinner in over 320ms on an EASE_OUT_QUINT curve, and it is what the plugin install sheet and the repo=add deeplink sheet in this very plugin already use. The dialog builds one, calls setRound() and hands the loading state straight to it. The hand-rolled button stays as a fallback for a client without the class, minus the overlay: there the label just dims. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/addSheet.py | 77 +++++++++++++------------ 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 48e5252..6cf6755 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.1.2-dev.7" +version: "0.1.2-dev.8" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/ReposActivity/addSheet.py index 8f42a46..c54796f 100644 --- a/packit/src/ui/ReposActivity/addSheet.py +++ b/packit/src/ui/ReposActivity/addSheet.py @@ -254,31 +254,42 @@ def _dismiss_from_overlay(v): error_tv.setVisibility(8) # GONE card.addView(error_tv, LayoutHelper.createLinear(-1, -2, 4, 0, 4, 6)) - button_box = FrameLayout(act) - button_box.setClickable(True) - button_box.setFocusable(True) + # the client's own button: setLoading() swaps the label for a spinner + # itself, so there is nothing to stack on top of the text + button = None try: - button_box.setBackground(Theme.createSimpleSelectorRoundRectDrawable( - dp(12), accent, _theme("key_featuredStickers_addButtonPressed", accent))) - except Exception: - pass - - button_tv = TextView(act) - button_tv.setText(button_text) - button_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) - button_tv.setGravity(Gravity.CENTER) - button_tv.setPadding(dp(16), dp(14), dp(16), dp(14)) - button_tv.setTextColor(_theme("key_featuredStickers_buttonText")) - try: - button_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) - except Exception: - pass - button_box.addView(button_tv, FrameLayout.LayoutParams(-1, -2)) - - spinner_holder = FrameLayout(act) - spinner_holder.setVisibility(8) - button_box.addView(spinner_holder, FrameLayout.LayoutParams(-1, -1)) - card.addView(button_box, LayoutHelper.createLinear(-1, -2, 0, 6, 0, 0)) + frag = get_last_fragment() + from org.telegram.ui.Stories.recorder import ButtonWithCounterView + button = ButtonWithCounterView(act, True, frag.getResourceProvider() if frag else None) + button.setRound() + button.setText(button_text, False) + card.addView(button, LayoutHelper.createLinear(-1, 48, 0, 6, 0, 0)) + except Exception as e: + logx(f"repos dialog: counter button unavailable: {e}", True) + button = None + + button_tv = None + if button is None: + button = FrameLayout(act) + button.setClickable(True) + button.setFocusable(True) + try: + button.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + dp(12), accent, _theme("key_featuredStickers_addButtonPressed", accent))) + except Exception: + pass + button_tv = TextView(act) + button_tv.setText(button_text) + button_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + button_tv.setGravity(Gravity.CENTER) + button_tv.setPadding(dp(16), dp(14), dp(16), dp(14)) + button_tv.setTextColor(_theme("key_featuredStickers_buttonText")) + try: + button_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + button.addView(button_tv, FrameLayout.LayoutParams(-1, -2)) + card.addView(button, LayoutHelper.createLinear(-1, -2, 0, 6, 0, 0)) class _Ui: def error(self, text): @@ -300,17 +311,11 @@ def loading(self, value): def _apply(): try: - button_box.setEnabled(not value) - button_tv.setAlpha(0.35 if value else 1.0) - if value and spinner_holder.getChildCount() == 0: - try: - from ..PluginListActivity.helpers.uiHelpers import create_circular_loading - spin = create_circular_loading(act, 20) - spinner_holder.addView(spin, FrameLayout.LayoutParams( - AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) - except Exception as e: - logx(f"repos dialog: spinner unavailable: {e}", True) - spinner_holder.setVisibility(0 if value else 8) + button.setEnabled(not value) + if button_tv is None: + button.setLoading(bool(value)) + else: + button_tv.setAlpha(0.4 if value else 1.0) except Exception as e: logx(f"repos dialog: loading paint failed: {e}", False) run_on_ui_thread(_apply) @@ -343,7 +348,7 @@ def _submit(v): ui.loading(False) ui.error(_s("repo_err_unknown", "{0}").replace("{0}", str(e))) - button_box.setOnClickListener(OnClickListener(_submit)) + button.setOnClickListener(OnClickListener(_submit)) overlay.setAlpha(0.0) card.setAlpha(0.0) From 723ddc31d58fe96fe2a3d0801c32ddf8a2d046d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:48:00 +0000 Subject: [PATCH 19/46] Restore the default repository instead of adding another (0.1.2-dev.9) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit restoreDefaultRepository appended the official repository unconditionally, so pressing it with the official repository already in the list left two identical entries, then three. The screen is new, the behaviour is not — the settings list did the same, its only guard being the ten-repository cap. It now looks for the entry first, by rm_rid and by url. If it is there the entry is repaired in place — id and url reset to the official ones, enabled turned back on, an empty name refilled — which is what someone reaching for "restore" after disabling or renaming it is after. Only a genuinely missing repository is appended. The action says which of the two happened: the existing "restored" bulletin, or a new line saying it was already there. The callback also means the bulletin now waits for the fetch instead of firing while it is still in flight. Existing duplicates are not left behind either: updateAllCaches already drops repositories whose rm_rid it has seen, so the copies go on the next refresh. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 3 +- packit/locales/strings_de.json | 3 +- packit/locales/strings_en.json | 3 +- packit/locales/strings_ru.json | 3 +- packit/meta.yml | 2 +- packit/src/RepositoryManager.py | 51 +++++++++++++++++++++----- packit/src/ui/ReposActivity/actions.py | 14 +++++-- 7 files changed, 61 insertions(+), 18 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index a0df5c5..dfc514a 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1205,5 +1205,6 @@ "repo_err_unknown": "Невядомая памылка: {0}", "repos_empty_title": "Пакуль пуста", "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", - "retry": "Паўтарыць" + "retry": "Паўтарыць", + "repo_default_already": "Стандартная крыніца ўжо на месцы" } diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index d35e888..8ffdc12 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1205,5 +1205,6 @@ "repo_err_unknown": "Unbekannter Fehler: {0}", "repos_empty_title": "Noch nichts da", "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", - "retry": "Erneut versuchen" + "retry": "Erneut versuchen", + "repo_default_already": "Die Standardquelle ist bereits vorhanden" } diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 84ac697..026eed7 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1205,5 +1205,6 @@ "repo_err_unknown": "Unknown error: {0}", "repos_empty_title": "Nothing here yet", "repos_empty_text": "Add a source to install plugins", - "retry": "Retry" + "retry": "Retry", + "repo_default_already": "The default repository is already there" } diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 5e82f6d..f033275 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1205,5 +1205,6 @@ "repo_err_unknown": "Неизвестная ошибка: {0}", "repos_empty_title": "Пока пусто", "repos_empty_text": "Добавьте источник, чтобы ставить плагины", - "retry": "Повторить" + "retry": "Повторить", + "repo_default_already": "Стандартный источник уже на месте" } diff --git a/packit/meta.yml b/packit/meta.yml index 6cf6755..59e8a9d 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.1.2-dev.8" +version: "0.1.2-dev.9" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 380ae5b..00bd5c3 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -289,23 +289,54 @@ def updateRepoField(self, idx, field, value): repos[idx][field] = value self.setRepositories(repos) - def restoreDefaultRepository(self): + def restoreDefaultRepository(self, on_done=None): + # "restore" means make sure the official repository is there and usable, + # not append another copy of it: it used to append unconditionally, so + # pressing the item twice left two identical entries, and again a third. def task(): repometa = self._fetch_and_save_repomap(OFFICIAL_REPO_URL) - newRepo = { - "id": repometa.get("rm_rid") if repometa else "shareui_official", - "name": repometa.get("rm_name", strings.official_repository) if repometa else strings.official_repository, - "url": OFFICIAL_REPO_URL, - "enabled": True, - "collapsed": False, - "icon": "chats_pin" - } + rm_rid = repometa.get("rm_rid") if repometa else "shareui_official" + rm_name = repometa.get("rm_name", strings.official_repository) if repometa else strings.official_repository + repos = self.getRepositories() - repos.append(newRepo) + existing = -1 + for i, repo in enumerate(repos): + if str(repo.get("id") or "") == str(rm_rid): + existing = i + break + if str(repo.get("url") or "").strip() == OFFICIAL_REPO_URL: + existing = i + break + + if existing >= 0: + # already present: repair it instead — that is what someone + # reaching for "restore" after disabling or renaming it wants + repos[existing]["id"] = rm_rid + repos[existing]["url"] = OFFICIAL_REPO_URL + repos[existing]["enabled"] = True + if not str(repos[existing].get("name") or "").strip(): + repos[existing]["name"] = rm_name + restored = False + else: + repos.append({ + "id": rm_rid, + "name": rm_name, + "url": OFFICIAL_REPO_URL, + "enabled": True, + "collapsed": False, + "icon": "chats_pin", + }) + restored = True + self.setRepositories(repos) fragment = get_last_fragment() if fragment and hasattr(fragment, "rebuildAllItems"): fragment.rebuildAllItems() + if on_done: + try: + on_done(restored) + except Exception as e: + logx(f"repom: restoreDefaultRepository on_done error: {e}", False) run_serial_io(task) def resetRepositories(self): diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/ReposActivity/actions.py index 5b8914c..a25081f 100644 --- a/packit/src/ui/ReposActivity/actions.py +++ b/packit/src/ui/ReposActivity/actions.py @@ -190,9 +190,17 @@ def restore_default(delegate): if len(repos) >= 10: BulletinHelper.show_error(str(strings.max_repositories_allowed)) return - delegate.repoManager.restoreDefaultRepository() - BulletinHelper.show_success(str(strings.default_repo_restored)) - delegate.reload() + + def _done(restored): + def _ui(): + if restored: + BulletinHelper.show_success(str(strings.default_repo_restored)) + else: + BulletinHelper.show_info(str(strings.repo_default_already)) + delegate.reload() + run_on_ui_thread(_ui) + + delegate.repoManager.restoreDefaultRepository(on_done=_done) def _easter_egg(act, message): From 825a3a8f235ba22037185523e4bc86f81d520c7d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 21:49:11 +0000 Subject: [PATCH 20/46] Say the link was sent, not copied (0.1.2-dev.10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sharing a repository to a chat answered "Уже в буфере обмена!". The link did go to the chat — only the bulletin was wrong. ShareAlert calls didShare() after sending and didCopy() when the user copies instead, and both were reporting the clipboard message, which the settings screen had done too. didShare now says the link was sent, in a new string, and didCopy still returns false so ShareAlert keeps reporting the copy itself. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 3 ++- packit/locales/strings_de.json | 3 ++- packit/locales/strings_en.json | 3 ++- packit/locales/strings_ru.json | 3 ++- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/actions.py | 5 ++++- 6 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index dfc514a..ea5bbf7 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1206,5 +1206,6 @@ "repos_empty_title": "Пакуль пуста", "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", "retry": "Паўтарыць", - "repo_default_already": "Стандартная крыніца ўжо на месцы" + "repo_default_already": "Стандартная крыніца ўжо на месцы", + "repo_link_shared": "Спасылка на крыніцу адпраўлена" } diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 8ffdc12..f86139f 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1206,5 +1206,6 @@ "repos_empty_title": "Noch nichts da", "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", "retry": "Erneut versuchen", - "repo_default_already": "Die Standardquelle ist bereits vorhanden" + "repo_default_already": "Die Standardquelle ist bereits vorhanden", + "repo_link_shared": "Link zur Quelle gesendet" } diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 026eed7..9511d91 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1206,5 +1206,6 @@ "repos_empty_title": "Nothing here yet", "repos_empty_text": "Add a source to install plugins", "retry": "Retry", - "repo_default_already": "The default repository is already there" + "repo_default_already": "The default repository is already there", + "repo_link_shared": "Repository link sent" } diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index f033275..fbae00d 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1206,5 +1206,6 @@ "repos_empty_title": "Пока пусто", "repos_empty_text": "Добавьте источник, чтобы ставить плагины", "retry": "Повторить", - "repo_default_already": "Стандартный источник уже на месте" + "repo_default_already": "Стандартный источник уже на месте", + "repo_link_shared": "Ссылка на источник отправлена" } diff --git a/packit/meta.yml b/packit/meta.yml index 59e8a9d..9bc3fe9 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.1.2-dev.9" +version: "0.1.2-dev.10" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/ReposActivity/actions.py index a25081f..072ab87 100644 --- a/packit/src/ui/ReposActivity/actions.py +++ b/packit/src/ui/ReposActivity/actions.py @@ -102,9 +102,12 @@ def __init__(self): super().__init__() def didShare(self): - run_on_ui_thread(lambda: _bulletin("voip_invite", strings.repo_link_copied)) + # the link went to a chat — saying it is in the clipboard, which + # is what this reported before, describes the other button + run_on_ui_thread(lambda: _bulletin("voip_invite", strings.repo_link_shared)) def didCopy(self): + # false: ShareAlert copies and reports it itself return False alert = ShareAlert(act, None, share_url, True, share_url, False) From 8c8e3dc10b1f942cabbaf786b82be3d5af13adc5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:00:11 +0000 Subject: [PATCH 21/46] Report whether the source is on, not how old its cache is MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status chip on a repository card read the mtime of its cached repomap, so a source that had just been switched off still claimed "up to date" — the one thing the chip sits next to is the switch, and it was describing something else entirely. It now says enabled or disabled, and keeps calling out a source whose repomap never downloaded, since that one is on and still contributes nothing. Cache age was never worth a chip anyway: every start refreshes the caches, so the reading only ever meant the user had been offline. Also brings the round telegram/source buttons down from 36 to 32dp with a 16dp glyph, which is the size the row wanted. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 3 +-- packit/locales/strings_de.json | 3 +-- packit/locales/strings_en.json | 3 +-- packit/locales/strings_ru.json | 3 +-- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 24 +++++++++++++++++------- packit/src/ui/ReposActivity/fragment.py | 12 ++++-------- 7 files changed, 26 insertions(+), 24 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index ea5bbf7..9a28f17 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1172,8 +1172,7 @@ "bi_app_version": "Версія праграмы", "bi_app_package": "Пакет праграмы", "plus_sponsor": "+ Спонсар", - "repo_card_status_ok": "Абноўлены", - "repo_card_status_stale": "Кэш састарэў", + "repo_card_status_enabled": "Уключаны", "repo_card_status_missing": "Не загружаны", "repo_card_status_disabled": "Адключаны", "repo_card_plugins": "{0} плагінаў", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index f86139f..5d6e981 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1172,8 +1172,7 @@ "bi_app_version": "App-Version", "bi_app_package": "App-Paket", "plus_sponsor": "+ Sponsor", - "repo_card_status_ok": "Aktuell", - "repo_card_status_stale": "Cache veraltet", + "repo_card_status_enabled": "Aktiviert", "repo_card_status_missing": "Nicht geladen", "repo_card_status_disabled": "Deaktiviert", "repo_card_plugins": "{0} Plugins", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 9511d91..0edba7c 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1172,8 +1172,7 @@ "bi_app_version": "App version", "bi_app_package": "App package", "plus_sponsor": "+ Sponsor", - "repo_card_status_ok": "Up to date", - "repo_card_status_stale": "Cache is stale", + "repo_card_status_enabled": "Enabled", "repo_card_status_missing": "Not loaded", "repo_card_status_disabled": "Disabled", "repo_card_plugins": "{0} plugins", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index fbae00d..1e0f50c 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1172,8 +1172,7 @@ "bi_app_version": "Версия приложения", "bi_app_package": "Пакет приложения", "plus_sponsor": "+ Спонсор", - "repo_card_status_ok": "Обновлён", - "repo_card_status_stale": "Кэш устарел", + "repo_card_status_enabled": "Включён", "repo_card_status_missing": "Не загружен", "repo_card_status_disabled": "Отключён", "repo_card_plugins": "{0} плагинов", diff --git a/packit/meta.yml b/packit/meta.yml index 9bc3fe9..1375bac 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.1.2-dev.10" +version: "0.1.2-dev.11" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 5de36b9..86398ac 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -53,7 +53,8 @@ def _theme(key: str, fallback: int = 0): return fallback -def _round_icon_button(ctx, icon_name: str, tint: int, on_click, size_dp: int = 36): +def _round_icon_button(ctx, icon_name: str, tint: int, on_click, + size_dp: int = 32, icon_dp: int = 16): btn = FrameLayout(ctx) btn.setClickable(True) btn.setFocusable(True) @@ -78,7 +79,7 @@ def _round_icon_button(ctx, icon_name: str, tint: int, on_click, size_dp: int = except Exception: pass btn.addView(iv, FrameLayout.LayoutParams( - AndroidUtilities.dp(18), AndroidUtilities.dp(18), Gravity.CENTER + AndroidUtilities.dp(icon_dp), AndroidUtilities.dp(icon_dp), Gravity.CENTER )) btn.setOnClickListener(OnClickListener(lambda v: on_click())) apply_press_scale_on_target(btn, btn) @@ -173,10 +174,19 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): chips.setOrientation(LinearLayout.HORIZONTAL) chips.setGravity(Gravity.CENTER_VERTICAL) - status = "disabled" if not enabled else str(info.get("status") or "ok") + # The chip answers "is this source in use", which is the one thing the + # switch beside it is about — it used to report the age of the cache + # instead, so a source the reader had just turned off still said "up to + # date". A source whose repomap never downloaded is called out separately, + # because that one is on and still gives nothing. + if not enabled: + status = "disabled" + elif str(info.get("status") or "") == "missing": + status = "missing" + else: + status = "enabled" status_text, status_key = { - "ok": (getattr(strings, "repo_card_status_ok", "OK"), "key_avatar_backgroundGreen"), - "stale": (getattr(strings, "repo_card_status_stale", "Stale"), "key_windowBackgroundWhiteGrayText"), + "enabled": (getattr(strings, "repo_card_status_enabled", "Enabled"), "key_avatar_backgroundGreen"), "missing": (getattr(strings, "repo_card_status_missing", "Not loaded"), "key_text_RedBold"), "disabled": (getattr(strings, "repo_card_status_disabled", "Disabled"), "key_windowBackgroundWhiteGrayText"), }.get(status, (status, "key_windowBackgroundWhiteGrayText")) @@ -207,8 +217,8 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): src_url = str(info.get("source") or "").strip() on_open = callbacks.get("on_open") or (lambda _u: None) - def _btn_lp(right_margin_dp=8): - lp = LinearLayout.LayoutParams(AndroidUtilities.dp(36), AndroidUtilities.dp(36)) + def _btn_lp(right_margin_dp=6): + lp = LinearLayout.LayoutParams(AndroidUtilities.dp(32), AndroidUtilities.dp(32)) lp.rightMargin = AndroidUtilities.dp(right_margin_dp) return lp diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 6f41645..8aa1873 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -15,7 +15,6 @@ import ctypes import json import os -import time from java import dynamic_proxy from android_utils import run_on_ui_thread, OnClickListener @@ -48,8 +47,6 @@ from ..viewUtils import applyFontToTree from ...utils.paths import getRepoCachePath -_STALE_AFTER = 24 * 60 * 60 - def _c(color: int) -> int: return ctypes.c_int32(color).value @@ -87,11 +84,10 @@ def read_repo_info(repo: dict) -> dict: info["maintainer"] = str(meta.get("rm_maintainer") or "") info["telegram"] = str(meta.get("rm_telegram") or "") info["source"] = str(meta.get("rm_source") or "") - try: - age = time.time() - os.path.getmtime(path) - info["status"] = "stale" if age > _STALE_AFTER else "ok" - except Exception: - info["status"] = "ok" + # the chip reports whether the source is in use, not how old its cache is: + # every start refreshes the caches anyway, so an age reading only ever told + # the reader that they had been offline for a day + info["status"] = "loaded" # a repomap that is itself the plugin list carries the count; the usual # shape only points at it by url, and the screen does not go online to count From 78945aaffb291756a2eb92c0bb45298fefe41c73 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:08:00 +0000 Subject: [PATCH 22/46] Repaint repository cards instead of rebuilding them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flipping a switch wrote the list back through RepositoryManager, which notified the screen, which threw every card away and built it again. A fresh card starts with an empty avatar and fills it from a worker thread, so every icon on screen dropped to its monogram for two frames and faded back in — the blink. Cards now hand back an update() that repaints them in place, and the screen only rebuilds when the sources themselves changed: same ids in the same order means the same cards, with new values. The avatar is kept across a repaint and only swapped when its url actually changes, which takes an updated repomap. Two supporting changes. The icon loader answers from its memory cache on the calling thread, so an icon that has been decoded once is on the view before it is measured, with no fade — the pool hop was itself a visible gap on every rebuild. And the cache key carries the decode size, since the card and the deeplink sheet ask for different ones and the smaller decode looked soft blown up. Cards also get their repo dict from the repaint rather than the one they were built with, so the overflow menu prefills current values after a rename. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 241 ++++++++++++++++-------- packit/src/ui/ReposActivity/fragment.py | 53 +++++- packit/src/ui/ReposActivity/repoIcon.py | 72 +++++-- 4 files changed, 262 insertions(+), 106 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 1375bac..1be74cf 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.1.2-dev.11" +version: "0.1.2-dev.12" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 86398ac..8371582 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -86,12 +86,25 @@ def _round_icon_button(ctx, icon_name: str, tint: int, on_click, return btn -def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): +def _card_background(enabled: bool): + surface = _theme("key_windowBackgroundWhite") + outline = _theme("key_divider") + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(16))) + bg.setColor(surface if enabled else _alpha(surface, 0x80)) + bg.setStroke(AndroidUtilities.dp(1), _alpha(outline, 0xFF if enabled else 0x66)) + return bg + + +def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict, handle: dict = None): """ repo — the stored dict (id / name / url / enabled) info — read off the ui thread from reposCache: maintainer, telegram, - source, plugins, icons, status ("ok"/"stale"/"missing") - callbacks — on_toggle(bool), on_menu(anchor), on_open(url) + source, icon_url, plugins, icons, status ("loaded"/"missing") + callbacks — on_toggle(bool, repo), on_menu(anchor, repo), on_open(url) + handle — filled with {"view", "update"}; call update(repo, info) to + repaint this card in place instead of building another one """ enabled = bool(repo.get("enabled", True)) accent = repoIcon.accent_for(repo) @@ -101,28 +114,22 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): card.setPadding(*(AndroidUtilities.dp(16),) * 4) card.setClickable(True) card.setFocusable(True) - try: - surface = _theme("key_windowBackgroundWhite") - outline = _theme("key_divider") - bg = GradientDrawable() - bg.setShape(GradientDrawable.RECTANGLE) - bg.setCornerRadius(float(AndroidUtilities.dp(16))) - bg.setColor(surface if enabled else _alpha(surface, 0x80)) - bg.setStroke(AndroidUtilities.dp(1), _alpha(outline, 0xFF if enabled else 0x66)) - card.setBackground(bg) - except Exception as e: - logx(f"repos card: background error: {e}", False) # ---- header: avatar | name + maintainer | switch header = LinearLayout(ctx) header.setOrientation(LinearLayout.HORIZONTAL) header.setGravity(Gravity.CENTER_VERTICAL) - icon_view = repoIcon.build_icon_view(ctx, repo, 48, 14) - icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(48), AndroidUtilities.dp(48)) - icon_lp.gravity = Gravity.CENTER_VERTICAL - icon_lp.rightMargin = AndroidUtilities.dp(12) - header.addView(icon_view, icon_lp) + def _icon_lp(): + lp = LinearLayout.LayoutParams(AndroidUtilities.dp(48), AndroidUtilities.dp(48)) + lp.gravity = Gravity.CENTER_VERTICAL + lp.rightMargin = AndroidUtilities.dp(12) + return lp + + icon_url = str(info.get("icon_url") or "") + icon_view = repoIcon.build_icon_view(ctx, repo, 48, 14, icon_url) + icon_holder = [icon_view] # the avatar is swapped only when its url changes + header.addView(icon_view, _icon_lp()) col = LinearLayout(ctx) col.setOrientation(LinearLayout.VERTICAL) @@ -141,14 +148,20 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): pass col.addView(name_tv, LayoutHelper.createLinear(-1, -2)) - sub = str(info.get("maintainer") or "").strip() or _host_of(repo.get("url")) - if sub: - sub_tv = TextView(ctx) - sub_tv.setText(sub) - sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) - sub_tv.setSingleLine(True) - sub_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - col.addView(sub_tv, LayoutHelper.createLinear(-1, -2, 0, 2, 0, 0)) + # always built, hidden when there is nothing to say: the card is repainted + # in place, and a row that only exists sometimes cannot be + sub_tv = TextView(ctx) + sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + sub_tv.setSingleLine(True) + sub_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + col.addView(sub_tv, LayoutHelper.createLinear(-1, -2, 0, 2, 0, 0)) + + def _fill_sub(r, i): + text = str(i.get("maintainer") or "").strip() or _host_of(r.get("url")) + sub_tv.setText(text) + sub_tv.setVisibility(0 if text else 8) # VISIBLE / GONE + + _fill_sub(repo, info) header.addView(col, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) @@ -174,38 +187,42 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): chips.setOrientation(LinearLayout.HORIZONTAL) chips.setGravity(Gravity.CENTER_VERTICAL) - # The chip answers "is this source in use", which is the one thing the - # switch beside it is about — it used to report the age of the cache - # instead, so a source the reader had just turned off still said "up to - # date". A source whose repomap never downloaded is called out separately, - # because that one is on and still gives nothing. - if not enabled: - status = "disabled" - elif str(info.get("status") or "") == "missing": - status = "missing" - else: - status = "enabled" - status_text, status_key = { - "enabled": (getattr(strings, "repo_card_status_enabled", "Enabled"), "key_avatar_backgroundGreen"), - "missing": (getattr(strings, "repo_card_status_missing", "Not loaded"), "key_text_RedBold"), - "disabled": (getattr(strings, "repo_card_status_disabled", "Disabled"), "key_windowBackgroundWhiteGrayText"), - }.get(status, (status, "key_windowBackgroundWhiteGrayText")) - chips.addView(make_info_chip(ctx, str(status_text), status_key), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - - plugins = info.get("plugins") - if isinstance(plugins, int): - chips.addView( - make_info_chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), - "key_windowBackgroundWhiteBlueText"), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - icons_n = info.get("icons") - if isinstance(icons_n, int): - chips.addView( - make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), - "key_avatar_backgroundViolet"), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - + def _fill_chips(is_on, i): + chips.removeAllViews() + # The chip answers "is this source in use", which is the one thing the + # switch beside it is about — it used to report the age of the cache + # instead, so a source the reader had just turned off still said "up to + # date". A source whose repomap never downloaded is called out + # separately, because that one is on and still gives nothing. + if not is_on: + status = "disabled" + elif str(i.get("status") or "") == "missing": + status = "missing" + else: + status = "enabled" + status_text, status_key = { + "enabled": (getattr(strings, "repo_card_status_enabled", "Enabled"), "key_avatar_backgroundGreen"), + "missing": (getattr(strings, "repo_card_status_missing", "Not loaded"), "key_text_RedBold"), + "disabled": (getattr(strings, "repo_card_status_disabled", "Disabled"), "key_windowBackgroundWhiteGrayText"), + }.get(status, (status, "key_windowBackgroundWhiteGrayText")) + chips.addView(make_info_chip(ctx, str(status_text), status_key), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + + plugins = i.get("plugins") + if isinstance(plugins, int): + chips.addView( + make_info_chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), + "key_windowBackgroundWhiteBlueText"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + icons_n = i.get("icons") + if isinstance(icons_n, int): + chips.addView( + make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), + "key_avatar_backgroundViolet"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + + # filled by the first _apply_enabled below, together with the rest of the + # state that depends on the switch card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) # ---- footer: telegram / source, overflow on the right @@ -213,8 +230,6 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict): footer.setOrientation(LinearLayout.HORIZONTAL) footer.setGravity(Gravity.CENTER_VERTICAL) - tg_url = str(info.get("telegram") or "").strip() - src_url = str(info.get("source") or "").strip() on_open = callbacks.get("on_open") or (lambda _u: None) def _btn_lp(right_margin_dp=6): @@ -222,14 +237,26 @@ def _btn_lp(right_margin_dp=6): lp.rightMargin = AndroidUtilities.dp(right_margin_dp) return lp - if tg_url: - footer.addView( - _round_icon_button(ctx, "msg_channel", accent, lambda u=tg_url: on_open(u)), - _btn_lp()) - if src_url: - footer.addView( - _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), - _btn_lp()) + # own container so a repaint can refill it without touching the overflow + links = LinearLayout(ctx) + links.setOrientation(LinearLayout.HORIZONTAL) + links.setGravity(Gravity.CENTER_VERTICAL) + + def _fill_links(i): + links.removeAllViews() + tg_url = str(i.get("telegram") or "").strip() + src_url = str(i.get("source") or "").strip() + if tg_url: + links.addView( + _round_icon_button(ctx, "msg_channel", accent, lambda u=tg_url: on_open(u)), + _btn_lp()) + if src_url: + links.addView( + _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), + _btn_lp()) + + _fill_links(info) + footer.addView(links, LayoutHelper.createLinear(-2, -2)) spacer = View(ctx) footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) @@ -237,7 +264,9 @@ def _btn_lp(right_margin_dp=6): on_menu = callbacks.get("on_menu") menu_btn = _round_icon_button( ctx, "ic_ab_other", _theme("key_windowBackgroundWhiteGrayText"), - lambda: on_menu(menu_holder[0]) if on_menu else None + # state["repo"] and not the dict this card was built from: a repaint + # hands over a freshly parsed one, and the menu prefills its dialogs + lambda: on_menu(menu_holder[0], state["repo"]) if on_menu else None ) menu_holder = [menu_btn] footer.addView(menu_btn, _btn_lp(0)) @@ -247,27 +276,75 @@ def _btn_lp(right_margin_dp=6): # tapping the card flips the switch — it is the only stateful control here, # everything else lives behind explicit buttons on_toggle = callbacks.get("on_toggle") - state = {"enabled": enabled} + state = {"enabled": enabled, "info": info, "repo": repo, "icon_url": icon_url} - def _toggle(_v=None): - state["enabled"] = not state["enabled"] + def _apply_enabled(is_on, animate): + state["enabled"] = is_on + try: + card.setBackground(_card_background(is_on)) + except Exception as e: + logx(f"repos card: background repaint error: {e}", False) try: if switch is not None: - switch.setChecked(state["enabled"], True) + switch.setChecked(is_on, animate) except Exception: pass + _fill_chips(is_on, state["info"]) + target = 1.0 if is_on else 0.55 + for view in (icon_holder[0], col, chips): + try: + if animate: + view.animate().alpha(target).setDuration(160).start() + else: + view.setAlpha(target) + except Exception: + pass + + def _toggle(_v=None): + _apply_enabled(not state["enabled"], True) if on_toggle: - on_toggle(state["enabled"]) + on_toggle(state["enabled"], state["repo"]) + + def _update(new_repo, new_info): + # Repaint, do not rebuild. The avatar in particular survives: rebuilding + # the card meant a fresh ImageView with nothing in it, so flipping a + # switch made every icon on screen blink. + state["repo"] = new_repo + state["info"] = new_info or {} + try: + name_tv.setText(str(new_repo.get("name") or strings.unnamed)) + _fill_sub(new_repo, state["info"]) + _fill_links(state["info"]) + new_url = str(state["info"].get("icon_url") or "") + if new_url != state["icon_url"]: + # only an updated repomap can do this, and then it really is a + # different picture — swap the whole avatar + state["icon_url"] = new_url + try: + header.removeView(icon_holder[0]) + except Exception: + pass + replacement = repoIcon.build_icon_view(ctx, new_repo, 48, 14, new_url) + replacement.setAlpha(1.0 if state["enabled"] else 0.55) + header.addView(replacement, 0, _icon_lp()) + icon_holder[0] = replacement + now = bool(new_repo.get("enabled", True)) + if now != state["enabled"]: + _apply_enabled(now, False) + else: + # the card is already showing this value — most repaints arrive + # right after its own tap, and re-setting alpha mid-animation + # would snap it + _fill_chips(now, state["info"]) + except Exception as e: + logx(f"repos card: update error: {e}", False) card.setOnClickListener(OnClickListener(_toggle)) apply_press_scale_on_target(card, card) - if not enabled: - try: - icon_view.setAlpha(0.55) - col.setAlpha(0.55) - chips.setAlpha(0.55) - except Exception: - pass + _apply_enabled(enabled, False) + if isinstance(handle, dict): + handle["view"] = card + handle["update"] = _update return card diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 8aa1873..30a5653 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -65,7 +65,7 @@ def _theme(key: str, fallback: int = 0): def read_repo_info(repo: dict) -> dict: """Everything the card needs, straight out of the cached repomap.""" - info = {"maintainer": "", "telegram": "", "source": "", + info = {"maintainer": "", "telegram": "", "source": "", "icon_url": "", "plugins": None, "icons": None, "status": "missing"} repo_id = str(repo.get("id") or "") if not repo_id: @@ -84,6 +84,9 @@ def read_repo_info(repo: dict) -> dict: info["maintainer"] = str(meta.get("rm_maintainer") or "") info["telegram"] = str(meta.get("rm_telegram") or "") info["source"] = str(meta.get("rm_source") or "") + icon_url = str(meta.get("rm_icon") or "").strip() + # older repositories put an R.drawable name in rm_icon; only a link is an icon + info["icon_url"] = icon_url if icon_url.lower().startswith(("http://", "https://")) else "" # the chip reports whether the source is in use, not how old its cache is: # every start refreshes the caches anyway, so an age reading only ever told # the reader that they had been offline for a day @@ -110,6 +113,8 @@ def __init__(self, repoManager): self._alive = [True] self._fragment = [None] self._first_build = True + self._handles = [] + self._signature_shown = None # ---------------------------------------------------------------- delegate def onFragmentCreate(self, *_): @@ -118,6 +123,8 @@ def onFragmentCreate(self, *_): def onFragmentDestroy(self, *_): self._alive[0] = False unregister(self) + self._handles = [] + self._signature_shown = None try: if self._root is not None: parent = self._root.getParent() @@ -162,6 +169,9 @@ def beforeCreateView(self): self._list = LinearLayout(act) self._list.setOrientation(LinearLayout.VERTICAL) + # the container is new, so nothing is on screen to repaint + self._handles = [] + self._signature_shown = None content.addView(self._list, LayoutHelper.createLinear(-1, -2)) scroll.addView(content, ScrollView.LayoutParams(-1, -2)) @@ -233,11 +243,32 @@ def _paint(): run_on_queue(_work) - def _render(self, act, repos, infos): - self._list.removeAllViews() + def _signature(self, repos): + return [(str(r.get("id") or ""), str(r.get("url") or "")) for r in repos] + def _render(self, act, repos, infos): self._summary.setText(self._summary_text(len(repos))) + # A repaint is not a rebuild. Flipping a switch writes the list back + # through RepositoryManager, which notifies this screen, which used to + # throw every card away and build it again — and a fresh card starts + # with an empty avatar, so every icon on screen blinked. When the same + # sources are still there in the same order, hand each card its new + # values and let it repaint itself. + signature = self._signature(repos) + if signature and signature == self._signature_shown and len(self._handles) == len(repos): + for idx, repo in enumerate(repos): + update = self._handles[idx].get("update") + if update: + update(repo, infos[idx] if idx < len(infos) else {}) + # a repaint makes fresh chip labels, and those have no font yet + applyFontToTree(self._list) + return + + self._list.removeAllViews() + self._handles = [] + self._signature_shown = signature + if not repos: self._list.addView(self._build_empty_state(act), LayoutHelper.createLinear(-1, -2)) self._first_build = False @@ -246,7 +277,9 @@ def _render(self, act, repos, infos): for idx, repo in enumerate(repos): info = infos[idx] if idx < len(infos) else {} - card = make_repo_card(act, repo, info, self._callbacks_for(act, repo)) + handle = {} + card = make_repo_card(act, repo, info, self._callbacks_for(act, repo), handle) + self._handles.append(handle) lp = LayoutHelper.createLinear(-1, -2, 0, 0, 0, 8) self._list.addView(card, lp) if self._first_build: @@ -292,16 +325,18 @@ def _index_of(self, repo: dict): def _callbacks_for(self, act, repo): from . import actions - def _on_toggle(value): - idx, _ = self._index_of(repo) + # the card passes its own repo dict back: a repaint replaces the one + # captured here with the freshly parsed entry + def _on_toggle(value, current): + idx, _ = self._index_of(current) if idx < 0: self.reload() return - repo["enabled"] = value + current["enabled"] = value self.repoManager.updateRepoField(idx, "enabled", value) - def _on_menu(anchor): - actions.show_card_menu(act, self, repo, anchor) + def _on_menu(anchor, current): + actions.show_card_menu(act, self, current, anchor) def _on_open(url): actions.open_url(act, url) diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index a8cd426..25a94a9 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -135,13 +135,33 @@ def icon_url_for(repo: dict): return None -def _load_bitmap(url: str, px: int): - # memory -> disk -> network, decoded to a px-sized bitmap +def peek_bitmap(url: str, px: int): + # The already-decoded answer, or None. Card rebuilds go through here first: + # routing a known bitmap through the worker pool costs a hop to the pool and + # back to the ui thread, and in those two frames the card shows its + # monogram — which is what made an avatar blink every time the list was + # rebuilt after a toggle. + if not url: + return None + key = _mem_key(url, px) with _lock(): - bmp = _mem.get(url) + bmp = _mem.get(key) if bmp is not None: - _mem.move_to_end(url) - return bmp + _mem.move_to_end(key) + return bmp + + +def _mem_key(url: str, px: int) -> str: + # px is part of the key: the same icon is decoded at different sizes for the + # card and for the deeplink sheet, and the smaller decode looks soft blown up + return f"{url}|{px}" + + +def _load_bitmap(url: str, px: int): + # memory -> disk -> network, decoded to a px-sized bitmap + bmp = peek_bitmap(url, px) + if bmp is not None: + return bmp import os path = getRepoIconCachePath(url) @@ -173,7 +193,7 @@ def _load_bitmap(url: str, px: int): pass return None with _lock(): - _mem[url] = bmp + _mem[_mem_key(url, px)] = bmp while len(_mem) > _MEM_CAP: _mem.popitem(last=False) return bmp @@ -191,6 +211,18 @@ def load_url_into(image_view, url: str, size_dp: int = 48): except Exception: pass + cached = peek_bitmap(url, size_px) + if cached is not None: + try: + image_view.setImageBitmap(cached) + try: + image_view.setColorFilter(None) + except Exception: + pass + return + except Exception as e: + logx(f"repoIcon: cached url bind error: {e}", False) + def _task(): bmp = _load_bitmap(url, size_px) if bmp is None: @@ -213,8 +245,9 @@ def _apply(): imagePool.submit(_task) -def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14): - # monogram now, real icon when it arrives +def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url=None): + # monogram now, real icon when it arrives — unless it has already arrived + # once, in which case it is on screen before the card is size_px = AndroidUtilities.dp(size_dp) accent = accent_for(repo) @@ -249,15 +282,26 @@ def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14): pass holder.addView(image, FrameLayout.LayoutParams(size_px, size_px)) - url = None - try: - url = repo.get("_icon_url") # resolved by the caller when it read the cache - except Exception: - url = None - want = f"packit_repoicon_{_seed(repo)}" holder.setTag(want) + if url is not None and not str(url).strip(): + # the caller read the cache and there is no icon in it — an empty string + # is an answer, unlike None, so no worker goes and reads it again + return holder + + cached = peek_bitmap(str(url or ""), size_px) + if cached is not None: + # straight onto the view, no fade: the icon was already on screen a + # moment ago and fading it back in is exactly what reads as a blink + try: + image.setImageBitmap(cached) + image.setVisibility(0) # VISIBLE + mono.setVisibility(8) + return holder + except Exception as e: + logx(f"repoIcon: cached bind error: {e}", False) + def _task(): target = url if url else icon_url_for(repo) if not target: From 46a27a66d52f09a8aa37a8ea6b3ddc693b19fe9e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:15:43 +0000 Subject: [PATCH 23/46] Cap repository names at 32 characters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rm_name comes out of a remote repomap and had no bound at all: whatever the file said went into settings and onto every screen that draws a repository — the source card, both pickers, the plugin list. A name long enough to matter pushed the switch off the card. The limit lives on the storage boundary rather than on each screen, so there is one rule and no way past it: getRepositories and setRepositories both clamp, which covers the add sheet, the edit dialog, the repo=add deeplink and the startup cache refresh, and also fixes names already on disk from before the limit. Whitespace is collapsed on the way through, since a name with a newline in it is only ever a way to break a single-line layout. Thirty-two is about what a card fits at 17sp on a narrow phone and twelve more than the longest real name so far. The edit field stops accepting characters at the same count instead of dropping them on save, and the card's name and subtitle now ellipsize rather than clipping mid-glyph when they still do not fit. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/RepositoryManager.py | 30 +++++++++++++++++++++++++ packit/src/ui/ReposActivity/addSheet.py | 20 +++++++++++++---- packit/src/ui/ReposActivity/card.py | 6 +++++ 4 files changed, 53 insertions(+), 5 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 1be74cf..f752b3b 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.1.2-dev.12" +version: "0.1.2-dev.13" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 00bd5c3..2878c5a 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -24,6 +24,25 @@ OFFICIAL_REPO_URL = "https://raw.githubusercontent.com/shareui/packit/refs/heads/main/configs/repomap.json" +# A repository name is a label, not a document. It is drawn on one line in the +# source card, in both repository pickers and in the plugin list, and it arrives +# from a remote repomap that is free to put anything at all in rm_name — so the +# limit belongs here, on the way into storage, and not on each screen that has +# to render whatever it finds. Thirty-two is about what the card fits at 17sp on +# a narrow phone, and comfortably more than any real name so far ("exteraGram +# Utilities" is twenty). +REPO_NAME_MAX = 32 + + +def clampRepoName(value) -> str: + text = str(value or "") + # the name is single-line everywhere it appears, so a newline or a tab in it + # is only ever a way to break someone's layout + text = " ".join(text.split()) + if len(text) > REPO_NAME_MAX: + text = text[:REPO_NAME_MAX - 1].rstrip() + "…" + return text + def _get_cache_dir() -> str: from .utils.paths import getReposCacheDir @@ -40,11 +59,22 @@ def getRepositories(self): repos = json.loads(reposJson) if not isinstance(repos, list): return [] + # also on the way out: names stored before the limit existed are + # already on disk, and nothing rewrites them until the list changes + for repo in repos: + if isinstance(repo, dict) and "name" in repo: + repo["name"] = clampRepoName(repo.get("name")) return repos except Exception: return [] def setRepositories(self, repos): + # every write lands here — the add sheet, the edit dialog, the repo=add + # deeplink, the startup cache refresh — so the name limit is applied + # once, in place, and the caller's list matches what was stored + for repo in repos: + if isinstance(repo, dict) and "name" in repo: + repo["name"] = clampRepoName(repo.get("name")) settings.set("repositories", json.dumps(repos), reload_settings=True) try: fragment = get_last_fragment() diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/ReposActivity/addSheet.py index c54796f..5fe4d14 100644 --- a/packit/src/ui/ReposActivity/addSheet.py +++ b/packit/src/ui/ReposActivity/addSheet.py @@ -37,6 +37,7 @@ _attach_keyboard_listener, _detach_keyboard_listener, ) from ...utils.bulletins import factory as _pbf +from ...RepositoryManager import REPO_NAME_MAX # addRepositoryWithUrl answers in lowercase english; the user gets their own # language and, where possible, a hint at what to do about it @@ -92,7 +93,7 @@ def _localize_reason(reason: str) -> str: return _s("repo_err_http", "{0}").replace("{0}", text) -def _make_field(act, label: str, hint: str, value: str, uri: bool): +def _make_field(act, label: str, hint: str, value: str, uri: bool, max_length: int = 0): from android.util import TypedValue from android.text import InputType, TextUtils from android.view import View @@ -115,6 +116,15 @@ def _make_field(act, label: str, hint: str, value: str, uri: bool): edit.setInputType( InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI if uri else InputType.TYPE_CLASS_TEXT ) + if max_length > 0: + # storage clamps the name anyway; the filter is so the field stops + # taking characters instead of quietly dropping them on save + try: + from java import jarray + from android.text import InputFilter + edit.setFilters(jarray(InputFilter)([InputFilter.LengthFilter(max_length)])) + except Exception as e: + logx(f"repos addSheet: length filter unavailable: {e}", True) if value: edit.setText(value) # cursor stays at the start: putting it at the end scrolls a long url so @@ -158,7 +168,7 @@ def onFocusChange(self, v, hasFocus): def _show_form_dialog(act, title: str, subtitle: str, fields: list, button_text: str, on_submit): """ - fields — [{"label","hint","value","uri"}] + fields — [{"label","hint","value","uri","max_length"}] on_submit(values: list[str], ui) — ui.error(text) / ui.loading(bool) / ui.dismiss() """ try: @@ -242,7 +252,8 @@ def _dismiss_from_overlay(v): for i, spec in enumerate(fields): outline, edit = _make_field( act, spec.get("label", ""), spec.get("hint", ""), - spec.get("value", ""), bool(spec.get("uri")) + spec.get("value", ""), bool(spec.get("uri")), + int(spec.get("max_length") or 0) ) card.addView(outline, LayoutHelper.createLinear(-1, -2, 0, 0, 0, 10)) edits.append(edit) @@ -493,7 +504,8 @@ def _done(): _s("repo_sheet_edit_sub", "Name and link"), [ {"label": str(strings.repo_name), "hint": str(strings.repo_name), - "value": str(repo.get("name") or ""), "uri": False}, + "value": str(repo.get("name") or ""), "uri": False, + "max_length": REPO_NAME_MAX}, {"label": str(strings.repo_url), "hint": "https://…/repomap.json", "value": str(repo.get("url") or ""), "uri": True}, ], diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 8371582..8dc270d 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -13,6 +13,7 @@ from android.widget import LinearLayout, TextView, FrameLayout, ImageView from android.view import View, Gravity +from android.text import TextUtils from android.util import TypedValue from android.graphics.drawable import GradientDrawable from android_utils import OnClickListener @@ -138,6 +139,10 @@ def _icon_lp(): name_tv.setText(str(repo.get("name") or strings.unnamed)) name_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 17) name_tv.setSingleLine(True) + # setSingleLine on its own only clips, and it clips mid-glyph: a name that + # is within the stored limit but still too wide for a narrow screen has to + # end in an ellipsis, not in half a letter + name_tv.setEllipsize(TextUtils.TruncateAt.END) name_tv.setTextColor(_theme("key_windowBackgroundWhiteBlackText")) try: name_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) @@ -153,6 +158,7 @@ def _icon_lp(): sub_tv = TextView(ctx) sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) sub_tv.setSingleLine(True) + sub_tv.setEllipsize(TextUtils.TruncateAt.END) sub_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) col.addView(sub_tv, LayoutHelper.createLinear(-1, -2, 0, 2, 0, 0)) From 979513b15e512aa6c7e1abc3582b51ddfe9eca73 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:20:27 +0000 Subject: [PATCH 24/46] Stop clipping the switch on repository cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit exteraGram has a second switch style, and with it on the toggle is an md3 pill wider than the 37dp box it is laid out in — Switch centres it there, so it overhangs both ends on purpose. Switch.getOverlayPadding says as much: five dp with the new style, zero with the old, and every cell in the client that hosts one turns off child clipping to let it paint. PluginCell, which this card took its switch from, does both setClipChildren(false) and setClipToPadding(false) on itself; the box and the colours came across and that did not, so the card sheared the ends off the pill. The card's own 16dp padding is more than the overhang needs, so the switch stays well inside the card outline. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packit/meta.yml b/packit/meta.yml index f752b3b..e9644b1 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.1.2-dev.13" +version: "0.1.2-dev.14" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 8dc270d..623f569 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -115,11 +115,21 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict, handle: dict = card.setPadding(*(AndroidUtilities.dp(16),) * 4) card.setClickable(True) card.setFocusable(True) + # PluginCell does both of these on itself, and this is why: with exteraGram's + # new switch style the toggle is an md3 pill wider than the 37dp box it is + # laid out in, and Switch centres it, so it hangs over both edges by design. + # Every cell in the client that hosts one stops clipping — copying the box + # and the colours without this is what sheared the ends off ours. The 16dp + # padding has room to spare for the overhang. + card.setClipChildren(False) + card.setClipToPadding(False) # ---- header: avatar | name + maintainer | switch header = LinearLayout(ctx) header.setOrientation(LinearLayout.HORIZONTAL) header.setGravity(Gravity.CENTER_VERTICAL) + header.setClipChildren(False) + header.setClipToPadding(False) def _icon_lp(): lp = LinearLayout.LayoutParams(AndroidUtilities.dp(48), AndroidUtilities.dp(48)) From 748632ee90f11f3b2816c993d35ee2dd8be413d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:27:13 +0000 Subject: [PATCH 25/46] Do not let a repository's name decide whether it works MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clearing the name in the edit dialog made both catalogues refuse to open: the plugin list and the icon list each collected sources with `if name and url`, so a renamed-to-nothing source silently dropped out of the list, and with none left the screen answered "no repositories". Nothing downstream needs the name — it is a label, and both picker sheets already print "unnamed" when it is missing. They now gate on the url alone. That was the half that broke; the other half is that the state was reachable at all. The edit dialog rejects an empty name the way it already rejects an empty link, and updateAllCaches — which parses repometa on every start anyway — puts rm_name back on any source that is sitting there without one, so installs that already hit this heal themselves on the next launch. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 1 + packit/locales/strings_de.json | 1 + packit/locales/strings_en.json | 1 + packit/locales/strings_ru.json | 1 + packit/meta.yml | 2 +- packit/src/RepositoryManager.py | 10 ++++++++++ packit/src/ui/IconsListActivity/fragment.py | 9 +++++---- packit/src/ui/PluginListActivity/fragment.py | 7 +++++-- packit/src/ui/ReposActivity/addSheet.py | 7 ++++++- 9 files changed, 31 insertions(+), 8 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 9a28f17..58d69bf 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1186,6 +1186,7 @@ "repo_sheet_edit_title": "Змяніць крыніцу", "repo_sheet_edit_sub": "Назва і спасылка", "repo_err_empty": "Увядзіце спасылку", + "repo_err_name_empty": "Увядзіце назву", "repo_err_scheme": "Спасылка мусіць пачынацца з https://", "repo_err_duplicate": "Гэтая крыніца ўжо дададзена", "repo_err_not_found": "Файл не знойдзены", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 5d6e981..9b8ab12 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1186,6 +1186,7 @@ "repo_sheet_edit_title": "Quelle bearbeiten", "repo_sheet_edit_sub": "Name und Link", "repo_err_empty": "Link eingeben", + "repo_err_name_empty": "Namen eingeben", "repo_err_scheme": "Der Link muss mit https:// beginnen", "repo_err_duplicate": "Diese Quelle ist bereits hinzugefügt", "repo_err_not_found": "Datei nicht gefunden", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 0edba7c..dad859f 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1186,6 +1186,7 @@ "repo_sheet_edit_title": "Edit source", "repo_sheet_edit_sub": "Name and link", "repo_err_empty": "Enter a link", + "repo_err_name_empty": "Enter a name", "repo_err_scheme": "The link must start with https://", "repo_err_duplicate": "This source is already added", "repo_err_not_found": "File not found", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 1e0f50c..20e19ac 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1186,6 +1186,7 @@ "repo_sheet_edit_title": "Изменить источник", "repo_sheet_edit_sub": "Название и ссылка", "repo_err_empty": "Введите ссылку", + "repo_err_name_empty": "Введите название", "repo_err_scheme": "Ссылка должна начинаться с https://", "repo_err_duplicate": "Этот источник уже добавлен", "repo_err_not_found": "Файл не найден", diff --git a/packit/meta.yml b/packit/meta.yml index e9644b1..54ee1df 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.1.2-dev.14" +version: "0.1.2-dev.15" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 2878c5a..4bbdcef 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -438,6 +438,16 @@ def task(): changed = True logx(f"updateAllCaches: set id='{rm_rid}' for repo '{repo.get('name')}'", True) + # a repository left without a name shows up as "unnamed" + # everywhere; the repomap has one, and this runs on every + # start, so take it back rather than leave it that way + if not str(repo.get("name") or "").strip(): + rm_name = str(repometa.get("rm_name") or "").strip() + if rm_name: + repos[i]["name"] = rm_name + changed = True + logx(f"updateAllCaches: restored name '{rm_name}' for '{rm_rid}'", True) + cache_path = os.path.join(cache_dir, f"{rm_rid}.json") with open(cache_path, "w", encoding="utf-8") as f: json.dump(data, f, indent=2, ensure_ascii=False) diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 6cef5a3..8bf4490 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -309,9 +309,10 @@ def _icons_show_repo_menu(self, act): 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 only: a nameless source still serves icons, and the sheet + # already labels it "unnamed" url = str(r.get("url") or "").strip() - if name and url: + if url: repos.append(r) except Exception: pass @@ -629,10 +630,10 @@ def open(self): if not r.get("enabled"): logx(f"IconList.open: skipping disabled repo '{name}'", True) continue - if name and url: + if url: repos.append(r) else: - logx(f"IconList.open: skipping repo with empty name or url: name='{name}' url='{url}'", True) + logx(f"IconList.open: skipping repo with empty url: name='{name}'", True) except Exception as e: logx(f"IconList.open: error processing repo: {e}", False) continue diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index 205cb17..d711f98 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -198,9 +198,12 @@ def open(self): try: if not r or not r.get("enabled"): continue - name = str(r.get("name") or "").strip() + # the url is what makes a repository usable; the name is a + # label, and requiring one meant a source someone had + # renamed to nothing dropped out of the catalogue — with + # every source nameless, the screen refused to open at all url = str(r.get("url") or "").strip() - if name and url: + if url: repos.append(r) except Exception: continue diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/ReposActivity/addSheet.py index 5fe4d14..ed07a1b 100644 --- a/packit/src/ui/ReposActivity/addSheet.py +++ b/packit/src/ui/ReposActivity/addSheet.py @@ -453,8 +453,13 @@ def _added(delegate): def show_edit_repo_dialog(act, delegate, repo: dict): def _submit(values, ui): - name = values[0] + name = " ".join(str(values[0] or "").split()) url = _normalize_url(values[1]) if len(values) > 1 else "" + if not name: + # a source saved without one showed up everywhere as "unnamed", and + # there is nothing here to fall back to — say so instead of storing it + ui.error(_s("repo_err_name_empty", "Enter a name")) + return if not url: ui.error(_s("repo_err_empty", "Enter a link")) return From 8d6326547bfbc7a346673f77f2f785baf461ce0f Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:45:54 +0000 Subject: [PATCH 26/46] Split the card's tap from its switch, and fix back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four things on the sources screen. The outline is gone. It was there to tell these cards apart from the plugin ones, but a 1dp divider-coloured stroke reads as a stray line rather than as a container; the fill against the gray window is what the client's own cards do. Turning a source on and off is now the switch's job alone. Tapping the card used to flip it, which left the two controls doing the same thing in the same place — the card opens the source's sheet instead. The switch takes its own taps, in a 56x48 box so the target covers the whole pill the new switch style draws, and drives its press ripple the way the client's cells do, off setDrawRipple. The card keeps its press scale. The sheet itself is a placeholder: a handle, the avatar, the name and the maintainer. The shell is real so the content can go under the header later. Back was broken, gesture and button both. UniversalFragment negates the delegate's answer — it does `return !delegate.onBackPressed()` and only finishes on true — so returning True swallowed the press. Every other fragment in the plugin returns False; this one does now too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 74 ++++++++----- packit/src/ui/ReposActivity/fragment.py | 14 ++- packit/src/ui/ReposActivity/repoSheet.py | 131 +++++++++++++++++++++++ 4 files changed, 194 insertions(+), 27 deletions(-) create mode 100644 packit/src/ui/ReposActivity/repoSheet.py diff --git a/packit/meta.yml b/packit/meta.yml index 54ee1df..3ffc5c6 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.1.2-dev.15" +version: "0.1.2-dev.16" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 623f569..0a581c5 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -88,13 +88,15 @@ def _round_icon_button(ctx, icon_name: str, tint: int, on_click, def _card_background(enabled: bool): + # Filled, no outline. The border was there to set these cards apart from the + # plugin ones, but a 1dp divider-coloured stroke reads as a stray line + # rather than as a container — the fill against key_windowBackgroundGray is + # what the client's own cards do and it is enough on its own. surface = _theme("key_windowBackgroundWhite") - outline = _theme("key_divider") bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) bg.setCornerRadius(float(AndroidUtilities.dp(16))) bg.setColor(surface if enabled else _alpha(surface, 0x80)) - bg.setStroke(AndroidUtilities.dp(1), _alpha(outline, 0xFF if enabled else 0x66)) return bg @@ -181,19 +183,23 @@ def _fill_sub(r, i): header.addView(col, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - switch = _build_switch(ctx, enabled) + # _toggle is defined further down; the lambda resolves it when tapped + switch = _build_switch(ctx, enabled, lambda: _toggle()) if switch is not None: - # Explicit params, not LayoutHelper.createLinear(37, 20, gravity, …): - # that call has a (w, h, float weight, …) twin, and picking it gives the + # Explicit params, not LayoutHelper.createLinear(w, h, gravity, …): that + # call has a (w, h, float weight, …) twin, and picking it gives the # switch a weight instead of a gravity. In a row that already has a # weighted column the switch then absorbs the overflow and is measured - # narrower than the 31dp track Switch.onDraw centres in it, so the track - # is clipped by the view bounds — which is what turned the pill into a + # narrower than the track Switch.onDraw centres in it, so the track is + # clipped by the view bounds — which is what turned the pill into a # rectangle with square corners. - sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(40)) + # + # 56x48 rather than the client's 37x40: the switch is the touch target + # now that the card no longer toggles, and the box has to cover the + # whole pill the new switch style draws, not just the middle of it. + sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(56), AndroidUtilities.dp(48)) sw_lp.gravity = Gravity.CENTER_VERTICAL - sw_lp.leftMargin = AndroidUtilities.dp(10) - switch.setMinimumWidth(AndroidUtilities.dp(37)) + sw_lp.leftMargin = AndroidUtilities.dp(6) header.addView(switch, sw_lp) card.addView(header, LayoutHelper.createLinear(-1, -2)) @@ -289,9 +295,10 @@ def _fill_links(i): card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) - # tapping the card flips the switch — it is the only stateful control here, - # everything else lives behind explicit buttons + # the card opens the source's sheet; the switch beside it is what turns the + # source on and off, so a tap meant for one is never the other on_toggle = callbacks.get("on_toggle") + on_open_card = callbacks.get("on_open_card") state = {"enabled": enabled, "info": info, "repo": repo, "icon_url": icon_url} def _apply_enabled(is_on, animate): @@ -355,7 +362,9 @@ def _update(new_repo, new_info): except Exception as e: logx(f"repos card: update error: {e}", False) - card.setOnClickListener(OnClickListener(_toggle)) + card.setOnClickListener(OnClickListener( + lambda _v: on_open_card(state["repo"], state["info"]) if on_open_card else None + )) apply_press_scale_on_target(card, card) _apply_enabled(enabled, False) if isinstance(handle, dict): @@ -364,14 +373,10 @@ def _update(new_repo, new_info): return card -def _build_switch(ctx, checked: bool): - # Set up exactly the way the client sets up the switch in its own plugin - # card (PluginCell): the same colour keys, and the same 37x40 box. - # - # The height matters. Switch.onDraw centres a 14dp track and then a 20dp - # thumb circle at the middle of the view, so at a 20dp-tall box the circle - # spans the full height and its top and bottom are shaved off by the view - # bounds — which is most of what made the toggle look square. +def _build_switch(ctx, checked: bool, on_toggle): + # Coloured the way the client colours the switch in its own plugin card + # (PluginCell). Unlike that one it takes its own taps: the card opens a + # sheet now, so turning a source on and off is the switch's job alone. try: from org.telegram.ui.Components import Switch as TgSwitch sw = TgSwitch(ctx) @@ -383,9 +388,30 @@ def _build_switch(ctx, checked: bool): except Exception as e: logx(f"repos card: switch colors unavailable: {e}", True) sw.setChecked(checked, False) - # taps are handled by the whole card, the switch only reflects state - sw.setClickable(False) - sw.setFocusable(False) + sw.setClickable(True) + sw.setFocusable(True) + sw.setOnClickListener(OnClickListener(lambda v: on_toggle())) + + # Switch has no touch handling of its own — the cells that host it drive + # its ripple from their own setPressed. Nothing overrides setPressed + # here, so the press is forwarded by hand, and the listener returns + # False so the click still goes through the normal path. + try: + from java import dynamic_proxy + from android.view import View as _View + + class _Press(dynamic_proxy(_View.OnTouchListener)): + def onTouch(self, v, event): + action = event.getActionMasked() + if action == 0: # DOWN + sw.setDrawRipple(True) + elif action in (1, 3): # UP, CANCEL + sw.setDrawRipple(False) + return False + + sw.setOnTouchListener(_Press()) + except Exception as e: + logx(f"repos card: switch ripple unavailable: {e}", True) return sw except Exception as e: logx(f"repos card: switch unavailable: {e}", False) diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 30a5653..566b0b7 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -196,7 +196,12 @@ def getTitle(self): return "Repositories" def onBackPressed(self): - return True + # UniversalFragment negates this before deciding: it does + # `return !delegate.onBackPressed()` and only calls finishFragment when + # that is true. Returning True here therefore swallowed both the button + # and the gesture. False means "nothing to handle, go ahead and close", + # which is what every other fragment in the plugin returns. + return False def fillItems(self, items, adapter): pass @@ -338,10 +343,15 @@ def _on_toggle(value, current): def _on_menu(anchor, current): actions.show_card_menu(act, self, current, anchor) + def _on_open_card(current, info): + from .repoSheet import show_repo_sheet + show_repo_sheet(act, current, info) + def _on_open(url): actions.open_url(act, url) - return {"on_toggle": _on_toggle, "on_menu": _on_menu, "on_open": _on_open} + return {"on_toggle": _on_toggle, "on_menu": _on_menu, "on_open": _on_open, + "on_open_card": _on_open_card} # ------------------------------------------------------------------ pieces def _build_summary_row(self, act): diff --git a/packit/src/ui/ReposActivity/repoSheet.py b/packit/src/ui/ReposActivity/repoSheet.py new file mode 100644 index 0000000..576c2b7 --- /dev/null +++ b/packit/src/ui/ReposActivity/repoSheet.py @@ -0,0 +1,131 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# The sheet behind a repository card. +# +# Tapping a card used to flip its switch; the switch does that itself now, and +# the card opens this instead. What goes in it is not decided yet, so for the +# moment it carries the source's identity and nothing else — the shell is real +# so that filling it is a matter of adding views under the header. + +from packutil import logx +import ctypes + +from android.widget import LinearLayout, TextView +from android.view import Gravity +from android.util import TypedValue +from android_utils import run_on_ui_thread +from client_utils import get_last_fragment + +try: + from org.telegram.ui.ActionBar import BottomSheet, Theme + from org.telegram.ui.Components import LayoutHelper + from org.telegram.messenger import AndroidUtilities +except Exception as e: + import android_utils as _au; _au.log(f"repoSheet: import telegram classes failed: {e}") + +try: + from elyx import strings +except Exception as e: + import android_utils as _au; _au.log(f"repoSheet: import elyx strings failed: {e}") + +from . import repoIcon +from ..viewUtils import applyFontToTree +from ..PluginListActivity.helpers.uiHelpers import setup_bottom_sheet, create_rounded_bg + + +def _c(color: int) -> int: + return ctypes.c_int32(color).value + + +def _theme(key: str, fallback: int = 0): + try: + return Theme.getColor(getattr(Theme, key)) + except Exception: + return fallback + + +def _handle(ctx): + from android.graphics.drawable import GradientDrawable + bar = TextView(ctx) + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(2))) + bg.setColor(_c((0x3D << 24) | (_theme("key_sheet_scrollUp") & 0xFFFFFF))) + bar.setBackground(bg) + return bar + + +def show_repo_sheet(act, repo: dict, info: dict = None): + info = info or {} + + def _show(): + try: + frag = get_last_fragment() + sheet = BottomSheet(act, False, frag.getResourceProvider() if frag else None) + setup_bottom_sheet(sheet) + + root = LinearLayout(act) + root.setOrientation(LinearLayout.VERTICAL) + root.setPadding(AndroidUtilities.dp(20), AndroidUtilities.dp(10), + AndroidUtilities.dp(20), AndroidUtilities.dp(20)) + try: + root.setBackground(create_rounded_bg(_theme("key_dialogBackground"))) + except Exception: + root.setBackgroundColor(_theme("key_dialogBackground")) + + handle_lp = LayoutHelper.createLinear(36, 4, Gravity.CENTER_HORIZONTAL, 0, 0, 0, 14) + root.addView(_handle(act), handle_lp) + + header = LinearLayout(act) + header.setOrientation(LinearLayout.HORIZONTAL) + header.setGravity(Gravity.CENTER_VERTICAL) + + icon = repoIcon.build_icon_view(act, repo, 52, 15, str(info.get("icon_url") or "")) + icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(52), AndroidUtilities.dp(52)) + icon_lp.rightMargin = AndroidUtilities.dp(14) + header.addView(icon, icon_lp) + + col = LinearLayout(act) + col.setOrientation(LinearLayout.VERTICAL) + + name = TextView(act) + name.setText(str(repo.get("name") or strings.unnamed)) + name.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20) + name.setTextColor(_theme("key_dialogTextBlack")) + name.setSingleLine(True) + try: + name.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass + col.addView(name, LayoutHelper.createLinear(-1, -2)) + + sub_text = str(info.get("maintainer") or "").strip() or _host_of(repo.get("url")) + if sub_text: + sub = TextView(act) + sub.setText(sub_text) + sub.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + sub.setTextColor(_theme("key_dialogTextGray2")) + sub.setSingleLine(True) + col.addView(sub, LayoutHelper.createLinear(-1, -2, 0, 3, 0, 0)) + + header.addView(col, LayoutHelper.createLinear(-1, -2)) + root.addView(header, LayoutHelper.createLinear(-1, -2)) + + sheet.setCustomView(root) + applyFontToTree(root) + sheet.show() + except Exception as e: + logx(f"repoSheet: show error: {e}", False) + + run_on_ui_thread(_show) + + +def _host_of(url) -> str: + try: + text = str(url or "") + if "://" in text: + text = text.split("://", 1)[1] + return text.split("/", 1)[0] + except Exception: + return "" From fa3e4cca97e7d3fbd4992607a8d28ed0eb8cdddb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:49:46 +0000 Subject: [PATCH 27/46] Make the sources toolbar the same component as the catalogue's MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The count and the round button above the list were a loose grey caption next to a floating circle — not a control strip. The plugin catalogue already has this exact row and it reads right, so this one is built the same way (listView.py): a 44dp frame, the count centred in a 16dp-radius pill on the card surface, the bulk-actions button the same shape beside it. The on/off chip is gone. It spelled out in words what the switch an inch away says by being on or off. What stays is the case the switch cannot show: a source that is switched on but whose repomap never downloaded. With nothing left to put in it the chip row usually has no children at all, so it goes GONE and takes its top margin with it instead of leaving a gap. The maintainer line runs through LocaleUtils.fullyFormatText now, the way the plugin screens format theirs — rm_maintainer is free text and reads like a message. On the card it is formatted but not tappable: a movement method makes a TextView clickable, and this one sits in a card whose tap opens the sheet. In the sheet the mention is a real link, since nothing there competes for the touch. Co-Authored-By: Claude Opus 5 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/src/ui/ReposActivity/card.py | 45 +++++++++------- packit/src/ui/ReposActivity/fragment.py | 67 +++++++++++++++++------- packit/src/ui/ReposActivity/repoSheet.py | 12 ++++- 8 files changed, 87 insertions(+), 47 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 58d69bf..0a0c8cc 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1172,9 +1172,7 @@ "bi_app_version": "Версія праграмы", "bi_app_package": "Пакет праграмы", "plus_sponsor": "+ Спонсар", - "repo_card_status_enabled": "Уключаны", "repo_card_status_missing": "Не загружаны", - "repo_card_status_disabled": "Адключаны", "repo_card_plugins": "{0} плагінаў", "repo_card_icons": "{0} набораў", "repo_edit": "Змяніць", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 9b8ab12..9020497 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1172,9 +1172,7 @@ "bi_app_version": "App-Version", "bi_app_package": "App-Paket", "plus_sponsor": "+ Sponsor", - "repo_card_status_enabled": "Aktiviert", "repo_card_status_missing": "Nicht geladen", - "repo_card_status_disabled": "Deaktiviert", "repo_card_plugins": "{0} Plugins", "repo_card_icons": "{0} Icon-Sets", "repo_edit": "Bearbeiten", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index dad859f..74bcc10 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1172,9 +1172,7 @@ "bi_app_version": "App version", "bi_app_package": "App package", "plus_sponsor": "+ Sponsor", - "repo_card_status_enabled": "Enabled", "repo_card_status_missing": "Not loaded", - "repo_card_status_disabled": "Disabled", "repo_card_plugins": "{0} plugins", "repo_card_icons": "{0} icon packs", "repo_edit": "Edit", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 20e19ac..26babfe 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1172,9 +1172,7 @@ "bi_app_version": "Версия приложения", "bi_app_package": "Пакет приложения", "plus_sponsor": "+ Спонсор", - "repo_card_status_enabled": "Включён", "repo_card_status_missing": "Не загружен", - "repo_card_status_disabled": "Отключён", "repo_card_plugins": "{0} плагинов", "repo_card_icons": "{0} наборов", "repo_edit": "Изменить", diff --git a/packit/meta.yml b/packit/meta.yml index 3ffc5c6..28b1dd6 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.1.2-dev.16" +version: "0.1.2-dev.17" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 0a581c5..500424c 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -176,7 +176,19 @@ def _icon_lp(): def _fill_sub(r, i): text = str(i.get("maintainer") or "").strip() or _host_of(r.get("url")) - sub_tv.setText(text) + # rm_maintainer is free text from the repomap and reads like a message — + # "@name", "ROBOT (список от @name)" — so it goes through the client's + # own formatter, which resolves mentions, emoji and markdown. + # + # No LinkMovementMethod here on purpose: it makes a TextView clickable, + # and this one sits inside a card whose tap opens the source's sheet. A + # tappable mention belongs in that sheet, where nothing competes for it. + try: + from com.exteragram.messenger.utils.text import LocaleUtils + sub_tv.setText(LocaleUtils.fullyFormatText(text)) + except Exception as e: + logx(f"repos card: maintainer format unavailable: {e}", True) + sub_tv.setText(text) sub_tv.setVisibility(0 if text else 8) # VISIBLE / GONE _fill_sub(repo, info) @@ -211,24 +223,16 @@ def _fill_sub(r, i): def _fill_chips(is_on, i): chips.removeAllViews() - # The chip answers "is this source in use", which is the one thing the - # switch beside it is about — it used to report the age of the cache - # instead, so a source the reader had just turned off still said "up to - # date". A source whose repomap never downloaded is called out - # separately, because that one is on and still gives nothing. - if not is_on: - status = "disabled" - elif str(i.get("status") or "") == "missing": - status = "missing" - else: - status = "enabled" - status_text, status_key = { - "enabled": (getattr(strings, "repo_card_status_enabled", "Enabled"), "key_avatar_backgroundGreen"), - "missing": (getattr(strings, "repo_card_status_missing", "Not loaded"), "key_text_RedBold"), - "disabled": (getattr(strings, "repo_card_status_disabled", "Disabled"), "key_windowBackgroundWhiteGrayText"), - }.get(status, (status, "key_windowBackgroundWhiteGrayText")) - chips.addView(make_info_chip(ctx, str(status_text), status_key), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + # No on/off chip. It said in words what the switch an inch away says by + # being on or off, and two controls reporting one fact is one too many. + # A source whose repomap never downloaded still gets a chip, because + # nothing else on the card says that — it is switched on and gives + # nothing, which the switch cannot show. + if is_on and str(i.get("status") or "") == "missing": + chips.addView( + make_info_chip(ctx, str(getattr(strings, "repo_card_status_missing", "Not loaded")), + "key_text_RedBold"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) plugins = i.get("plugins") if isinstance(plugins, int): @@ -242,6 +246,9 @@ def _fill_chips(is_on, i): make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), "key_avatar_backgroundViolet"), LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + # with the on/off chip gone most sources have nothing to put here; GONE + # takes the row's top margin with it instead of leaving a gap + chips.setVisibility(0 if chips.getChildCount() > 0 else 8) # filled by the first _apply_enabled below, together with the rest of the # state that depends on the switch diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 566b0b7..9118c70 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -165,7 +165,9 @@ def beforeCreateView(self): content.setPadding(AndroidUtilities.dp(12), AndroidUtilities.dp(8), AndroidUtilities.dp(12), AndroidUtilities.dp(96)) - content.addView(self._build_summary_row(act), LayoutHelper.createLinear(-1, -2)) + # 44dp, the same strip height the catalogue's toolbar uses + content.addView(self._build_summary_row(act), + LayoutHelper.createLinear(-1, 44, 0, 2, 0, 6)) self._list = LinearLayout(act) self._list.setOrientation(LinearLayout.VERTICAL) @@ -355,29 +357,58 @@ def _on_open(url): # ------------------------------------------------------------------ pieces def _build_summary_row(self, act): - # the bulk actions the old screen kept under "Дополнительно" live behind - # the button on the right of this row - row = LinearLayout(act) - row.setOrientation(LinearLayout.HORIZONTAL) - row.setGravity(Gravity.CENTER_VERTICAL) - row.setPadding(AndroidUtilities.dp(6), 0, 0, AndroidUtilities.dp(8)) + # Built the way the plugin catalogue builds its own toolbar + # (listView.py): a 44dp frame, the count centred in a 16dp-radius pill + # on the card surface, icon buttons of the same shape on the right. A + # loose grey caption over a floating circle did not read as a control + # strip at all — this is the same component the rest of the plugin uses. + from ..PluginListActivity.helpers.uiHelpers import get_theme_colors, apply_press_scale_on_target + colors = get_theme_colors() + card_bg = colors.get("card_bg_color") + card_pressed = colors.get("card_pressed_color") + text_color = colors.get("text_color") + + row = FrameLayout(act) self._summary = TextView(act) - self._summary.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) - self._summary.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - row.addView(self._summary, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - - from .card import _round_icon_button + self._summary.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16) + self._summary.setGravity(Gravity.CENTER) + self._summary.setPadding(AndroidUtilities.dp(12), AndroidUtilities.dp(7), + AndroidUtilities.dp(12), AndroidUtilities.dp(7)) + self._summary.setTextColor(text_color) + try: + self._summary.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + AndroidUtilities.dp(16), card_bg, card_bg)) + except Exception as e: + logx(f"repos fragment: summary pill background error: {e}", True) + row.addView(self._summary, FrameLayout.LayoutParams(-2, -2, Gravity.CENTER)) - def _menu(): + # the bulk actions the old screen kept under "Дополнительно" + def _menu(v=None): from . import actions actions.show_bulk_menu(act, self, menu_btn) - menu_btn = _round_icon_button( - act, "msg_customize", _theme("key_windowBackgroundWhiteGrayText"), _menu, 34) - menu_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(34), AndroidUtilities.dp(34)) - menu_lp.gravity = Gravity.CENTER_VERTICAL - row.addView(menu_btn, menu_lp) + menu_btn = FrameLayout(act) + menu_btn.setClickable(True) + menu_btn.setFocusable(True) + try: + menu_btn.setBackground(Theme.createSimpleSelectorRoundRectDrawable( + AndroidUtilities.dp(16), card_bg, card_pressed)) + except Exception: + pass + menu_btn.setPadding(*(AndroidUtilities.dp(8),) * 4) + menu_icon = ImageView(act) + try: + menu_icon.setImageResource(getattr(R_tg.drawable, "msg_customize")) + menu_icon.setColorFilter(text_color) + except Exception: + pass + menu_btn.addView(menu_icon, FrameLayout.LayoutParams( + AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) + menu_btn.setOnClickListener(OnClickListener(_menu)) + apply_press_scale_on_target(menu_btn, menu_btn) + row.addView(menu_btn, FrameLayout.LayoutParams( + -2, -2, Gravity.RIGHT | Gravity.CENTER_VERTICAL)) return row def _build_add_button(self, act): diff --git a/packit/src/ui/ReposActivity/repoSheet.py b/packit/src/ui/ReposActivity/repoSheet.py index 576c2b7..11ed320 100644 --- a/packit/src/ui/ReposActivity/repoSheet.py +++ b/packit/src/ui/ReposActivity/repoSheet.py @@ -103,10 +103,20 @@ def _show(): sub_text = str(info.get("maintainer") or "").strip() or _host_of(repo.get("url")) if sub_text: sub = TextView(act) - sub.setText(sub_text) sub.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) sub.setTextColor(_theme("key_dialogTextGray2")) sub.setSingleLine(True) + # here the mention can be tapped: unlike on the card, nothing + # else in this row wants the touch + try: + from com.exteragram.messenger.utils.text import LocaleUtils + from android.text.method import LinkMovementMethod + sub.setText(LocaleUtils.fullyFormatText(sub_text)) + sub.setLinkTextColor(_theme("key_dialogTextBlue")) + sub.setMovementMethod(LinkMovementMethod.getInstance()) + except Exception as e: + logx(f"repoSheet: maintainer format unavailable: {e}", True) + sub.setText(sub_text) col.addView(sub, LayoutHelper.createLinear(-1, -2, 0, 3, 0, 0)) header.addView(col, LayoutHelper.createLinear(-1, -2)) From 2ae762d932319991bf590dd6b0cf0c3fbeb1af44 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 22:56:04 +0000 Subject: [PATCH 28/46] Give the sources screen something to say MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cards were mostly empty and the page under them entirely so. The counts the card was designed around never appeared, because repomap does not carry any: repomap.plugins and repomap.icons are urls, and the screen refuses to download a file that runs to hundreds of kilobytes just to print a number beside a name. It does not have to. Both catalogues download exactly that file every time they open, so they now leave the count behind in the repository's cache and the sources screen reads it for free — a source nobody has opened has no number, which is honest rather than blank. The installer already keeps a per-repository index of what came from where, so how much of a source is actually installed costs one more file read. That fills the cards. The footer row, which for a source declaring neither a channel nor a repository was one overflow button adrift on an empty line, now carries when the repomap was last fetched — the thing the counts above it were read from. The wording is LocaleController's own: it already says "just now / N minutes ago / today at" in every language the client ships, for location updates, and a cache time is the same kind of fact. Under the list is the grey caption the client puts at the end of every settings section, saying what a source is and how much of what is installed came through one. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 3 + packit/locales/strings_de.json | 3 + packit/locales/strings_en.json | 3 + packit/locales/strings_ru.json | 3 + packit/meta.yml | 2 +- packit/src/ui/IconsListActivity/fragment.py | 7 ++ packit/src/ui/PluginListActivity/fragment.py | 7 ++ packit/src/ui/ReposActivity/card.py | 50 +++++++++++-- packit/src/ui/ReposActivity/fragment.py | 64 ++++++++++++++-- packit/src/utils/repoStats.py | 79 ++++++++++++++++++++ 10 files changed, 208 insertions(+), 13 deletions(-) create mode 100644 packit/src/utils/repoStats.py diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 0a0c8cc..8919012 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1175,6 +1175,7 @@ "repo_card_status_missing": "Не загружаны", "repo_card_plugins": "{0} плагінаў", "repo_card_icons": "{0} набораў", + "repo_card_installed": "Усталявана: {0}", "repo_edit": "Змяніць", "repo_copy_link": "Скапіяваць спасылку", "repos_updating": "Абнаўленне крыніц…", @@ -1203,6 +1204,8 @@ "repo_err_unknown": "Невядомая памылка: {0}", "repos_empty_title": "Пакуль пуста", "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", + "repos_footnote": "Плагіны і наборы значкоў бяруцца з крыніц. Уключаная крыніца з'яўляецца ў каталогу, выключаная — не.", + "repos_footnote_installed": "З іх усталявана {0} — крыніц уключана: {1}.", "retry": "Паўтарыць", "repo_default_already": "Стандартная крыніца ўжо на месцы", "repo_link_shared": "Спасылка на крыніцу адпраўлена" diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 9020497..efb5699 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1175,6 +1175,7 @@ "repo_card_status_missing": "Nicht geladen", "repo_card_plugins": "{0} Plugins", "repo_card_icons": "{0} Icon-Sets", + "repo_card_installed": "{0} installiert", "repo_edit": "Bearbeiten", "repo_copy_link": "Link kopieren", "repos_updating": "Quellen werden aktualisiert…", @@ -1203,6 +1204,8 @@ "repo_err_unknown": "Unbekannter Fehler: {0}", "repos_empty_title": "Noch nichts da", "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", + "repos_footnote": "Plugins und Icon-Pakete stammen aus Quellen. Eine aktivierte Quelle erscheint im Katalog, eine deaktivierte nicht.", + "repos_footnote_installed": "Davon sind {0} installiert, aus {1} aktivierten Quellen.", "retry": "Erneut versuchen", "repo_default_already": "Die Standardquelle ist bereits vorhanden", "repo_link_shared": "Link zur Quelle gesendet" diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 74bcc10..859d2fd 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1175,6 +1175,7 @@ "repo_card_status_missing": "Not loaded", "repo_card_plugins": "{0} plugins", "repo_card_icons": "{0} icon packs", + "repo_card_installed": "{0} installed", "repo_edit": "Edit", "repo_copy_link": "Copy link", "repos_updating": "Refreshing repositories…", @@ -1203,6 +1204,8 @@ "repo_err_unknown": "Unknown error: {0}", "repos_empty_title": "Nothing here yet", "repos_empty_text": "Add a source to install plugins", + "repos_footnote": "Plugins and icon packs come from sources. An enabled source shows up in the catalogue, a disabled one does not.", + "repos_footnote_installed": "{0} of them are installed, across {1} enabled sources.", "retry": "Retry", "repo_default_already": "The default repository is already there", "repo_link_shared": "Repository link sent" diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 26babfe..d9522e5 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1175,6 +1175,7 @@ "repo_card_status_missing": "Не загружен", "repo_card_plugins": "{0} плагинов", "repo_card_icons": "{0} наборов", + "repo_card_installed": "Установлено: {0}", "repo_edit": "Изменить", "repo_copy_link": "Копировать ссылку", "repos_updating": "Обновление источников…", @@ -1203,6 +1204,8 @@ "repo_err_unknown": "Неизвестная ошибка: {0}", "repos_empty_title": "Пока пусто", "repos_empty_text": "Добавьте источник, чтобы ставить плагины", + "repos_footnote": "Плагины и наборы иконок берутся из источников. Включённый источник появляется в каталоге, выключенный — нет.", + "repos_footnote_installed": "Из них установлено {0} — источников включено: {1}.", "retry": "Повторить", "repo_default_already": "Стандартный источник уже на месте", "repo_link_shared": "Ссылка на источник отправлена" diff --git a/packit/meta.yml b/packit/meta.yml index 28b1dd6..0f91acc 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.1.2-dev.17" +version: "0.1.2-dev.18" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 8bf4490..00d2594 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -800,6 +800,13 @@ def load_task(): logx(f"IconList._open_repo_icons: skipping list item (no id or not dict): {item}", True) logx(f"IconList._open_repo_icons: parsed icons count={len(icons)}", True) + # left behind for the sources screen: repomap points at this + # file by url and carries no count of its own + try: + from ...utils import repoStats + repoStats.remember(repo_id, icons=len(icons)) + except Exception as e: + logx(f"IconList._open_repo_icons: stats write failed: {e}", True) # index build is heavy — run it here on the queue thread prebuilt = search_mod.build_index(icons) run_on_ui_thread(lambda: self._update_current_fragment_icons(icons, prebuilt)) diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index d711f98..896103f 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -309,6 +309,13 @@ def load_task(): for item in plugins_raw: if isinstance(item, dict) and item.get("id"): plugins.append(item) + # the sources screen has no other way to know how big a + # source is: repomap only points at this file by url + try: + from ...utils import repoStats + repoStats.remember(repo_id, plugins=len(plugins)) + except Exception as e: + logx(f"InstallUI: repo stats write failed: {e}", True) run_on_ui_thread(lambda: self._update_current_fragment_plugins(plugins)) except Exception as e: BulletinHelper.show_error(str(strings["pl_load_failed"])) diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 500424c..a5a8bac 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -246,8 +246,16 @@ def _fill_chips(is_on, i): make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), "key_avatar_backgroundViolet"), LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - # with the on/off chip gone most sources have nothing to put here; GONE - # takes the row's top margin with it instead of leaving a gap + # how much of this source the user is actually running, off the + # installer's own per-repository index + installed = i.get("installed") + if isinstance(installed, int) and installed > 0: + chips.addView( + make_info_chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) + .replace("{0}", str(installed)), "key_avatar_backgroundGreen"), + LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + # a source nobody has opened yet has nothing to put here; GONE takes the + # row's top margin with it instead of leaving a gap chips.setVisibility(0 if chips.getChildCount() > 0 else 8) # filled by the first _apply_enabled below, together with the rest of the @@ -266,6 +274,24 @@ def _btn_lp(right_margin_dp=6): lp.rightMargin = AndroidUtilities.dp(right_margin_dp) return lp + # When a source declares neither a channel nor a repository the footer used + # to be one lone overflow button adrift on an empty row. The age of the + # cached repomap belongs on a screen about sources anyway — it is what the + # counts above it were read from. + updated_tv = TextView(ctx) + updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) + updated_tv.setSingleLine(True) + updated_tv.setEllipsize(TextUtils.TruncateAt.END) + updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + + def _fill_updated(i): + text = _updated_label(i.get("updated_at")) + updated_tv.setText(text) + updated_tv.setVisibility(0 if text else 8) + + _fill_updated(info) + footer.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) + # own container so a repaint can refill it without touching the overflow links = LinearLayout(ctx) links.setOrientation(LinearLayout.HORIZONTAL) @@ -287,9 +313,6 @@ def _fill_links(i): _fill_links(info) footer.addView(links, LayoutHelper.createLinear(-2, -2)) - spacer = View(ctx) - footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) - on_menu = callbacks.get("on_menu") menu_btn = _round_icon_button( ctx, "ic_ab_other", _theme("key_windowBackgroundWhiteGrayText"), @@ -345,6 +368,7 @@ def _update(new_repo, new_info): name_tv.setText(str(new_repo.get("name") or strings.unnamed)) _fill_sub(new_repo, state["info"]) _fill_links(state["info"]) + _fill_updated(state["info"]) new_url = str(state["info"].get("icon_url") or "") if new_url != state["icon_url"]: # only an updated repomap can do this, and then it really is a @@ -425,6 +449,22 @@ def onTouch(self, v, event): return None +def _updated_label(mtime) -> str: + # LocaleController already words "just now / N minutes ago / today at …" for + # the client's location updates, in every language it ships, and a repomap + # cache is the same kind of fact — so the wording comes from there rather + # than from four more locale keys of my own. + try: + seconds = int(float(mtime or 0)) + if seconds <= 0: + return "" + from org.telegram.messenger import LocaleController + return str(LocaleController.formatLocationUpdateDate(seconds)) + except Exception as e: + logx(f"repos card: updated label unavailable: {e}", True) + return "" + + def _host_of(url) -> str: try: text = str(url or "") diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 9118c70..b71ead3 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -64,12 +64,27 @@ def _theme(key: str, fallback: int = 0): def read_repo_info(repo: dict) -> dict: - """Everything the card needs, straight out of the cached repomap.""" + """Everything the card needs, straight off disk. No network.""" info = {"maintainer": "", "telegram": "", "source": "", "icon_url": "", - "plugins": None, "icons": None, "status": "missing"} + "plugins": None, "icons": None, "installed": 0, "updated_at": 0.0, + "status": "missing"} repo_id = str(repo.get("id") or "") if not repo_id: return info + + # what the user has taken from this source, and what the catalogues counted + # in it the last time they were opened + try: + from ...utils import repoStats + info["installed"] = repoStats.installed_count(repo_id) + counted = repoStats.read(repo_id) + if isinstance(counted.get("plugins"), int): + info["plugins"] = counted["plugins"] + if isinstance(counted.get("icons"), int): + info["icons"] = counted["icons"] + except Exception as e: + logx(f"repos: stats unavailable for '{repo_id}': {e}", True) + path = getRepoCachePath(repo_id) try: if not os.path.isfile(path): @@ -87,13 +102,14 @@ def read_repo_info(repo: dict) -> dict: icon_url = str(meta.get("rm_icon") or "").strip() # older repositories put an R.drawable name in rm_icon; only a link is an icon info["icon_url"] = icon_url if icon_url.lower().startswith(("http://", "https://")) else "" - # the chip reports whether the source is in use, not how old its cache is: - # every start refreshes the caches anyway, so an age reading only ever told - # the reader that they had been offline for a day info["status"] = "loaded" + try: + info["updated_at"] = os.path.getmtime(path) + except Exception: + info["updated_at"] = 0.0 - # a repomap that is itself the plugin list carries the count; the usual - # shape only points at it by url, and the screen does not go online to count + # a repomap that is itself the plugin list carries the count inline; the + # usual shape only points at it by url, and that count comes from repoStats plugins = cached.get("plugins") if isinstance(plugins, list): info["plugins"] = len(plugins) @@ -115,6 +131,7 @@ def __init__(self, repoManager): self._first_build = True self._handles = [] self._signature_shown = None + self._footnote = None # ---------------------------------------------------------------- delegate def onFragmentCreate(self, *_): @@ -176,6 +193,16 @@ def beforeCreateView(self): self._signature_shown = None content.addView(self._list, LayoutHelper.createLinear(-1, -2)) + # The client ends every settings section with a grey caption saying + # what the section is for, and this screen — a short list on a tall + # page — has the room for one. It also carries the one number no + # single card can: how much of what is installed came from here. + self._footnote = TextView(act) + self._footnote.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + self._footnote.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + self._footnote.setLineSpacing(float(AndroidUtilities.dp(2)), 1.0) + content.addView(self._footnote, LayoutHelper.createLinear(-1, -2, 9, 14, 9, 0)) + scroll.addView(content, ScrollView.LayoutParams(-1, -2)) root.addView(scroll, FrameLayout.LayoutParams(-1, -1)) root.addView(self._build_add_button(act), LayoutHelper.createFrame( @@ -255,6 +282,7 @@ def _signature(self, repos): def _render(self, act, repos, infos): self._summary.setText(self._summary_text(len(repos))) + self._set_footnote(repos, infos) # A repaint is not a rebuild. Flipping a switch writes the list back # through RepositoryManager, which notifies this screen, which used to @@ -295,6 +323,28 @@ def _render(self, act, repos, infos): self._first_build = False applyFontToTree(self._root) + def _set_footnote(self, repos, infos): + if self._footnote is None: + return + try: + if not repos: + # the empty state already explains the screen; two captions + # saying the same thing on an otherwise blank page is worse + self._footnote.setVisibility(8) + return + parts = [str(getattr(strings, "repos_footnote", ""))] + installed = sum(int(i.get("installed") or 0) for i in infos) + if installed > 0: + enabled = sum(1 for r in repos if r.get("enabled", True)) + parts.append( + str(getattr(strings, "repos_footnote_installed", "")) + .replace("{0}", str(installed)).replace("{1}", str(enabled))) + text = "\n\n".join(p for p in parts if p) + self._footnote.setText(text) + self._footnote.setVisibility(0 if text else 8) + except Exception as e: + logx(f"repos fragment: footnote error: {e}", True) + def _summary_text(self, count: int) -> str: try: from ..PluginListActivity.helpers.utils import _format_plural diff --git a/packit/src/utils/repoStats.py b/packit/src/utils/repoStats.py new file mode 100644 index 0000000..c4c7150 --- /dev/null +++ b/packit/src/utils/repoStats.py @@ -0,0 +1,79 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# What a repository turned out to hold, remembered from the last time something +# actually looked. +# +# repomap does not carry counts — repomap.plugins and repomap.icons are urls, +# not lists — so the sources screen has no way to say how big a source is +# without downloading a file that can run to hundreds of kilobytes. The +# catalogues download it anyway, every time they open, so they leave the number +# behind here and the sources screen reads it for free. A source nobody has +# opened yet simply has no number, which is honest: nothing has counted it. + +from packutil import logx +import json +import os + +_FILE = "{}-stats.json" + + +def _path(rm_rid: str) -> str: + from .paths import getReposCacheDir + return os.path.join(getReposCacheDir(), _FILE.format(rm_rid)) + + +def read(rm_rid: str) -> dict: + rm_rid = str(rm_rid or "") + if not rm_rid: + return {} + try: + path = _path(rm_rid) + if not os.path.isfile(path): + return {} + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + return data if isinstance(data, dict) else {} + except Exception as e: + logx(f"repoStats: read error for '{rm_rid}': {e}", True) + return {} + + +def remember(rm_rid: str, **counts): + """remember(rid, plugins=1011) — only the keys passed are touched.""" + rm_rid = str(rm_rid or "") + if not rm_rid: + return + clean = {k: int(v) for k, v in counts.items() if isinstance(v, int) and v >= 0} + if not clean: + return + try: + data = read(rm_rid) + if all(data.get(k) == v for k, v in clean.items()): + return # same numbers as last time, no write + data.update(clean) + path = _path(rm_rid) + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", encoding="utf-8") as f: + json.dump(data, f, ensure_ascii=False) + except Exception as e: + logx(f"repoStats: write error for '{rm_rid}': {e}", True) + + +def installed_count(rm_rid: str) -> int: + # the per-repository install index the installer keeps + rm_rid = str(rm_rid or "") + if not rm_rid: + return 0 + try: + from .paths import getRepoIndexPath + path = getRepoIndexPath(rm_rid) + if not os.path.isfile(path): + return 0 + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + plugins = data.get("installed_plugins") if isinstance(data, dict) else None + return len(plugins) if isinstance(plugins, list) else 0 + except Exception as e: + logx(f"repoStats: index read error for '{rm_rid}': {e}", True) + return 0 From d4df568b39512ed677fe682789777015375da0c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 12 Aug 2026 23:11:00 +0000 Subject: [PATCH 29/46] One accent, no translucent fills, and the client's switch box back MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switch draws itself to the size of the view it is given, so last build's 56x48 box came out half again as large as the pill the client draws everywhere else. It gets 37x40 back — the box PluginCell uses — and the bigger touch target moves to a wrapper around it, which also drives the press ripple now. The per-repository accent is gone. Picking a colour out of the avatar palette by hashing the id gave every source a stable look, which is worth nothing on a theme built from a single accent: a violet or an orange dropped into a Monet palette is simply the wrong colour on the screen. Monograms, link buttons and chips all take the theme accent, and the one chip that does not is the failure case, which has a colour of its own. Those fills are opaque now. Accent at an eighth alpha takes its colour from whatever is behind it, which on a card that scales under a press is not one thing, and two of them overlapping stack. They are mixed against the card surface instead, for the same look out of one solid value. The overflow button keeps its translucent grey — it is meant to sit back. The maintainer line is coloured the way the catalogue colours its author line, windowBackgroundWhiteBlueText with a movement method; left to itself the formatter had been painting mentions a teal that appears nowhere else. The caption under the list is gone. Empty space reads better than it did. Co-Authored-By: Claude Opus 5 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/src/ui/ReposActivity/card.py | 158 ++++++++++++++--------- packit/src/ui/ReposActivity/fragment.py | 34 ----- packit/src/ui/ReposActivity/repoIcon.py | 76 ++++++----- packit/src/ui/ReposActivity/repoSheet.py | 2 +- 9 files changed, 138 insertions(+), 142 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 8919012..758d6dd 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1204,8 +1204,6 @@ "repo_err_unknown": "Невядомая памылка: {0}", "repos_empty_title": "Пакуль пуста", "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", - "repos_footnote": "Плагіны і наборы значкоў бяруцца з крыніц. Уключаная крыніца з'яўляецца ў каталогу, выключаная — не.", - "repos_footnote_installed": "З іх усталявана {0} — крыніц уключана: {1}.", "retry": "Паўтарыць", "repo_default_already": "Стандартная крыніца ўжо на месцы", "repo_link_shared": "Спасылка на крыніцу адпраўлена" diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index efb5699..3615715 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1204,8 +1204,6 @@ "repo_err_unknown": "Unbekannter Fehler: {0}", "repos_empty_title": "Noch nichts da", "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", - "repos_footnote": "Plugins und Icon-Pakete stammen aus Quellen. Eine aktivierte Quelle erscheint im Katalog, eine deaktivierte nicht.", - "repos_footnote_installed": "Davon sind {0} installiert, aus {1} aktivierten Quellen.", "retry": "Erneut versuchen", "repo_default_already": "Die Standardquelle ist bereits vorhanden", "repo_link_shared": "Link zur Quelle gesendet" diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 859d2fd..281c147 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1204,8 +1204,6 @@ "repo_err_unknown": "Unknown error: {0}", "repos_empty_title": "Nothing here yet", "repos_empty_text": "Add a source to install plugins", - "repos_footnote": "Plugins and icon packs come from sources. An enabled source shows up in the catalogue, a disabled one does not.", - "repos_footnote_installed": "{0} of them are installed, across {1} enabled sources.", "retry": "Retry", "repo_default_already": "The default repository is already there", "repo_link_shared": "Repository link sent" diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index d9522e5..0242a18 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1204,8 +1204,6 @@ "repo_err_unknown": "Неизвестная ошибка: {0}", "repos_empty_title": "Пока пусто", "repos_empty_text": "Добавьте источник, чтобы ставить плагины", - "repos_footnote": "Плагины и наборы иконок берутся из источников. Включённый источник появляется в каталоге, выключенный — нет.", - "repos_footnote_installed": "Из них установлено {0} — источников включено: {1}.", "retry": "Повторить", "repo_default_already": "Стандартный источник уже на месте", "repo_link_shared": "Ссылка на источник отправлена" diff --git a/packit/meta.yml b/packit/meta.yml index 0f91acc..7a199c8 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.1.2-dev.18" +version: "0.1.2-dev.19" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index a5a8bac..08711f2 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -17,6 +17,7 @@ from android.util import TypedValue from android.graphics.drawable import GradientDrawable from android_utils import OnClickListener +from java import dynamic_proxy try: from org.telegram.messenger import AndroidUtilities @@ -35,10 +36,30 @@ from . import repoIcon from ..PluginListActivity.helpers.uiHelpers import ( - make_info_chip, apply_press_scale_on_target, resolve_icon, + apply_press_scale_on_target, resolve_icon, ) +def _chip(ctx, text: str, tint: int): + # uiHelpers.make_info_chip fills at a third alpha and paints the label in a + # palette colour. Both are wrong here: the fill has to be solid, and the + # colour has to be the theme's, not a green borrowed from the avatar + # palette that no other pixel on the screen is using. + surface = _theme("key_windowBackgroundWhite") + bg = GradientDrawable() + bg.setShape(GradientDrawable.RECTANGLE) + bg.setCornerRadius(float(AndroidUtilities.dp(8))) + bg.setColor(repoIcon.tonal(tint, surface, 0.16)) + tv = TextView(ctx) + tv.setText(text) + tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11) + tv.setTextColor(_alpha(tint, 0xFF)) + tv.setBackground(bg) + tv.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(3), + AndroidUtilities.dp(8), AndroidUtilities.dp(3)) + return tv + + def _c(color: int) -> int: return ctypes.c_int32(color).value @@ -55,17 +76,27 @@ def _theme(key: str, fallback: int = 0): def _round_icon_button(ctx, icon_name: str, tint: int, on_click, - size_dp: int = 32, icon_dp: int = 16): + size_dp: int = 32, icon_dp: int = 16, translucent: bool = False): + # Opaque by default: a fill of accent-at-8-percent takes its colour from + # whatever happens to be behind the button, which on an animating card is + # not one thing. The overflow is the exception — it is a neutral grey on the + # card and is meant to sit back. btn = FrameLayout(ctx) btn.setClickable(True) btn.setFocusable(True) + if translucent: + fill, pressed = _alpha(tint, 0x14), _alpha(tint, 0x28) + else: + surface = _theme("key_windowBackgroundWhite") + fill = repoIcon.tonal(tint, surface, 0.16) + pressed = repoIcon.tonal(tint, surface, 0.30) bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) bg.setCornerRadius(float(AndroidUtilities.dp(size_dp) / 2)) - bg.setColor(_alpha(tint, 0x14)) + bg.setColor(fill) try: btn.setBackground(Theme.createSimpleSelectorRoundRectDrawable( - AndroidUtilities.dp(size_dp) // 2, _alpha(tint, 0x14), _alpha(tint, 0x28) + AndroidUtilities.dp(size_dp) // 2, fill, pressed )) except Exception: btn.setBackground(bg) @@ -176,16 +207,17 @@ def _icon_lp(): def _fill_sub(r, i): text = str(i.get("maintainer") or "").strip() or _host_of(r.get("url")) - # rm_maintainer is free text from the repomap and reads like a message — - # "@name", "ROBOT (список от @name)" — so it goes through the client's - # own formatter, which resolves mentions, emoji and markdown. - # - # No LinkMovementMethod here on purpose: it makes a TextView clickable, - # and this one sits inside a card whose tap opens the source's sheet. A - # tappable mention belongs in that sheet, where nothing competes for it. + # Set up exactly the way the plugin catalogue sets up its author line + # (PluginListActivity/card.py): fullyFormatText, grey body, + # windowBackgroundWhiteBlueText for the mention, LinkMovementMethod. + # Left to itself the formatter paints mentions in its own colour, which + # is why these came out a teal that appears nowhere else on the screen. try: from com.exteragram.messenger.utils.text import LocaleUtils + from android.text.method import LinkMovementMethod sub_tv.setText(LocaleUtils.fullyFormatText(text)) + sub_tv.setLinkTextColor(_theme("key_windowBackgroundWhiteBlueText")) + sub_tv.setMovementMethod(LinkMovementMethod.getInstance()) except Exception as e: logx(f"repos card: maintainer format unavailable: {e}", True) sub_tv.setText(text) @@ -195,24 +227,52 @@ def _fill_sub(r, i): header.addView(col, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - # _toggle is defined further down; the lambda resolves it when tapped - switch = _build_switch(ctx, enabled, lambda: _toggle()) + switch = _build_switch(ctx, enabled) if switch is not None: + # Switch draws itself to the size of its view, so the box is not a + # matter of taste: at 56x48 the pill came out half again as big as the + # one the client draws everywhere else. It gets the client's own 37x40 + # back, and the larger touch target moves to a wrapper around it — + # 37x40 is under the 48dp a control should answer to, and this switch + # takes its own taps now. + # # Explicit params, not LayoutHelper.createLinear(w, h, gravity, …): that # call has a (w, h, float weight, …) twin, and picking it gives the # switch a weight instead of a gravity. In a row that already has a # weighted column the switch then absorbs the overflow and is measured - # narrower than the track Switch.onDraw centres in it, so the track is - # clipped by the view bounds — which is what turned the pill into a - # rectangle with square corners. - # - # 56x48 rather than the client's 37x40: the switch is the touch target - # now that the card no longer toggles, and the box has to cover the - # whole pill the new switch style draws, not just the middle of it. + # narrower than its own track, which shears the ends off the pill. + sw_wrap = FrameLayout(ctx) + sw_wrap.setClipChildren(False) + sw_wrap.setClickable(True) + sw_wrap.setFocusable(True) + sw_wrap.setOnClickListener(OnClickListener(lambda v: _toggle())) + # Switch has no touch handling of its own — the cells that host it drive + # its ripple from their own setPressed, so the wrapper does the same. + # The listener returns False, leaving the click to the normal path. + try: + from android.view import View as _View + + class _Press(dynamic_proxy(_View.OnTouchListener)): + def onTouch(self, v, event): + action = event.getActionMasked() + if action == 0: # DOWN + switch.setDrawRipple(True) + elif action in (1, 3): # UP, CANCEL + switch.setDrawRipple(False) + return False + + sw_wrap.setOnTouchListener(_Press()) + except Exception as e: + logx(f"repos card: switch ripple unavailable: {e}", True) + + sw_inner = FrameLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(40)) + sw_inner.gravity = Gravity.CENTER + sw_wrap.addView(switch, sw_inner) + sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(56), AndroidUtilities.dp(48)) sw_lp.gravity = Gravity.CENTER_VERTICAL sw_lp.leftMargin = AndroidUtilities.dp(6) - header.addView(switch, sw_lp) + header.addView(sw_wrap, sw_lp) card.addView(header, LayoutHelper.createLinear(-1, -2)) @@ -228,31 +288,31 @@ def _fill_chips(is_on, i): # A source whose repomap never downloaded still gets a chip, because # nothing else on the card says that — it is switched on and gives # nothing, which the switch cannot show. + chip_lp = LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0) if is_on and str(i.get("status") or "") == "missing": + # the one chip that is not the accent: this is a fault, and the + # theme has a colour for those chips.addView( - make_info_chip(ctx, str(getattr(strings, "repo_card_status_missing", "Not loaded")), - "key_text_RedBold"), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + _chip(ctx, str(getattr(strings, "repo_card_status_missing", "Not loaded")), + _theme("key_text_RedBold")), chip_lp) plugins = i.get("plugins") if isinstance(plugins, int): chips.addView( - make_info_chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), - "key_windowBackgroundWhiteBlueText"), + _chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), accent), LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) icons_n = i.get("icons") if isinstance(icons_n, int): chips.addView( - make_info_chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), - "key_avatar_backgroundViolet"), + _chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), accent), LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) # how much of this source the user is actually running, off the # installer's own per-repository index installed = i.get("installed") if isinstance(installed, int) and installed > 0: chips.addView( - make_info_chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) - .replace("{0}", str(installed)), "key_avatar_backgroundGreen"), + _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) + .replace("{0}", str(installed)), accent), LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) # a source nobody has opened yet has nothing to put here; GONE takes the # row's top margin with it instead of leaving a gap @@ -318,7 +378,8 @@ def _fill_links(i): ctx, "ic_ab_other", _theme("key_windowBackgroundWhiteGrayText"), # state["repo"] and not the dict this card was built from: a repaint # hands over a freshly parsed one, and the menu prefills its dialogs - lambda: on_menu(menu_holder[0], state["repo"]) if on_menu else None + lambda: on_menu(menu_holder[0], state["repo"]) if on_menu else None, + translucent=True ) menu_holder = [menu_btn] footer.addView(menu_btn, _btn_lp(0)) @@ -404,10 +465,11 @@ def _update(new_repo, new_info): return card -def _build_switch(ctx, checked: bool, on_toggle): +def _build_switch(ctx, checked: bool): # Coloured the way the client colours the switch in its own plugin card - # (PluginCell). Unlike that one it takes its own taps: the card opens a - # sheet now, so turning a source on and off is the switch's job alone. + # (PluginCell), and given that cell's box. The card opens a sheet now, so + # the tap that turns a source on and off belongs to the switch — but it + # arrives through the wrapper, which is big enough to aim at. try: from org.telegram.ui.Components import Switch as TgSwitch sw = TgSwitch(ctx) @@ -419,30 +481,10 @@ def _build_switch(ctx, checked: bool, on_toggle): except Exception as e: logx(f"repos card: switch colors unavailable: {e}", True) sw.setChecked(checked, False) - sw.setClickable(True) - sw.setFocusable(True) - sw.setOnClickListener(OnClickListener(lambda v: on_toggle())) - - # Switch has no touch handling of its own — the cells that host it drive - # its ripple from their own setPressed. Nothing overrides setPressed - # here, so the press is forwarded by hand, and the listener returns - # False so the click still goes through the normal path. - try: - from java import dynamic_proxy - from android.view import View as _View - - class _Press(dynamic_proxy(_View.OnTouchListener)): - def onTouch(self, v, event): - action = event.getActionMasked() - if action == 0: # DOWN - sw.setDrawRipple(True) - elif action in (1, 3): # UP, CANCEL - sw.setDrawRipple(False) - return False - - sw.setOnTouchListener(_Press()) - except Exception as e: - logx(f"repos card: switch ripple unavailable: {e}", True) + # the wrapper around it takes the taps; the switch keeps the client's + # box so it keeps the client's proportions + sw.setClickable(False) + sw.setFocusable(False) return sw except Exception as e: logx(f"repos card: switch unavailable: {e}", False) diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index b71ead3..f73db04 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -131,7 +131,6 @@ def __init__(self, repoManager): self._first_build = True self._handles = [] self._signature_shown = None - self._footnote = None # ---------------------------------------------------------------- delegate def onFragmentCreate(self, *_): @@ -193,16 +192,6 @@ def beforeCreateView(self): self._signature_shown = None content.addView(self._list, LayoutHelper.createLinear(-1, -2)) - # The client ends every settings section with a grey caption saying - # what the section is for, and this screen — a short list on a tall - # page — has the room for one. It also carries the one number no - # single card can: how much of what is installed came from here. - self._footnote = TextView(act) - self._footnote.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) - self._footnote.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - self._footnote.setLineSpacing(float(AndroidUtilities.dp(2)), 1.0) - content.addView(self._footnote, LayoutHelper.createLinear(-1, -2, 9, 14, 9, 0)) - scroll.addView(content, ScrollView.LayoutParams(-1, -2)) root.addView(scroll, FrameLayout.LayoutParams(-1, -1)) root.addView(self._build_add_button(act), LayoutHelper.createFrame( @@ -282,7 +271,6 @@ def _signature(self, repos): def _render(self, act, repos, infos): self._summary.setText(self._summary_text(len(repos))) - self._set_footnote(repos, infos) # A repaint is not a rebuild. Flipping a switch writes the list back # through RepositoryManager, which notifies this screen, which used to @@ -323,28 +311,6 @@ def _render(self, act, repos, infos): self._first_build = False applyFontToTree(self._root) - def _set_footnote(self, repos, infos): - if self._footnote is None: - return - try: - if not repos: - # the empty state already explains the screen; two captions - # saying the same thing on an otherwise blank page is worse - self._footnote.setVisibility(8) - return - parts = [str(getattr(strings, "repos_footnote", ""))] - installed = sum(int(i.get("installed") or 0) for i in infos) - if installed > 0: - enabled = sum(1 for r in repos if r.get("enabled", True)) - parts.append( - str(getattr(strings, "repos_footnote_installed", "")) - .replace("{0}", str(installed)).replace("{1}", str(enabled))) - text = "\n\n".join(p for p in parts if p) - self._footnote.setText(text) - self._footnote.setVisibility(0 if text else 8) - except Exception as e: - logx(f"repos fragment: footnote error: {e}", True) - def _summary_text(self, count: int) -> str: try: from ..PluginListActivity.helpers.utils import _format_plural diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index 25a94a9..5323354 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -38,17 +38,6 @@ _mem = OrderedDict() _mem_lock = None -_PALETTE = ( - "key_avatar_backgroundBlue", - "key_avatar_backgroundViolet", - "key_avatar_backgroundGreen", - "key_avatar_backgroundOrange", - "key_avatar_backgroundPink", - "key_avatar_backgroundCyan", - "key_avatar_backgroundRed", -) - - def _c(color: int) -> int: # java setColor(int) rejects python ints >= 0x80000000 return ctypes.c_int32(color).value @@ -58,6 +47,24 @@ def _alpha(color: int, a: int) -> int: return _c((a << 24) | (color & 0xFFFFFF)) +def tonal(accent: int, surface: int, fraction: float) -> int: + """An opaque container colour: accent mixed into the surface behind it. + + Not accent-at-low-alpha. A translucent fill picks up whatever is under it — + the card, then the window, then anything the card is animating over — so + two identical chips on different backgrounds come out different colours, + and overlapping ones stack. Mixing the two colours here gives the same look + as one solid value that owes nothing to what is behind it. + """ + fraction = max(0.0, min(1.0, float(fraction))) + out = 0xFF000000 + for shift in (16, 8, 0): + a = (accent >> shift) & 0xFF + b = (surface >> shift) & 0xFF + out |= int(round(b + (a - b) * fraction)) << shift + return _c(out) + + def _lock(): global _mem_lock if _mem_lock is None: @@ -74,37 +81,21 @@ def _seed(repo: dict) -> int: return total -_harmonized = {} - - -def _harmonize(color: int) -> int: - # The client ships MonetUtils for exactly this: on Android 12+ it pulls a - # colour towards the system palette (MaterialColors.harmonize against - # system_accent1_600), which is what keeps a fixed palette from clashing - # with a Monet theme. Below 12, and on themes without it, it hands the - # colour back unchanged. - if color in _harmonized: - return _harmonized[color] - result = color - try: - from com.exteragram.messenger.utils.ui import MonetUtils - result = _c(int(MonetUtils.harmonize(color))) - except Exception: - result = color - _harmonized[color] = result - return result - - def accent_for(repo: dict) -> int: - # deterministic colour so a repository keeps its look between launches - try: - name = _PALETTE[_seed(repo) % len(_PALETTE)] - return _harmonize(Theme.getColor(getattr(Theme, name))) - except Exception: + """The theme's accent. The repository does not get a say any more. + + This used to pick a colour per repository out of the avatar palette, so + that a source kept its own look. On a theme built from one accent — which + is every Monet theme, and the client's own — a violet or an orange dropped + into it is simply the wrong colour on the screen, however stable it is. + The argument stays so the call sites read the same. + """ + for key in ("key_featuredStickers_addButton", "key_windowBackgroundWhiteBlueText"): try: - return Theme.getColor(Theme.key_featuredStickers_addButton) + return _c(int(Theme.getColor(getattr(Theme, key)))) except Exception: - return _c(0xFF2AABEE) + continue + return _c(0xFF2AABEE) def _letter(repo: dict) -> str: @@ -253,6 +244,11 @@ def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url holder = FrameLayout(ctx) + try: + surface = _c(int(Theme.getColor(Theme.key_windowBackgroundWhite))) + except Exception: + surface = _c(0xFF1C1C1E) + mono = TextView(ctx) mono.setText(_letter(repo)) mono.setGravity(Gravity.CENTER) @@ -268,7 +264,7 @@ def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) bg.setCornerRadius(float(AndroidUtilities.dp(radius_dp))) - bg.setColor(_alpha(accent, 0x1C)) + bg.setColor(tonal(accent, surface, 0.16)) mono.setBackground(bg) holder.addView(mono, FrameLayout.LayoutParams(size_px, size_px)) diff --git a/packit/src/ui/ReposActivity/repoSheet.py b/packit/src/ui/ReposActivity/repoSheet.py index 11ed320..5fd37d0 100644 --- a/packit/src/ui/ReposActivity/repoSheet.py +++ b/packit/src/ui/ReposActivity/repoSheet.py @@ -112,7 +112,7 @@ def _show(): from com.exteragram.messenger.utils.text import LocaleUtils from android.text.method import LinkMovementMethod sub.setText(LocaleUtils.fullyFormatText(sub_text)) - sub.setLinkTextColor(_theme("key_dialogTextBlue")) + sub.setLinkTextColor(_theme("key_windowBackgroundWhiteBlueText")) sub.setMovementMethod(LinkMovementMethod.getInstance()) except Exception as e: logx(f"repoSheet: maintainer format unavailable: {e}", True) From 7fe75b1f0a37ef6de2a014ab1af2c1890e6deaec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 08:48:58 +0000 Subject: [PATCH 30/46] Give the card two rows that start and end in the same places MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The installed chip moves to the right of its row and the fetch time comes up beside it, which leaves the card two rows under the header that are anchored alike: what it knows on the left, what it offers on the right. Before, the numbers sat left and the buttons right on a row of their own, so the eye started somewhere different on each line, and a source with no channel and no repository ended on a row holding one overflow button and nothing else. Sizes follow from that. Every pill on the card is 32dp now — md3's assist chip height, and already the size of the round buttons — so a chip beside a button lines up instead of nearly lining up, and both rows are the same height. Chip labels go to 12sp with 12dp of side padding and a full radius, matching the buttons they sit with. The switch is right-aligned inside its touch target rather than centred. The target is 19dp wider than the switch purely to be easier to hit, and centring spent half that pushing the pill in from the card's content edge, out of line with the overflow button directly below it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 148 +++++++++++++++++----------- 2 files changed, 91 insertions(+), 59 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 7a199c8..f60c97d 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.1.2-dev.19" +version: "0.1.2-dev.20" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 08711f2..aaa4a67 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -40,26 +40,44 @@ ) +_ROW_H = 32 # every pill on the card is this tall: chips and round buttons alike + + def _chip(ctx, text: str, tint: int): # uiHelpers.make_info_chip fills at a third alpha and paints the label in a # palette colour. Both are wrong here: the fill has to be solid, and the # colour has to be the theme's, not a green borrowed from the avatar # palette that no other pixel on the screen is using. + # + # The geometry is md3's assist chip — 32dp tall, fully rounded — which is + # also the size of the round buttons it shares a card with, so a chip and a + # button standing next to each other line up instead of nearly lining up. surface = _theme("key_windowBackgroundWhite") bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) - bg.setCornerRadius(float(AndroidUtilities.dp(8))) + bg.setCornerRadius(float(AndroidUtilities.dp(_ROW_H) / 2)) bg.setColor(repoIcon.tonal(tint, surface, 0.16)) tv = TextView(ctx) tv.setText(text) - tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 11) + tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) tv.setTextColor(_alpha(tint, 0xFF)) + tv.setSingleLine(True) + tv.setGravity(Gravity.CENTER) tv.setBackground(bg) - tv.setPadding(AndroidUtilities.dp(8), AndroidUtilities.dp(3), - AndroidUtilities.dp(8), AndroidUtilities.dp(3)) + tv.setPadding(AndroidUtilities.dp(12), 0, AndroidUtilities.dp(12), 0) + try: + tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass return tv +def _chip_lp(right_margin_dp=6): + lp = LinearLayout.LayoutParams(-2, AndroidUtilities.dp(_ROW_H)) + lp.rightMargin = AndroidUtilities.dp(right_margin_dp) + return lp + + def _c(color: int) -> int: return ctypes.c_int32(color).value @@ -265,8 +283,13 @@ def onTouch(self, v, event): except Exception as e: logx(f"repos card: switch ripple unavailable: {e}", True) + # right-aligned inside the wrapper, not centred: the wrapper is 19dp + # wider than the switch purely to be easier to hit, and centring it + # pushed the pill 9dp in from the card's content edge — out of line with + # the overflow button directly below it. The slack goes leftward, which + # is the side a thumb arrives from anyway. sw_inner = FrameLayout.LayoutParams(AndroidUtilities.dp(37), AndroidUtilities.dp(40)) - sw_inner.gravity = Gravity.CENTER + sw_inner.gravity = Gravity.RIGHT | Gravity.CENTER_VERTICAL sw_wrap.addView(switch, sw_inner) sw_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(56), AndroidUtilities.dp(48)) @@ -276,7 +299,59 @@ def onTouch(self, v, event): card.addView(header, LayoutHelper.createLinear(-1, -2)) - # ---- chips: status and what the repository carries + # Two rows under the header, both 32dp and both anchored the same way: what + # the card knows on the left, what it offers on the right. Before this the + # numbers sat left and the buttons right on a row of their own, so the eye + # had to start in a different place on each line and the last row was often + # a single overflow button by itself. + on_open = callbacks.get("on_open") or (lambda _u: None) + + def _btn_lp(right_margin_dp=6): + lp = LinearLayout.LayoutParams(AndroidUtilities.dp(_ROW_H), AndroidUtilities.dp(_ROW_H)) + lp.rightMargin = AndroidUtilities.dp(right_margin_dp) + return lp + + # ---- meta row: when it was fetched | how much of it is installed + meta = LinearLayout(ctx) + meta.setOrientation(LinearLayout.HORIZONTAL) + meta.setGravity(Gravity.CENTER_VERTICAL) + + updated_tv = TextView(ctx) + updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) + updated_tv.setSingleLine(True) + updated_tv.setEllipsize(TextUtils.TruncateAt.END) + updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + # weighted, so a long "installed" chip eats into the label rather than + # pushing itself off the card + meta.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) + + installed_box = LinearLayout(ctx) + installed_box.setOrientation(LinearLayout.HORIZONTAL) + installed_box.setGravity(Gravity.CENTER_VERTICAL) + meta.addView(installed_box, LayoutHelper.createLinear(-2, -2)) + + def _fill_meta(i): + text = _updated_label(i.get("updated_at")) + updated_tv.setText(text) + updated_tv.setVisibility(0 if text else 8) + + installed_box.removeAllViews() + installed = i.get("installed") + if isinstance(installed, int) and installed > 0: + installed_box.addView( + _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) + .replace("{0}", str(installed)), accent), _chip_lp(0)) + meta.setVisibility(0 if (text or installed_box.getChildCount()) else 8) + + _fill_meta(info) + card.addView(meta, LayoutHelper.createLinear(-1, _ROW_H, 0, 12, 0, 0)) + + # ---- action row: what it carries on the left, what you can do on the right + footer = LinearLayout(ctx) + footer.setOrientation(LinearLayout.HORIZONTAL) + footer.setGravity(Gravity.CENTER_VERTICAL) + + # own containers so a repaint can refill them without touching the overflow chips = LinearLayout(ctx) chips.setOrientation(LinearLayout.HORIZONTAL) chips.setGravity(Gravity.CENTER_VERTICAL) @@ -288,71 +363,28 @@ def _fill_chips(is_on, i): # A source whose repomap never downloaded still gets a chip, because # nothing else on the card says that — it is switched on and gives # nothing, which the switch cannot show. - chip_lp = LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0) if is_on and str(i.get("status") or "") == "missing": # the one chip that is not the accent: this is a fault, and the # theme has a colour for those chips.addView( _chip(ctx, str(getattr(strings, "repo_card_status_missing", "Not loaded")), - _theme("key_text_RedBold")), chip_lp) - + _theme("key_text_RedBold")), _chip_lp()) plugins = i.get("plugins") if isinstance(plugins, int): chips.addView( _chip(ctx, str(strings.repo_card_plugins).replace("{0}", str(plugins)), accent), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) + _chip_lp()) icons_n = i.get("icons") if isinstance(icons_n, int): chips.addView( _chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), accent), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - # how much of this source the user is actually running, off the - # installer's own per-repository index - installed = i.get("installed") - if isinstance(installed, int) and installed > 0: - chips.addView( - _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) - .replace("{0}", str(installed)), accent), - LayoutHelper.createLinear(-2, -2, 0, 0, 6, 0)) - # a source nobody has opened yet has nothing to put here; GONE takes the - # row's top margin with it instead of leaving a gap - chips.setVisibility(0 if chips.getChildCount() > 0 else 8) - - # filled by the first _apply_enabled below, together with the rest of the - # state that depends on the switch - card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) + _chip_lp()) - # ---- footer: telegram / source, overflow on the right - footer = LinearLayout(ctx) - footer.setOrientation(LinearLayout.HORIZONTAL) - footer.setGravity(Gravity.CENTER_VERTICAL) - - on_open = callbacks.get("on_open") or (lambda _u: None) - - def _btn_lp(right_margin_dp=6): - lp = LinearLayout.LayoutParams(AndroidUtilities.dp(32), AndroidUtilities.dp(32)) - lp.rightMargin = AndroidUtilities.dp(right_margin_dp) - return lp - - # When a source declares neither a channel nor a repository the footer used - # to be one lone overflow button adrift on an empty row. The age of the - # cached repomap belongs on a screen about sources anyway — it is what the - # counts above it were read from. - updated_tv = TextView(ctx) - updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) - updated_tv.setSingleLine(True) - updated_tv.setEllipsize(TextUtils.TruncateAt.END) - updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - - def _fill_updated(i): - text = _updated_label(i.get("updated_at")) - updated_tv.setText(text) - updated_tv.setVisibility(0 if text else 8) + footer.addView(chips, LayoutHelper.createLinear(-2, -2)) - _fill_updated(info) - footer.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) + spacer = View(ctx) + footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) - # own container so a repaint can refill it without touching the overflow links = LinearLayout(ctx) links.setOrientation(LinearLayout.HORIZONTAL) links.setGravity(Gravity.CENTER_VERTICAL) @@ -384,7 +416,7 @@ def _fill_links(i): menu_holder = [menu_btn] footer.addView(menu_btn, _btn_lp(0)) - card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) + card.addView(footer, LayoutHelper.createLinear(-1, _ROW_H, 0, 8, 0, 0)) # the card opens the source's sheet; the switch beside it is what turns the # source on and off, so a tap meant for one is never the other @@ -405,7 +437,7 @@ def _apply_enabled(is_on, animate): pass _fill_chips(is_on, state["info"]) target = 1.0 if is_on else 0.55 - for view in (icon_holder[0], col, chips): + for view in (icon_holder[0], col, meta, chips): try: if animate: view.animate().alpha(target).setDuration(160).start() @@ -429,7 +461,7 @@ def _update(new_repo, new_info): name_tv.setText(str(new_repo.get("name") or strings.unnamed)) _fill_sub(new_repo, state["info"]) _fill_links(state["info"]) - _fill_updated(state["info"]) + _fill_meta(state["info"]) new_url = str(state["info"].get("icon_url") or "") if new_url != state["icon_url"]: # only an updated repomap can do this, and then it really is a From 6aa8197e3ea8dc569797220b1d50acc7651a7caf Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:02:14 +0000 Subject: [PATCH 31/46] Put the card's bottom half on one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Splitting the fetch time onto its own row set a line of content against a line of nothing, twice: below the avatar the left half of the card was empty for its entire height, and a source with no channel and no repository ended on a row carrying a single overflow button. It is all one row now — the label takes the slack on the left, the installed pill and the buttons sit at the end of it. The label is the only thing there that can afford to give up room, so it is the weighted one: a long pill shortens the text rather than pushing a button off the card. The plugin and icon counts keep a line of their own, because they are the one thing that can run to three pills at once and no single line survives that. That line is absent unless a catalogue has actually counted something, which for most sources is never. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 97 +++++++++++++---------------- 2 files changed, 46 insertions(+), 53 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index f60c97d..4ac3036 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.1.2-dev.20" +version: "0.1.2-dev.21" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index aaa4a67..9759a12 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -299,11 +299,11 @@ def onTouch(self, v, event): card.addView(header, LayoutHelper.createLinear(-1, -2)) - # Two rows under the header, both 32dp and both anchored the same way: what - # the card knows on the left, what it offers on the right. Before this the - # numbers sat left and the buttons right on a row of their own, so the eye - # had to start in a different place on each line and the last row was often - # a single overflow button by itself. + # One row under the header, not two. Splitting "when it was fetched" from + # the buttons put a line of content against a line of nothing twice over: + # the left half of the card below the avatar was empty for its whole + # height. Everything that is left of a card here is small enough to stand + # on one line — the label takes the slack, the pills sit at the end. on_open = callbacks.get("on_open") or (lambda _u: None) def _btn_lp(right_margin_dp=6): @@ -311,47 +311,9 @@ def _btn_lp(right_margin_dp=6): lp.rightMargin = AndroidUtilities.dp(right_margin_dp) return lp - # ---- meta row: when it was fetched | how much of it is installed - meta = LinearLayout(ctx) - meta.setOrientation(LinearLayout.HORIZONTAL) - meta.setGravity(Gravity.CENTER_VERTICAL) - - updated_tv = TextView(ctx) - updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) - updated_tv.setSingleLine(True) - updated_tv.setEllipsize(TextUtils.TruncateAt.END) - updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - # weighted, so a long "installed" chip eats into the label rather than - # pushing itself off the card - meta.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - - installed_box = LinearLayout(ctx) - installed_box.setOrientation(LinearLayout.HORIZONTAL) - installed_box.setGravity(Gravity.CENTER_VERTICAL) - meta.addView(installed_box, LayoutHelper.createLinear(-2, -2)) - - def _fill_meta(i): - text = _updated_label(i.get("updated_at")) - updated_tv.setText(text) - updated_tv.setVisibility(0 if text else 8) - - installed_box.removeAllViews() - installed = i.get("installed") - if isinstance(installed, int) and installed > 0: - installed_box.addView( - _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) - .replace("{0}", str(installed)), accent), _chip_lp(0)) - meta.setVisibility(0 if (text or installed_box.getChildCount()) else 8) - - _fill_meta(info) - card.addView(meta, LayoutHelper.createLinear(-1, _ROW_H, 0, 12, 0, 0)) - - # ---- action row: what it carries on the left, what you can do on the right - footer = LinearLayout(ctx) - footer.setOrientation(LinearLayout.HORIZONTAL) - footer.setGravity(Gravity.CENTER_VERTICAL) - - # own containers so a repaint can refill them without touching the overflow + # ---- counts, when there are any: these are the one thing that can run to + # three pills at once, which no single line survives, so they get a line of + # their own and it is simply absent the rest of the time chips = LinearLayout(ctx) chips.setOrientation(LinearLayout.HORIZONTAL) chips.setGravity(Gravity.CENTER_VERTICAL) @@ -379,12 +341,42 @@ def _fill_chips(is_on, i): chips.addView( _chip(ctx, str(strings.repo_card_icons).replace("{0}", str(icons_n)), accent), _chip_lp()) + chips.setVisibility(0 if chips.getChildCount() else 8) + + card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) + + # ---- the one row: label, then the installed pill and the buttons + footer = LinearLayout(ctx) + footer.setOrientation(LinearLayout.HORIZONTAL) + footer.setGravity(Gravity.CENTER_VERTICAL) + + updated_tv = TextView(ctx) + updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) + updated_tv.setSingleLine(True) + updated_tv.setEllipsize(TextUtils.TruncateAt.END) + updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) + # weighted: it is the only thing here that can afford to give up room, so a + # long pill shortens the label instead of pushing a button off the card + footer.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - footer.addView(chips, LayoutHelper.createLinear(-2, -2)) + installed_box = LinearLayout(ctx) + installed_box.setOrientation(LinearLayout.HORIZONTAL) + installed_box.setGravity(Gravity.CENTER_VERTICAL) + footer.addView(installed_box, LayoutHelper.createLinear(-2, -2)) + + def _fill_updated(i): + text = _updated_label(i.get("updated_at")) + updated_tv.setText(text) + updated_tv.setVisibility(0 if text else 8) - spacer = View(ctx) - footer.addView(spacer, LayoutHelper.createLinear(0, 0, 1.0)) + installed_box.removeAllViews() + installed = i.get("installed") + if isinstance(installed, int) and installed > 0: + installed_box.addView( + _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) + .replace("{0}", str(installed)), accent), _chip_lp(8)) + # own container so a repaint can refill it without touching the overflow links = LinearLayout(ctx) links.setOrientation(LinearLayout.HORIZONTAL) links.setGravity(Gravity.CENTER_VERTICAL) @@ -402,6 +394,7 @@ def _fill_links(i): _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), _btn_lp()) + _fill_updated(info) _fill_links(info) footer.addView(links, LayoutHelper.createLinear(-2, -2)) @@ -416,7 +409,7 @@ def _fill_links(i): menu_holder = [menu_btn] footer.addView(menu_btn, _btn_lp(0)) - card.addView(footer, LayoutHelper.createLinear(-1, _ROW_H, 0, 8, 0, 0)) + card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) # the card opens the source's sheet; the switch beside it is what turns the # source on and off, so a tap meant for one is never the other @@ -437,7 +430,7 @@ def _apply_enabled(is_on, animate): pass _fill_chips(is_on, state["info"]) target = 1.0 if is_on else 0.55 - for view in (icon_holder[0], col, meta, chips): + for view in (icon_holder[0], col, chips, updated_tv, installed_box): try: if animate: view.animate().alpha(target).setDuration(160).start() @@ -461,7 +454,7 @@ def _update(new_repo, new_info): name_tv.setText(str(new_repo.get("name") or strings.unnamed)) _fill_sub(new_repo, state["info"]) _fill_links(state["info"]) - _fill_meta(state["info"]) + _fill_updated(state["info"]) new_url = str(state["info"].get("icon_url") or "") if new_url != state["icon_url"]: # only an updated repomap can do this, and then it really is a From 784b82705c89dae5db2857d56310ebe2b169bdae Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:04:37 +0000 Subject: [PATCH 32/46] Label the manage button, move the count to the left edge The count sat centred over a list whose cards all start at the left edge, so it lined up with nothing. It moves to that edge. The button opposite it gets its word back: an icon alone says nothing about what is behind it, and the row has room. Cards lose two dp of air top and bottom and two off each row gap. The rows below the header end in 32dp circles that carry their own ring of empty pixels, so a square 16dp all round measured as more than it looked like it needed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 1 + packit/locales/strings_de.json | 1 + packit/locales/strings_en.json | 1 + packit/locales/strings_ru.json | 1 + packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 17 ++++++++---- packit/src/ui/ReposActivity/fragment.py | 37 +++++++++++++++++++------ 7 files changed, 45 insertions(+), 15 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 758d6dd..b8b5209 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -1204,6 +1204,7 @@ "repo_err_unknown": "Невядомая памылка: {0}", "repos_empty_title": "Пакуль пуста", "repos_empty_text": "Дадайце крыніцу, каб ставіць плагіны", + "repos_manage": "Кіраванне", "retry": "Паўтарыць", "repo_default_already": "Стандартная крыніца ўжо на месцы", "repo_link_shared": "Спасылка на крыніцу адпраўлена" diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 3615715..584a493 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -1204,6 +1204,7 @@ "repo_err_unknown": "Unbekannter Fehler: {0}", "repos_empty_title": "Noch nichts da", "repos_empty_text": "Füge eine Quelle hinzu, um Plugins zu installieren", + "repos_manage": "Verwalten", "retry": "Erneut versuchen", "repo_default_already": "Die Standardquelle ist bereits vorhanden", "repo_link_shared": "Link zur Quelle gesendet" diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 281c147..5c829cf 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -1204,6 +1204,7 @@ "repo_err_unknown": "Unknown error: {0}", "repos_empty_title": "Nothing here yet", "repos_empty_text": "Add a source to install plugins", + "repos_manage": "Manage", "retry": "Retry", "repo_default_already": "The default repository is already there", "repo_link_shared": "Repository link sent" diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 0242a18..1bf92ce 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -1204,6 +1204,7 @@ "repo_err_unknown": "Неизвестная ошибка: {0}", "repos_empty_title": "Пока пусто", "repos_empty_text": "Добавьте источник, чтобы ставить плагины", + "repos_manage": "Управление", "retry": "Повторить", "repo_default_already": "Стандартный источник уже на месте", "repo_link_shared": "Ссылка на источник отправлена" diff --git a/packit/meta.yml b/packit/meta.yml index 4ac3036..5717e42 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.1.2-dev.21" +version: "0.1.2-dev.22" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 9759a12..6e1090d 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -3,9 +3,10 @@ # One repository card. # -# Deliberately not the plugin card: that one is a filled surface with a 18dp -# radius, this is an outlined 16dp container so the two lists read as different -# places. Everything the card shows comes from the repomap already sitting in +# A filled 16dp container, a header row and one row under it. It started out +# outlined, to read as a different kind of thing from the plugin card, but a +# hairline in the divider colour reads as a stray line and not as an edge. +# Everything the card shows comes from the repomap already sitting in # reposCache — the card never touches the network. from packutil import logx @@ -163,7 +164,11 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict, handle: dict = card = LinearLayout(ctx) card.setOrientation(LinearLayout.VERTICAL) - card.setPadding(*(AndroidUtilities.dp(16),) * 4) + # Tighter top and bottom than the sides. The rows below the header end in + # 32dp circles that already carry their own ring of empty pixels, so a + # square 16dp all round measured as more air than it looked like it needed. + card.setPadding(AndroidUtilities.dp(16), AndroidUtilities.dp(14), + AndroidUtilities.dp(16), AndroidUtilities.dp(12)) card.setClickable(True) card.setFocusable(True) # PluginCell does both of these on itself, and this is why: with exteraGram's @@ -343,7 +348,7 @@ def _fill_chips(is_on, i): _chip_lp()) chips.setVisibility(0 if chips.getChildCount() else 8) - card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) + card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) # ---- the one row: label, then the installed pill and the buttons footer = LinearLayout(ctx) @@ -409,7 +414,7 @@ def _fill_links(i): menu_holder = [menu_btn] footer.addView(menu_btn, _btn_lp(0)) - card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 12, 0, 0)) + card.addView(footer, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) # the card opens the source's sheet; the switch beside it is what turns the # source on and off, so a tap meant for one is never the other diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index f73db04..479c02b 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -386,25 +386,33 @@ def _build_summary_row(self, act): row = FrameLayout(act) + # Left, not centred: a count centred over a left-aligned list has + # nothing under it to line up with, and the cards below all start at + # the same edge this now starts at. self._summary = TextView(act) self._summary.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16) self._summary.setGravity(Gravity.CENTER) - self._summary.setPadding(AndroidUtilities.dp(12), AndroidUtilities.dp(7), - AndroidUtilities.dp(12), AndroidUtilities.dp(7)) + self._summary.setPadding(AndroidUtilities.dp(14), AndroidUtilities.dp(7), + AndroidUtilities.dp(14), AndroidUtilities.dp(7)) self._summary.setTextColor(text_color) try: self._summary.setBackground(Theme.createSimpleSelectorRoundRectDrawable( AndroidUtilities.dp(16), card_bg, card_bg)) except Exception as e: logx(f"repos fragment: summary pill background error: {e}", True) - row.addView(self._summary, FrameLayout.LayoutParams(-2, -2, Gravity.CENTER)) + row.addView(self._summary, FrameLayout.LayoutParams( + -2, -2, Gravity.LEFT | Gravity.CENTER_VERTICAL)) - # the bulk actions the old screen kept under "Дополнительно" + # the bulk actions the old screen kept under "Дополнительно". Labelled, + # not a bare icon: an icon on its own says nothing about what is behind + # it, and there is room on this row for the word. def _menu(v=None): from . import actions actions.show_bulk_menu(act, self, menu_btn) - menu_btn = FrameLayout(act) + menu_btn = LinearLayout(act) + menu_btn.setOrientation(LinearLayout.HORIZONTAL) + menu_btn.setGravity(Gravity.CENTER_VERTICAL) menu_btn.setClickable(True) menu_btn.setFocusable(True) try: @@ -412,15 +420,28 @@ def _menu(v=None): AndroidUtilities.dp(16), card_bg, card_pressed)) except Exception: pass - menu_btn.setPadding(*(AndroidUtilities.dp(8),) * 4) + menu_btn.setPadding(AndroidUtilities.dp(12), AndroidUtilities.dp(8), + AndroidUtilities.dp(14), AndroidUtilities.dp(8)) menu_icon = ImageView(act) try: menu_icon.setImageResource(getattr(R_tg.drawable, "msg_customize")) menu_icon.setColorFilter(text_color) except Exception: pass - menu_btn.addView(menu_icon, FrameLayout.LayoutParams( - AndroidUtilities.dp(20), AndroidUtilities.dp(20), Gravity.CENTER)) + icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(20), AndroidUtilities.dp(20)) + icon_lp.rightMargin = AndroidUtilities.dp(6) + menu_btn.addView(menu_icon, icon_lp) + + menu_label = TextView(act) + menu_label.setText(str(getattr(strings, "repos_manage", "Manage"))) + menu_label.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) + menu_label.setTextColor(text_color) + try: + menu_label.setTypeface(AndroidUtilities.bold()) + except Exception: + pass + menu_btn.addView(menu_label, LinearLayout.LayoutParams(-2, -2)) + menu_btn.setOnClickListener(OnClickListener(_menu)) apply_press_scale_on_target(menu_btn, menu_btn) row.addView(menu_btn, FrameLayout.LayoutParams( From 75d31ed4d81878d8fd4558fc1146f02d96ac12af Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:09:39 +0000 Subject: [PATCH 33/46] Move the fetch time off the card, pin the installed pill left MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The timestamp was the wrong thing to give half a row to. The pill next to it kept squeezing it down to "обновлена т…", and nobody opens a list of sources to read one. It moves into the sheet the card already opens, where a detail has room to be read whole. That leaves the row reading left to right the way the rest of the card does: what this source has given you, then what you can do with it. The pill is always drawn, zero included — a source you have installed nothing from is worth saying out loud, and one that appears and disappears makes the row shift as the numbers change. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/ui/ReposActivity/card.py | 56 ++++++++---------------- packit/src/ui/ReposActivity/repoSheet.py | 26 +++++++++++ 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 5717e42..f4a5efb 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.1.2-dev.22" +version: "0.1.2-dev.23" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/ReposActivity/card.py index 6e1090d..a9cc03c 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/ReposActivity/card.py @@ -350,36 +350,34 @@ def _fill_chips(is_on, i): card.addView(chips, LayoutHelper.createLinear(-1, -2, 0, 10, 0, 0)) - # ---- the one row: label, then the installed pill and the buttons + # ---- the one row: the installed pill on the left, the buttons on the right + # + # The fetch time used to hold this left side. It was the wrong thing to + # give a whole row to — the pill beside it kept squeezing it down to + # "обновлена т…", and no one opens this screen to read a timestamp. It + # moved into the sheet the card opens, which is where a detail belongs. footer = LinearLayout(ctx) footer.setOrientation(LinearLayout.HORIZONTAL) footer.setGravity(Gravity.CENTER_VERTICAL) - updated_tv = TextView(ctx) - updated_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) - updated_tv.setSingleLine(True) - updated_tv.setEllipsize(TextUtils.TruncateAt.END) - updated_tv.setTextColor(_theme("key_windowBackgroundWhiteGrayText")) - # weighted: it is the only thing here that can afford to give up room, so a - # long pill shortens the label instead of pushing a button off the card - footer.addView(updated_tv, LayoutHelper.createLinear(0, -2, 1.0, Gravity.CENTER_VERTICAL)) - installed_box = LinearLayout(ctx) installed_box.setOrientation(LinearLayout.HORIZONTAL) installed_box.setGravity(Gravity.CENTER_VERTICAL) footer.addView(installed_box, LayoutHelper.createLinear(-2, -2)) - def _fill_updated(i): - text = _updated_label(i.get("updated_at")) - updated_tv.setText(text) - updated_tv.setVisibility(0 if text else 8) + footer.addView(View(ctx), LayoutHelper.createLinear(0, 0, 1.0)) + def _fill_installed(i): + # always drawn, zero included: a source you have taken nothing from is + # a fact worth stating, and a pill that comes and goes makes the row + # jump around as the numbers change installed_box.removeAllViews() installed = i.get("installed") - if isinstance(installed, int) and installed > 0: - installed_box.addView( - _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) - .replace("{0}", str(installed)), accent), _chip_lp(8)) + if not isinstance(installed, int) or installed < 0: + installed = 0 + installed_box.addView( + _chip(ctx, str(getattr(strings, "repo_card_installed", "{0} installed")) + .replace("{0}", str(installed)), accent), _chip_lp(8)) # own container so a repaint can refill it without touching the overflow links = LinearLayout(ctx) @@ -399,7 +397,7 @@ def _fill_links(i): _round_icon_button(ctx, "msg_link", accent, lambda u=src_url: on_open(u)), _btn_lp()) - _fill_updated(info) + _fill_installed(info) _fill_links(info) footer.addView(links, LayoutHelper.createLinear(-2, -2)) @@ -435,7 +433,7 @@ def _apply_enabled(is_on, animate): pass _fill_chips(is_on, state["info"]) target = 1.0 if is_on else 0.55 - for view in (icon_holder[0], col, chips, updated_tv, installed_box): + for view in (icon_holder[0], col, chips, installed_box): try: if animate: view.animate().alpha(target).setDuration(160).start() @@ -459,7 +457,7 @@ def _update(new_repo, new_info): name_tv.setText(str(new_repo.get("name") or strings.unnamed)) _fill_sub(new_repo, state["info"]) _fill_links(state["info"]) - _fill_updated(state["info"]) + _fill_installed(state["info"]) new_url = str(state["info"].get("icon_url") or "") if new_url != state["icon_url"]: # only an updated repomap can do this, and then it really is a @@ -521,22 +519,6 @@ def _build_switch(ctx, checked: bool): return None -def _updated_label(mtime) -> str: - # LocaleController already words "just now / N minutes ago / today at …" for - # the client's location updates, in every language it ships, and a repomap - # cache is the same kind of fact — so the wording comes from there rather - # than from four more locale keys of my own. - try: - seconds = int(float(mtime or 0)) - if seconds <= 0: - return "" - from org.telegram.messenger import LocaleController - return str(LocaleController.formatLocationUpdateDate(seconds)) - except Exception as e: - logx(f"repos card: updated label unavailable: {e}", True) - return "" - - def _host_of(url) -> str: try: text = str(url or "") diff --git a/packit/src/ui/ReposActivity/repoSheet.py b/packit/src/ui/ReposActivity/repoSheet.py index 5fd37d0..cc2dd86 100644 --- a/packit/src/ui/ReposActivity/repoSheet.py +++ b/packit/src/ui/ReposActivity/repoSheet.py @@ -45,6 +45,22 @@ def _theme(key: str, fallback: int = 0): return fallback +def _updated_label(mtime) -> str: + # LocaleController already words "just now / N minutes ago / today at …" for + # the client's location updates, in every language it ships, and a repomap + # cache is the same kind of fact — so the wording comes from there rather + # than from four more locale keys of my own. + try: + seconds = int(float(mtime or 0)) + if seconds <= 0: + return "" + from org.telegram.messenger import LocaleController + return str(LocaleController.formatLocationUpdateDate(seconds)) + except Exception as e: + logx(f"repoSheet: updated label unavailable: {e}", True) + return "" + + def _handle(ctx): from android.graphics.drawable import GradientDrawable bar = TextView(ctx) @@ -122,6 +138,16 @@ def _show(): header.addView(col, LayoutHelper.createLinear(-1, -2)) root.addView(header, LayoutHelper.createLinear(-1, -2)) + # off the card and in here, where a detail has room to be read in + # full instead of being squeezed to "обновлена т…" + updated = _updated_label(info.get("updated_at")) + if updated: + upd = TextView(act) + upd.setText(updated) + upd.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + upd.setTextColor(_theme("key_dialogTextGray2")) + root.addView(upd, LayoutHelper.createLinear(-1, -2, 0, 16, 0, 0)) + sheet.setCustomView(root) applyFontToTree(root) sheet.show() From b55390f9e5aa505cc0d6e548739b2b3039a33d2a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:19:20 +0000 Subject: [PATCH 34/46] Show the repository in the add sheet, not a folder glyph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The picture at the top of the repo=add sheet was an R.drawable tinted with the accent — the same folder for every repository in existence, which told the reader nothing about the one they were about to add. It is the repomap's own rm_icon now, drawn by the same loader the sources screen uses, so a source without a picture falls back to its monogram instead of to a generic icon. The link's icon argument is gone from the reading side entirely. It named an R.drawable, and a repository is not a glyph out of the client's sheet. It stays in the accepted set so that links already sent are tolerated rather than rejected, and it is no longer minted into new ones or written into storage. The rest of the sheet follows: the repository's name as the title, since the question is what the buttons are for and the name is what is being decided about; the maintainer under it with the mention live; the plugin count and the host as pills, the same ones the cards use, rather than as clauses inside a paragraph. What is left of the disclaimer is the sentence that was actually a disclaimer. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/locales/strings_be.json | 1 + packit/locales/strings_de.json | 1 + packit/locales/strings_en.json | 1 + packit/locales/strings_ru.json | 1 + packit/meta.yml | 2 +- packit/src/deeplinks/repo.py | 128 +++++++++++++++++------- packit/src/ui/ReposActivity/actions.py | 6 +- packit/src/ui/ReposActivity/repoIcon.py | 46 --------- 8 files changed, 101 insertions(+), 85 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index b8b5209..8b201fe 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -169,6 +169,7 @@ "repo_add_fetching": "Загрузка дадзеных рэпазіторыя...", "repo_add_fetch_failed": "Не атрымалася загрузіць дадзеныя рэпазіторыя.", "repo_add_disclaimer": "PackIt не нясе адказнасці за змест {0}. Усе правы і адказнасць належаць уладальніку, {1}.\\n\\nКолькасць плагінаў: {2}", + "repo_add_disclaimer_short": "PackIt не адказвае за змест крыніцы — за яго адказвае яе ўладальнік.", "repo_add_no_repometa": "Рэпазіторый не мае метададзеных. Дадаць усё роўна?\\n\\nКолькасць плагінаў: {0}", "sort_title": "Сартаваць плагіны", "sort_title_icons": "Сартаванне набораў іконак", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 584a493..d528540 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -169,6 +169,7 @@ "repo_add_fetching": "Repository-Daten werden abgerufen...", "repo_add_fetch_failed": "Das Abrufen der Repository-Daten ist fehlgeschlagen.", "repo_add_disclaimer": "PackIt ist nicht für den Inhalt von {0} verantwortlich. Alle Rechte und Pflichten liegen beim Eigentümer {1}.\n\nPlugin-Anzahl: {2}", + "repo_add_disclaimer_short": "PackIt ist nicht für den Inhalt einer Quelle verantwortlich, sondern deren Besitzer.", "repo_add_no_repometa": "Das Repository verfügt über keine Metadaten. Trotzdem hinzufügen?\n\nPlugin-Anzahl: {0}", "sort_title": "Plugins sortieren", "sort_title_icons": "Symbolpakete sortieren", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 5c829cf..dd0b568 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -169,6 +169,7 @@ "repo_add_fetching": "Fetching repository data...", "repo_add_fetch_failed": "Failed to fetch repository data.", "repo_add_disclaimer": "PackIt is not responsible for the content of {0}. All rights and responsibility belong to the owner, {1}.\n\nPlugin count: {2}", + "repo_add_disclaimer_short": "PackIt is not responsible for what a source contains. Its owner is.", "repo_add_no_repometa": "Repository has no metadata. Add anyway?\n\nPlugin count: {0}", "sort_title": "Sort Plugins", "sort_title_icons": "Sort Icons", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 1bf92ce..72e3874 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -169,6 +169,7 @@ "repo_add_fetching": "Получение данных...", "repo_add_fetch_failed": "Не удалось получить данные репозитория.", "repo_add_disclaimer": "PackIt не отвечает за содержимое репозитория {0}. Все права и ответственность за него несёт владелец, то есть {1}.\n\nКоличество плагинов: {2}", + "repo_add_disclaimer_short": "PackIt не отвечает за содержимое источника — за него отвечает его владелец.", "repo_add_no_repometa": "Метаданные репозитория потерялись...( Добавить всё равно?\n\nКоличество плагинов: {0}", "sort_title": "Сортировка плагинов", "sort_title_icons": "Сортировка наборов иконок", diff --git a/packit/meta.yml b/packit/meta.yml index f4a5efb..d8478c5 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.1.2-dev.23" +version: "0.1.2-dev.24" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index a55c7c9..2dcca81 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -6,7 +6,7 @@ from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread, OnClickListener -from android.widget import LinearLayout, TextView, FrameLayout, ImageView, ScrollView +from android.widget import LinearLayout, TextView, FrameLayout, ScrollView from android.util import TypedValue from android.view import Gravity from android.graphics.drawable import GradientDrawable @@ -40,7 +40,13 @@ BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") -# repo=add: required: link — optional: name, icon +# repo=add: required: link — optional: name +# +# "icon" stays in the accepted set and is read nowhere. It used to name an +# R.drawable, which is not how a repository is pictured any more — the icon +# comes out of the repomap the link points at. Links minted before that are +# still in people's chats, so the argument has to be tolerated rather than +# rejected; it is simply ignored. _REPO_ADD_REQUIRED = {"link"} _REPO_ADD_OPTIONAL = {"name", "icon"} _REPO_ADD_ALL = _REPO_ADD_REQUIRED | _REPO_ADD_OPTIONAL @@ -51,6 +57,22 @@ def _get_cache_dir() -> str: return getReposCacheDir() +def _sheet_chip(act, text: str): + # the same pill the source cards use, so the sheet that adds a source and + # the card it becomes are recognisably the same thing + from ..ui.ReposActivity.card import _chip + from ..ui.ReposActivity.repoIcon import accent_for + return _chip(act, text, accent_for({})) + + +def _sheet_chip_lp(margin_dp=3): + from ..ui.ReposActivity.card import _ROW_H + lp = LinearLayout.LayoutParams(-2, AndroidUtilities.dp(_ROW_H)) + lp.leftMargin = AndroidUtilities.dp(margin_dp) + lp.rightMargin = AndroidUtilities.dp(margin_dp) + return lp + + def handle(url, repoManager): try: if "repo=add" not in url: @@ -70,7 +92,6 @@ def handle(url, repoManager): name = query.get("name", [""])[0].strip() link = query.get("link", [""])[0].strip() - icon = query.get("icon", [""])[0].strip() if not link: BulletinHelper.show_error(strings.repo_add_invalid) @@ -129,14 +150,14 @@ def fetch_task(): except Exception as e: logx(f"repo deeplink: fetch error: {e}", False) - run_on_ui_thread(lambda: _show_confirm_sheet(repometa, pluginCount, name, link, icon, repoManager)) + run_on_ui_thread(lambda: _show_confirm_sheet(repometa, pluginCount, name, link, repoManager)) run_on_queue(fetch_task) except Exception as e: logx(f"repo deeplink: handle error: {e}", False) -def _show_confirm_sheet(repometa, pluginCount, name, link, icon, repoManager): +def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): try: frag = get_last_fragment() act = frag.getParentActivity() if frag else None @@ -147,11 +168,16 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, icon, repoManager): BulletinHelper.show_error(str(strings["dl_repo_no_metadata"])) return + rm_rid = str(repometa.get("rm_rid") or "") + rm_name = str(repometa.get("rm_name") or name or "") rm_url = str(repometa.get("rm_url") or link) - rm_url_display = rm_url.removeprefix("https://").removeprefix("http://") + rm_url_display = rm_url.removeprefix("https://").removeprefix("http://").rstrip("/") rm_maintainer = str(repometa.get("rm_maintainer") or name) - rm_icon = icon if icon else str(repometa.get("rm_icon") or "msg_folders") - disclaimer_text = strings("repo_add_disclaimer", rm_url_display, rm_maintainer, pluginCount) + # only ever the repomap's own picture — the link's icon argument named an + # R.drawable, and a repository is not a glyph out of the client's sheet + rm_icon = str(repometa.get("rm_icon") or "").strip() + if not rm_icon.lower().startswith(("http://", "https://")): + rm_icon = "" sheet = BottomSheet(act, False, frag.getResourceProvider()) sheet.fixNavigationBar() @@ -161,49 +187,78 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, icon, repoManager): linear.setOrientation(LinearLayout.VERTICAL) frame.addView(linear) - # icon centered — rm_icon is an image url in current repomaps and a - # R.drawable name in older ones, so both have to work here + # The source itself, drawn the way the sources screen draws it: the + # repomap's picture, falling back to a monogram on a tonal square. What + # used to be here was a folder glyph tinted with the accent — the same + # picture for every repository in existence, which told the reader + # nothing about the one they were about to add. try: - icon_view = ImageView(act) - if str(rm_icon).lower().startswith(("http://", "https://")): - from ..ui.ReposActivity.repoIcon import load_url_into - load_url_into(icon_view, rm_icon, 48) - else: - icon_id = getattr(R_tg.drawable, rm_icon, 0) - if not icon_id: - icon_id = getattr(R_tg.drawable, "msg_folders", 0) - if icon_id: - icon_view.setImageResource(icon_id) - icon_view.setColorFilter(Theme.getColor(Theme.key_featuredStickers_addButton)) - linear.addView(icon_view, LayoutHelper.createLinear(48, 48, Gravity.CENTER_HORIZONTAL, 0, 20, 0, 0)) + from ..ui.ReposActivity.repoIcon import build_icon_view + icon_view = build_icon_view( + act, {"id": rm_rid, "name": rm_name, "url": link}, 64, 18, rm_icon) + linear.addView(icon_view, LayoutHelper.createLinear( + 64, 64, Gravity.CENTER_HORIZONTAL, 0, 22, 0, 0)) except Exception as e: logx(f"repo deeplink: icon error: {e}", False) - # title + # the repository's name, not "Add repository?" — the question is what + # the buttons are for, and the name is the thing being decided about title_tv = TextView(act) title_tv.setGravity(Gravity.CENTER_HORIZONTAL) - title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 20) + title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 21) + title_tv.setSingleLine(True) + try: + from android.text import TextUtils as _TextUtils + title_tv.setEllipsize(_TextUtils.TruncateAt.END) + except Exception: + pass try: title_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) except Exception: title_tv.setTypeface(AndroidUtilities.bold()) - title_tv.setText(strings.repo_add_title) + title_tv.setText(rm_name or str(strings.repo_add_title)) title_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteBlackText)) - linear.addView(title_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 16.0, 21.0, 0.0)) + linear.addView(title_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 14.0, 21.0, 0.0)) + + # maintainer, with the mention live + if rm_maintainer: + sub_tv = TextView(act) + sub_tv.setGravity(Gravity.CENTER_HORIZONTAL) + sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) + sub_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) + try: + sub_tv.setText(LocaleUtils.fullyFormatText(rm_maintainer)) + sub_tv.setLinkTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText)) + sub_tv.setMovementMethod(LinkMovementMethod.getInstance()) + except Exception: + sub_tv.setText(rm_maintainer) + linear.addView(sub_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 4.0, 21.0, 0.0)) + + # the facts as pills rather than as a sentence: how much is in there and + # where it comes from + try: + chips_row = LinearLayout(act) + chips_row.setOrientation(LinearLayout.HORIZONTAL) + chips_row.setGravity(Gravity.CENTER) + if pluginCount: + chips_row.addView( + _sheet_chip(act, str(strings.repo_card_plugins).replace("{0}", str(pluginCount))), + _sheet_chip_lp(3)) + if rm_url_display: + chips_row.addView(_sheet_chip(act, rm_url_display), _sheet_chip_lp(3)) + if chips_row.getChildCount(): + linear.addView(chips_row, LayoutHelper.createFrame(-1, -2, 0, 16.0, 14.0, 16.0, 0.0)) + except Exception as e: + logx(f"repo deeplink: chips error: {e}", False) - # disclaimer with accent links + # what is left of the disclaimer once the concrete facts are drawn above msg_tv = TextView(act) msg_tv.setGravity(Gravity.CENTER_HORIZONTAL) - msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) - try: - msg_tv.setText(LocaleUtils.fullyFormatText(disclaimer_text)) - msg_tv.setLinkTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText)) - msg_tv.setMovementMethod(LinkMovementMethod.getInstance()) - except Exception: - msg_tv.setText(disclaimer_text) + msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + msg_tv.setText(str(strings["repo_add_disclaimer_short"])) msg_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) msg_tv.setLineSpacing(AndroidUtilities.dp(2), 1.0) - linear.addView(msg_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 12.0, 21.0, 0.0)) + linear.addView(msg_tv, LayoutHelper.createFrame(-1, -2, 0, 24.0, 14.0, 24.0, 0.0)) # add button add_btn = ButtonWithCounterView(act, True, frag.getResourceProvider()) @@ -224,13 +279,14 @@ def onClick(self, v): sheet.dismiss() return + # no "icon": it held an R.drawable name and nothing reads + # one any more — the picture comes from the repomap newRepo = { "id": rm_rid, "name": repo_name, "url": link, "enabled": True, "collapsed": False, - "icon": icon if icon else "msg_folders" } currentRepos.append(newRepo) repoManager.setRepositories(currentRepos) diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/ReposActivity/actions.py index 072ab87..c1c3ebb 100644 --- a/packit/src/ui/ReposActivity/actions.py +++ b/packit/src/ui/ReposActivity/actions.py @@ -82,8 +82,10 @@ def _share_link(repo: dict) -> str: for ch, esc in (("%", "%25"), ("&", "%26"), ("=", "%3D"), ("#", "%23"), (" ", "%20")): name = name.replace(ch, esc) url = str(repo.get("url") or "").strip() - icon = str(repo.get("icon") or "").strip() - return f"tg://packit?repo=add&name={name}&link={url}&icon={icon}" + # no icon= any more: it carried an R.drawable name, and the other side now + # takes the picture from the repomap. Links already sent with one still + # work — the argument is accepted and ignored. + return f"tg://packit?repo=add&name={name}&link={url}" def share_repository(act, repo: dict): diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index 5323354..fde54c5 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -190,52 +190,6 @@ def _load_bitmap(url: str, px: int): return bmp -def load_url_into(image_view, url: str, size_dp: int = 48): - # for callers that already have their own ImageView (the repo=add deeplink - # sheet), no monogram layer involved - if not url: - return - size_px = AndroidUtilities.dp(size_dp) - want = f"packit_repoicon_url_{abs(hash(url))}" - try: - image_view.setTag(want) - except Exception: - pass - - cached = peek_bitmap(url, size_px) - if cached is not None: - try: - image_view.setImageBitmap(cached) - try: - image_view.setColorFilter(None) - except Exception: - pass - return - except Exception as e: - logx(f"repoIcon: cached url bind error: {e}", False) - - def _task(): - bmp = _load_bitmap(url, size_px) - if bmp is None: - return - - def _apply(): - try: - if str(image_view.getTag() or "") != want: - return - image_view.setImageBitmap(bmp) - try: - image_view.setColorFilter(None) - except Exception: - pass - except Exception as e: - logx(f"repoIcon: url bind error: {e}", False) - - run_on_ui_thread(_apply) - - imagePool.submit(_task) - - def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url=None): # monogram now, real icon when it arrives — unless it has already arrived # once, in which case it is on screen before the card is From ef04e64c14bd297193e67697e9d1205de593b553 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:23:54 +0000 Subject: [PATCH 35/46] Reword the add-sheet disclaimer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "PackIt is not responsible for what a source contains. Its owner is." was a sentence written to fit a legal shape rather than to be read. It also never said the thing a reader actually needs at that moment: that nobody has looked inside this repository. Now it does — third-party source, unchecked, owner's responsibility — in one sentence, and without the pronoun tangle the Russian had. Co-Authored-By: Claude Opus 5 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 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 8b201fe..882ed64 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Загрузка дадзеных рэпазіторыя...", "repo_add_fetch_failed": "Не атрымалася загрузіць дадзеныя рэпазіторыя.", "repo_add_disclaimer": "PackIt не нясе адказнасці за змест {0}. Усе правы і адказнасць належаць уладальніку, {1}.\\n\\nКолькасць плагінаў: {2}", - "repo_add_disclaimer_short": "PackIt не адказвае за змест крыніцы — за яго адказвае яе ўладальнік.", + "repo_add_disclaimer_short": "Гэта старонняя крыніца. PackIt не правярае яе змест — за гэта адказвае ўладальнік.", "repo_add_no_repometa": "Рэпазіторый не мае метададзеных. Дадаць усё роўна?\\n\\nКолькасць плагінаў: {0}", "sort_title": "Сартаваць плагіны", "sort_title_icons": "Сартаванне набораў іконак", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index d528540..7c12fb3 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Repository-Daten werden abgerufen...", "repo_add_fetch_failed": "Das Abrufen der Repository-Daten ist fehlgeschlagen.", "repo_add_disclaimer": "PackIt ist nicht für den Inhalt von {0} verantwortlich. Alle Rechte und Pflichten liegen beim Eigentümer {1}.\n\nPlugin-Anzahl: {2}", - "repo_add_disclaimer_short": "PackIt ist nicht für den Inhalt einer Quelle verantwortlich, sondern deren Besitzer.", + "repo_add_disclaimer_short": "Dies ist eine Quelle von Dritten. PackIt prüft ihren Inhalt nicht — dafür ist der Besitzer verantwortlich.", "repo_add_no_repometa": "Das Repository verfügt über keine Metadaten. Trotzdem hinzufügen?\n\nPlugin-Anzahl: {0}", "sort_title": "Plugins sortieren", "sort_title_icons": "Symbolpakete sortieren", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index dd0b568..51800ef 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Fetching repository data...", "repo_add_fetch_failed": "Failed to fetch repository data.", "repo_add_disclaimer": "PackIt is not responsible for the content of {0}. All rights and responsibility belong to the owner, {1}.\n\nPlugin count: {2}", - "repo_add_disclaimer_short": "PackIt is not responsible for what a source contains. Its owner is.", + "repo_add_disclaimer_short": "This is a third-party source. PackIt does not check what it contains — its owner is responsible for that.", "repo_add_no_repometa": "Repository has no metadata. Add anyway?\n\nPlugin count: {0}", "sort_title": "Sort Plugins", "sort_title_icons": "Sort Icons", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 72e3874..50c909d 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Получение данных...", "repo_add_fetch_failed": "Не удалось получить данные репозитория.", "repo_add_disclaimer": "PackIt не отвечает за содержимое репозитория {0}. Все права и ответственность за него несёт владелец, то есть {1}.\n\nКоличество плагинов: {2}", - "repo_add_disclaimer_short": "PackIt не отвечает за содержимое источника — за него отвечает его владелец.", + "repo_add_disclaimer_short": "Это сторонний источник. PackIt не проверяет его содержимое — за это отвечает владелец.", "repo_add_no_repometa": "Метаданные репозитория потерялись...( Добавить всё равно?\n\nКоличество плагинов: {0}", "sort_title": "Сортировка плагинов", "sort_title_icons": "Сортировка наборов иконок", diff --git a/packit/meta.yml b/packit/meta.yml index d8478c5..29c5229 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.1.2-dev.24" +version: "0.1.2-dev.25" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" From 3fa74b266405d9554c08a5029a638149acf2a371 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:25:01 +0000 Subject: [PATCH 36/46] Cut the add-sheet disclaimer to one line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two sentences to say one thing. Both facts a reader needs — that this came from someone else and that nobody looked inside — fit in five words. Co-Authored-By: Claude Opus 5 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 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 882ed64..6bdffe7 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Загрузка дадзеных рэпазіторыя...", "repo_add_fetch_failed": "Не атрымалася загрузіць дадзеныя рэпазіторыя.", "repo_add_disclaimer": "PackIt не нясе адказнасці за змест {0}. Усе правы і адказнасць належаць уладальніку, {1}.\\n\\nКолькасць плагінаў: {2}", - "repo_add_disclaimer_short": "Гэта старонняя крыніца. PackIt не правярае яе змест — за гэта адказвае ўладальнік.", + "repo_add_disclaimer_short": "Старонняя крыніца — PackIt яе не правярае.", "repo_add_no_repometa": "Рэпазіторый не мае метададзеных. Дадаць усё роўна?\\n\\nКолькасць плагінаў: {0}", "sort_title": "Сартаваць плагіны", "sort_title_icons": "Сартаванне набораў іконак", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index 7c12fb3..e2ce67c 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Repository-Daten werden abgerufen...", "repo_add_fetch_failed": "Das Abrufen der Repository-Daten ist fehlgeschlagen.", "repo_add_disclaimer": "PackIt ist nicht für den Inhalt von {0} verantwortlich. Alle Rechte und Pflichten liegen beim Eigentümer {1}.\n\nPlugin-Anzahl: {2}", - "repo_add_disclaimer_short": "Dies ist eine Quelle von Dritten. PackIt prüft ihren Inhalt nicht — dafür ist der Besitzer verantwortlich.", + "repo_add_disclaimer_short": "Fremde Quelle — PackIt prüft sie nicht.", "repo_add_no_repometa": "Das Repository verfügt über keine Metadaten. Trotzdem hinzufügen?\n\nPlugin-Anzahl: {0}", "sort_title": "Plugins sortieren", "sort_title_icons": "Symbolpakete sortieren", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index 51800ef..d292e30 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Fetching repository data...", "repo_add_fetch_failed": "Failed to fetch repository data.", "repo_add_disclaimer": "PackIt is not responsible for the content of {0}. All rights and responsibility belong to the owner, {1}.\n\nPlugin count: {2}", - "repo_add_disclaimer_short": "This is a third-party source. PackIt does not check what it contains — its owner is responsible for that.", + "repo_add_disclaimer_short": "Third-party source — PackIt does not check it.", "repo_add_no_repometa": "Repository has no metadata. Add anyway?\n\nPlugin count: {0}", "sort_title": "Sort Plugins", "sort_title_icons": "Sort Icons", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 50c909d..3ecc07c 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Получение данных...", "repo_add_fetch_failed": "Не удалось получить данные репозитория.", "repo_add_disclaimer": "PackIt не отвечает за содержимое репозитория {0}. Все права и ответственность за него несёт владелец, то есть {1}.\n\nКоличество плагинов: {2}", - "repo_add_disclaimer_short": "Это сторонний источник. PackIt не проверяет его содержимое — за это отвечает владелец.", + "repo_add_disclaimer_short": "Сторонний источник — PackIt его не проверяет.", "repo_add_no_repometa": "Метаданные репозитория потерялись...( Добавить всё равно?\n\nКоличество плагинов: {0}", "sort_title": "Сортировка плагинов", "sort_title_icons": "Сортировка наборов иконок", diff --git a/packit/meta.yml b/packit/meta.yml index 29c5229..8027603 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.1.2-dev.25" +version: "0.1.2-dev.26" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" From 858b1f86c2bf2e50b4b2ddd259a4c76fc0c635c4 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:29:54 +0000 Subject: [PATCH 37/46] Use the wording the author asked for on the add sheet Co-Authored-By: Claude Opus 5 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 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packit/locales/strings_be.json b/packit/locales/strings_be.json index 6bdffe7..14aa9ed 100644 --- a/packit/locales/strings_be.json +++ b/packit/locales/strings_be.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Загрузка дадзеных рэпазіторыя...", "repo_add_fetch_failed": "Не атрымалася загрузіць дадзеныя рэпазіторыя.", "repo_add_disclaimer": "PackIt не нясе адказнасці за змест {0}. Усе правы і адказнасць належаць уладальніку, {1}.\\n\\nКолькасць плагінаў: {2}", - "repo_add_disclaimer_short": "Старонняя крыніца — PackIt яе не правярае.", + "repo_add_disclaimer_short": "PackIt не адказвае за змест старонніх крыніц.", "repo_add_no_repometa": "Рэпазіторый не мае метададзеных. Дадаць усё роўна?\\n\\nКолькасць плагінаў: {0}", "sort_title": "Сартаваць плагіны", "sort_title_icons": "Сартаванне набораў іконак", diff --git a/packit/locales/strings_de.json b/packit/locales/strings_de.json index e2ce67c..21b717d 100644 --- a/packit/locales/strings_de.json +++ b/packit/locales/strings_de.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Repository-Daten werden abgerufen...", "repo_add_fetch_failed": "Das Abrufen der Repository-Daten ist fehlgeschlagen.", "repo_add_disclaimer": "PackIt ist nicht für den Inhalt von {0} verantwortlich. Alle Rechte und Pflichten liegen beim Eigentümer {1}.\n\nPlugin-Anzahl: {2}", - "repo_add_disclaimer_short": "Fremde Quelle — PackIt prüft sie nicht.", + "repo_add_disclaimer_short": "PackIt ist nicht für den Inhalt fremder Quellen verantwortlich.", "repo_add_no_repometa": "Das Repository verfügt über keine Metadaten. Trotzdem hinzufügen?\n\nPlugin-Anzahl: {0}", "sort_title": "Plugins sortieren", "sort_title_icons": "Symbolpakete sortieren", diff --git a/packit/locales/strings_en.json b/packit/locales/strings_en.json index d292e30..a5df9d9 100644 --- a/packit/locales/strings_en.json +++ b/packit/locales/strings_en.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Fetching repository data...", "repo_add_fetch_failed": "Failed to fetch repository data.", "repo_add_disclaimer": "PackIt is not responsible for the content of {0}. All rights and responsibility belong to the owner, {1}.\n\nPlugin count: {2}", - "repo_add_disclaimer_short": "Third-party source — PackIt does not check it.", + "repo_add_disclaimer_short": "PackIt is not responsible for the content of third-party sources.", "repo_add_no_repometa": "Repository has no metadata. Add anyway?\n\nPlugin count: {0}", "sort_title": "Sort Plugins", "sort_title_icons": "Sort Icons", diff --git a/packit/locales/strings_ru.json b/packit/locales/strings_ru.json index 3ecc07c..2f57545 100644 --- a/packit/locales/strings_ru.json +++ b/packit/locales/strings_ru.json @@ -169,7 +169,7 @@ "repo_add_fetching": "Получение данных...", "repo_add_fetch_failed": "Не удалось получить данные репозитория.", "repo_add_disclaimer": "PackIt не отвечает за содержимое репозитория {0}. Все права и ответственность за него несёт владелец, то есть {1}.\n\nКоличество плагинов: {2}", - "repo_add_disclaimer_short": "Сторонний источник — PackIt его не проверяет.", + "repo_add_disclaimer_short": "PackIt не отвечает за содержимое сторонних источников.", "repo_add_no_repometa": "Метаданные репозитория потерялись...( Добавить всё равно?\n\nКоличество плагинов: {0}", "sort_title": "Сортировка плагинов", "sort_title_icons": "Сортировка наборов иконок", diff --git a/packit/meta.yml b/packit/meta.yml index 8027603..750e6cf 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.1.2-dev.26" +version: "0.1.2-dev.27" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" From 1d67307ca85d9b6e603d12be892075d348ef8134 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:34:57 +0000 Subject: [PATCH 38/46] Separate the subtitle from the fine print on the add sheet The maintainer and the disclaimer were both centred grey paragraphs a few dp apart at nearly the same size, so they read as one block of small print rather than as a subtitle attached to the name and a warning attached to the button. The maintainer takes medium weight and a size up; the disclaimer keeps the plain weight and stays the smallest thing on the sheet, which is now what tells them apart. The block above it grows with the room it has: a 76dp picture and a 23dp name, where the old 64 and 21 left the sheet looking emptier than it needed to. The disclaimer gains a dp and a wider gap of its own, having been small enough to skip. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/deeplinks/repo.py | 30 ++++++++++++++++++++---------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 750e6cf..15f1e43 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.1.2-dev.27" +version: "0.1.2-dev.28" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index 2dcca81..f34ed41 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -195,9 +195,9 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): try: from ..ui.ReposActivity.repoIcon import build_icon_view icon_view = build_icon_view( - act, {"id": rm_rid, "name": rm_name, "url": link}, 64, 18, rm_icon) + act, {"id": rm_rid, "name": rm_name, "url": link}, 76, 22, rm_icon) linear.addView(icon_view, LayoutHelper.createLinear( - 64, 64, Gravity.CENTER_HORIZONTAL, 0, 22, 0, 0)) + 76, 76, Gravity.CENTER_HORIZONTAL, 0, 22, 0, 0)) except Exception as e: logx(f"repo deeplink: icon error: {e}", False) @@ -205,7 +205,7 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): # the buttons are for, and the name is the thing being decided about title_tv = TextView(act) title_tv.setGravity(Gravity.CENTER_HORIZONTAL) - title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 21) + title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 23) title_tv.setSingleLine(True) try: from android.text import TextUtils as _TextUtils @@ -220,19 +220,26 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): title_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteBlackText)) linear.addView(title_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 14.0, 21.0, 0.0)) - # maintainer, with the mention live + # Maintainer, with the mention live. Medium weight and a size up on the + # disclaimer: both are centred grey paragraphs a few dp apart, and at + # the same weight the eye read them as one block of small print instead + # of as a subtitle belonging to the name above it. if rm_maintainer: sub_tv = TextView(act) sub_tv.setGravity(Gravity.CENTER_HORIZONTAL) - sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) + sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) sub_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) + try: + sub_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) + except Exception: + pass try: sub_tv.setText(LocaleUtils.fullyFormatText(rm_maintainer)) sub_tv.setLinkTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlueText)) sub_tv.setMovementMethod(LinkMovementMethod.getInstance()) except Exception: sub_tv.setText(rm_maintainer) - linear.addView(sub_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 4.0, 21.0, 0.0)) + linear.addView(sub_tv, LayoutHelper.createFrame(-1, -2, 0, 21.0, 5.0, 21.0, 0.0)) # the facts as pills rather than as a sentence: how much is in there and # where it comes from @@ -251,14 +258,17 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): except Exception as e: logx(f"repo deeplink: chips error: {e}", False) - # what is left of the disclaimer once the concrete facts are drawn above + # What is left of the disclaimer once the concrete facts are drawn + # above. It keeps the plain weight and stays the smallest thing here — + # that is what tells it apart from the subtitle — but it gains a dp and + # a wider gap above, because it was small enough to skip over. msg_tv = TextView(act) msg_tv.setGravity(Gravity.CENTER_HORIZONTAL) - msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 13) + msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) msg_tv.setText(str(strings["repo_add_disclaimer_short"])) msg_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) - msg_tv.setLineSpacing(AndroidUtilities.dp(2), 1.0) - linear.addView(msg_tv, LayoutHelper.createFrame(-1, -2, 0, 24.0, 14.0, 24.0, 0.0)) + msg_tv.setLineSpacing(AndroidUtilities.dp(3), 1.0) + linear.addView(msg_tv, LayoutHelper.createFrame(-1, -2, 0, 24.0, 18.0, 24.0, 0.0)) # add button add_btn = ButtonWithCounterView(act, True, frag.getResourceProvider()) From 69b1c19d11da33feea840e20c4ad2fa8a9d6e492 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 13 Aug 2026 09:39:04 +0000 Subject: [PATCH 39/46] Scale the add sheet's prose and balance its line breaks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sheet's text was in dp, which ignores the font size the user set for their device. Everything on it that is prose — the name, the maintainer, the disclaimer — is in sp now and follows that setting. The pills stay in dp: their height is fixed, so a label that grew would sit in a box that did not. Android also breaks a paragraph greedily, filling the first line to the margin and dropping the remainder on the second, which for a centred two-line sentence stranded one word under a full line. The two paragraphs ask for BALANCED breaking instead, so the lines come out roughly even. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/deeplinks/repo.py | 23 ++++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/packit/meta.yml b/packit/meta.yml index 15f1e43..e74b215 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.1.2-dev.28" +version: "0.1.2-dev.29" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index f34ed41..4d934c0 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -73,6 +73,18 @@ def _sheet_chip_lp(margin_dp=3): return lp +def _balance_lines(tv): + # Android breaks a paragraph greedily by default: it fills the first line to + # the margin and drops whatever is left onto the second, which for a + # centred two-line sentence leaves one word stranded under a full line. + # BALANCED asks the line breaker to even the lines out instead. + try: + from android.text import Layout + tv.setBreakStrategy(Layout.BREAK_STRATEGY_BALANCED) + except Exception as e: + logx(f"repo deeplink: balanced break unavailable: {e}", True) + + def handle(url, repoManager): try: if "repo=add" not in url: @@ -205,7 +217,10 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): # the buttons are for, and the name is the thing being decided about title_tv = TextView(act) title_tv.setGravity(Gravity.CENTER_HORIZONTAL) - title_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 23) + # SP, not DIP: everything written on this sheet is prose, and prose is + # what the system font scale is for. The pills keep DIP — their height + # is fixed, so a label that grew would sit in a box that did not. + title_tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 23) title_tv.setSingleLine(True) try: from android.text import TextUtils as _TextUtils @@ -227,8 +242,9 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): if rm_maintainer: sub_tv = TextView(act) sub_tv.setGravity(Gravity.CENTER_HORIZONTAL) - sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 15) + sub_tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 15) sub_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) + _balance_lines(sub_tv) try: sub_tv.setTypeface(AndroidUtilities.getTypeface("fonts/rmedium.ttf")) except Exception: @@ -264,10 +280,11 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): # a wider gap above, because it was small enough to skip over. msg_tv = TextView(act) msg_tv.setGravity(Gravity.CENTER_HORIZONTAL) - msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 14) + msg_tv.setTextSize(TypedValue.COMPLEX_UNIT_SP, 14) msg_tv.setText(str(strings["repo_add_disclaimer_short"])) msg_tv.setTextColor(sheet.getThemedColor(Theme.key_windowBackgroundWhiteGrayText)) msg_tv.setLineSpacing(AndroidUtilities.dp(3), 1.0) + _balance_lines(msg_tv) linear.addView(msg_tv, LayoutHelper.createFrame(-1, -2, 0, 24.0, 18.0, 24.0, 0.0)) # add button From 18c27190052bbc3a1f8f34813d7b1f07f6c5d200 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 10:53:37 +0000 Subject: [PATCH 40/46] Ask a repository through one module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every screen that wanted a repository's plugin list wrote the same twenty lines: open reposCache/{rm_rid}.json, walk to repomap.plugins, fall back to the stored url, GET it, and unpack a "plugins" value that is an object in some repositories and an array in others. Nine copies of that walk existed and no two were alike. Some used json.loads and choked on the trailing comma the official repomap has shipped with more than once, while others used the lenient parser. Some sent the plugin's User-Agent, most sent python-requests'. The timeout for the same file ranged from 10 to 20 seconds depending on which screen asked. Three places re-implemented "fetch a repomap and validate it", and only one of them had the full table of http reasons the add dialog localises. network/Storage.py is now the only thing that reads a repository. Two layers, and a caller can tell which it is using from the name: read_* comes off disk and is safe anywhere, fetch_* goes to the network and must not run on the ui thread. Between them they cover the repomap and its cache, the fields inside it — maintainer, links, icon, report reasons, suggestion config — the plugin and icon lists, and the avatar bitmaps, which move out of the sources screen's icon view and leave it as the view it was meant to be. Two things fall out of having one implementation. Every request now carries the same User-Agent and the same timeout for the same kind of file, and every repomap is parsed leniently, so a repository that one screen could read is no longer unreadable to another. addRepositoryWithUrl loses its staging dance as well: it downloaded to packitTemp and moved the file into the cache once it validated, which Storage makes unnecessary by validating what it parsed before anything is written. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- .../SecurityBottomSheets/hashBottomSheet.py | 34 +- packit/src/ChatActivity/inline/enterView.py | 69 +--- packit/src/RepositoryManager.py | 206 ++-------- packit/src/deeplinks/install.py | 34 +- packit/src/deeplinks/plugin.py | 20 +- packit/src/deeplinks/repo.py | 49 +-- packit/src/deeplinks/suggestion.py | 18 +- packit/src/deeplinks/update.py | 56 +-- packit/src/network/Storage.py | 389 ++++++++++++++++++ packit/src/network/__init__.py | 4 + packit/src/ui/IconsListActivity/fragment.py | 82 +--- packit/src/ui/PluginListActivity/fragment.py | 72 +--- packit/src/ui/ReposActivity/fragment.py | 21 +- packit/src/ui/ReposActivity/repoIcon.py | 110 +---- packit/src/ui/pluginsUpdates/fragment.py | 98 +---- packit/src/ui/reportDialog.py | 39 +- packit/src/ui/suggest/fragment.py | 85 +--- 18 files changed, 581 insertions(+), 807 deletions(-) create mode 100644 packit/src/network/Storage.py create mode 100644 packit/src/network/__init__.py diff --git a/packit/meta.yml b/packit/meta.yml index e74b215..eff0fd8 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.1.2-dev.29" +version: "0.1.2-dev.30" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py b/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py index aecb99f..5dcc7ad 100644 --- a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py +++ b/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py @@ -62,35 +62,15 @@ def _extractPluginId(filePath: str) -> str | None: return None def _loadCachedRepos() -> list: - import os, json + # [(name, pluginsUrl, repoId), …] for every repository with a usable cache + from ...network import Storage result = [] - try: - from ...utils.paths import getReposCacheDir - cacheDir = getReposCacheDir() - except Exception as e: - if DEBUG_LOGS: - logx(f"hashBottomSheet: _loadCachedRepos error: {e}", False) - return result - - if not os.path.isdir(cacheDir): - return result - - for fname in os.listdir(cacheDir): - if not fname.endswith(".json"): + for rm_rid, cached in Storage.all_cached(): + pluginsUrl = Storage.plugins_url(rm_rid) + if not pluginsUrl: continue - try: - with open(os.path.join(cacheDir, fname), "r", encoding="utf-8") as f: - cached = json.load(f) - pluginsUrl = cached.get("repomap", {}).get("plugins") - if not pluginsUrl: - continue - name = cached.get("repometa", {}).get("rm_name") or fname.replace(".json", "") - repoId = cached.get("repometa", {}).get("rm_rid") or fname.replace(".json", "") - result.append((name, pluginsUrl, repoId)) - except Exception as e: - if DEBUG_LOGS: - logx(f"hashBottomSheet: error reading cache {fname}: {e}", False) - + meta = cached.get("repometa") or {} + result.append((meta.get("rm_name") or rm_rid, pluginsUrl, meta.get("rm_rid") or rm_rid)) return result diff --git a/packit/src/ChatActivity/inline/enterView.py b/packit/src/ChatActivity/inline/enterView.py index 3511dd7..ef11a04 100644 --- a/packit/src/ChatActivity/inline/enterView.py +++ b/packit/src/ChatActivity/inline/enterView.py @@ -5,7 +5,6 @@ import os import json import weakref -import requests from base_plugin import MethodHook from client_utils import get_last_fragment, run_on_queue from hook_utils import find_class, get_private_field @@ -116,14 +115,6 @@ def _flag_match(plugin, flags): return True -def _get_cache_dir(): - from ...utils.paths import getReposCacheDir - return getReposCacheDir() - -def _get_repo_cache_path(repo_id): - from ...utils.paths import getRepoCachePath - return getRepoCachePath(repo_id) - class _PackitAutocompleteHook(MethodHook): def __init__(self, plugin): MethodHook.__init__(self) @@ -250,62 +241,26 @@ def do_search(): def _packit_load_plugins_from_cache(self): + from ...network import Storage plugins_list = [] try: - cache_dir = _get_cache_dir() - if not os.path.exists(cache_dir): - return plugins_list - - repos = self.repoManager.getRepositories() - for repo in repos: + for repo in self.repoManager.getRepositories(): repo_id = repo.get("id") if not repo_id: continue - - cache_path = _get_repo_cache_path(repo_id) - if not os.path.exists(cache_path): + plugins_url = Storage.plugins_url(repo) + if not plugins_url: continue - - try: - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - - plugins_url = cached.get("repomap", {}).get("plugins") - if not plugins_url: - continue - - try: - r = requests.get(plugins_url, timeout=10) - if r.status_code != 200: - continue - config = r.json() - plugins_raw = config.get("plugins", {}) - - if isinstance(plugins_raw, dict): - for pid, info in plugins_raw.items(): - if isinstance(info, dict): - plugins_list.append({ - "id": pid, - "repo_id": repo_id, - "repo_name": repo.get("name", "Unknown"), - **info - }) - elif isinstance(plugins_raw, list): - for item in plugins_raw: - if isinstance(item, dict) and item.get("id"): - plugins_list.append({ - "id": item.get("id"), - "repo_id": repo_id, - "repo_name": repo.get("name", "Unknown"), - **item - }) - except Exception as e: - logx(f"Packit load plugins from url error: {e}", False) - except Exception as e: - logx(f"Packit load repo cache error for {repo_id}: {e}", False) + entries, error = Storage.fetch_plugins(plugins_url) + if error: + logx(f"Packit autocomplete: repo '{repo_id}': {error}", True) + continue + repo_name = repo.get("name", "Unknown") + for entry in entries: + plugins_list.append({"repo_id": repo_id, "repo_name": repo_name, **entry}) except Exception as e: logx(f"Packit load plugins from cache error: {e}", False) - + return plugins_list diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 4bbdcef..6ed9405 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -3,10 +3,8 @@ from packutil import logx from .utils.netQueue import run_serial_io -import os +from .network import Storage import json -from .utils import jsonx as _jsonx -import requests from client_utils import get_last_fragment, run_on_queue try: from elyx import settings, strings @@ -20,8 +18,6 @@ import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() -_HEADERS = {"User-Agent": "PackIt/1.0 (Android; github.com/shareui/packit)"} - OFFICIAL_REPO_URL = "https://raw.githubusercontent.com/shareui/packit/refs/heads/main/configs/repomap.json" # A repository name is a label, not a document. It is drawn on one line in the @@ -44,11 +40,6 @@ def clampRepoName(value) -> str: return text -def _get_cache_dir() -> str: - from .utils.paths import getReposCacheDir - return getReposCacheDir() - - class RepositoryManager: def __init__(self): pass @@ -91,145 +82,39 @@ def setRepositories(self, repos): pass def _fetch_and_save_repomap(self, url: str) -> dict | None: - """Fetch repomap.json from url, save to packit/{rm_rid}.json, return repometa dict.""" - try: - r = requests.get(url, timeout=15, headers=_HEADERS) - if r.status_code != 200: - logx(f"repom: failed to fetch repomap from '{url}': HTTP {r.status_code}", True) - return None - data = _jsonx.loads(r.text) - repometa = data.get("repometa") - if not repometa: - logx(f"repom: no 'repometa' key in response from '{url}'", True) - return None - rm_rid = repometa.get("rm_rid") - if not rm_rid: - logx(f"repom: 'rm_rid' missing in repometa", True) - return None - cache_dir = _get_cache_dir() - os.makedirs(cache_dir, exist_ok=True) - cache_path = os.path.join(cache_dir, f"{rm_rid}.json") - with open(cache_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - logx(f"repom: saved repomap to {cache_path}", True) - return repometa - except Exception as e: - logx(f"repom: _fetch_and_save_repomap error: {e}", False) + """Fetch a repomap, cache it, return its repometa. None if any of that fails.""" + data, error = Storage.fetch_repomap(url) + if error: + logx(f"repom: cannot fetch repomap from '{url}': {error}", True) return None - - def _get_temp_dir(self) -> str: - from .utils.paths import getTempDir - return getTempDir() - - def _cleanup_temp_dir(self): - import shutil - temp_dir = self._get_temp_dir() - try: - if os.path.exists(temp_dir): - shutil.rmtree(temp_dir) - logx("repom: cleaned up packitTemp", True) - except Exception as e: - logx(f"repom: _cleanup_temp_dir error: {e}", False) + repometa = data.get("repometa") + if not Storage.write_repomap(repometa.get("rm_rid"), data): + return None + return repometa def addRepositoryWithUrl(self, url: str): - # download to packitTemp, validate, move to reposCache or discard - # returns (repometa, error_reason) — error_reason is None on success - import shutil - - temp_dir = self._get_temp_dir() - temp_path = os.path.join(temp_dir, "repomap_download.json") - - try: - os.makedirs(temp_dir, exist_ok=True) - logx(f"repom: addRepositoryWithUrl: GET {url}", True) - logx(f"repom: addRepositoryWithUrl: sending headers={_HEADERS}", True) - r = requests.get(url, timeout=15, headers=_HEADERS) - status = r.status_code - logx(f"repom: addRepositoryWithUrl: status={status}", True) - try: - resp_headers = dict(r.headers) - logx(f"repom: addRepositoryWithUrl: resp_headers={resp_headers}", True) - except Exception as ex: - logx(f"repom: addRepositoryWithUrl: could not read resp headers: {ex}", True) - try: - logx(f"repom: addRepositoryWithUrl: body[:500]={r.text[:500]}", True) - except Exception as ex: - logx(f"repom: addRepositoryWithUrl: could not read body: {ex}", True) - if status != 200: - reasons = { - 301: "permanently redirected", - 302: "redirected", - 303: "see other", - 307: "temporarily redirected", - 308: "permanently redirected", - 400: "bad request", - 401: "unauthorized", - 403: "forbidden", - 404: "file not found", - 408: "request timeout", - 410: "resource gone", - 429: "rate limited, try again later", - 451: "unavailable for legal reasons", - 500: "server error", - 502: "bad gateway", - 503: "service unavailable", - 504: "gateway timeout", - } - reason = reasons.get(status, f"HTTP {status}") - self._cleanup_temp_dir() - return None, reason - - with open(temp_path, "w", encoding="utf-8") as f: - f.write(r.text) - logx(f"repom: downloaded to {temp_path}", True) - except Exception as e: - self._cleanup_temp_dir() - return None, str(e) + # returns (repometa, error_reason) — error_reason is None on success. + # + # This used to download into packitTemp and move the file into the cache + # once it validated. Storage validates what it parsed before anything is + # written, so there is nothing to stage: a repomap that fails its checks + # never reaches the disk in the first place. + logx(f"repom: addRepositoryWithUrl: GET {url}", True) + data, error = Storage.fetch_repomap(url) + if error: + logx(f"repom: addRepositoryWithUrl: {error}", True) + return None, error - # validate - try: - with open(temp_path, "r", encoding="utf-8") as f: - data = _jsonx.loads(f.read()) - except Exception as e: - logx(f"repom: addRepositoryWithUrl: json parse error: {e}", False) - self._cleanup_temp_dir() - return None, "invalid json" - - logx(f"repom: addRepositoryWithUrl: parsed ok, keys={list(data.keys())}", True) repometa = data.get("repometa") - if not repometa: - logx("repom: addRepositoryWithUrl: missing repometa", True) - self._cleanup_temp_dir() - return None, "missing repometa" - rm_rid = repometa.get("rm_rid") - logx(f"repom: addRepositoryWithUrl: rm_rid={repr(rm_rid)}", True) - if not rm_rid: - logx("repom: addRepositoryWithUrl: missing rm_rid", True) - self._cleanup_temp_dir() - return None, "missing rm_rid" - rm_name = repometa.get("rm_name") - logx(f"repom: addRepositoryWithUrl: rm_name={repr(rm_name)}", True) + logx(f"repom: addRepositoryWithUrl: rm_rid={repr(rm_rid)} rm_name={repr(rm_name)}", True) if not rm_name: - logx("repom: addRepositoryWithUrl: missing rm_name", True) - self._cleanup_temp_dir() return None, "missing rm_name" - # move to reposCache - try: - cache_dir = _get_cache_dir() - os.makedirs(cache_dir, exist_ok=True) - cache_path = os.path.join(cache_dir, f"{rm_rid}.json") - shutil.move(temp_path, cache_path) - logx(f"repom: moved repomap to {cache_path}", True) - except Exception as e: - logx(f"repom: addRepositoryWithUrl: move to cache error: {e}", False) - self._cleanup_temp_dir() + if not Storage.write_repomap(rm_rid, data): return None, "cache write failed" - self._cleanup_temp_dir() - repos = self.getRepositories() newRepo = { "id": rm_rid, @@ -294,20 +179,10 @@ def removeRepository(self, idx): repo_id = repo.get("id") logx(f"repom.removeRepository: removing idx={idx}, id={repr(repo_id)}, name={repr(repo.get('name'))}", True) - try: - if repo_id: - cache_dir = _get_cache_dir() - cache_path = os.path.join(cache_dir, f"{repo_id}.json") - logx(f"repom.removeRepository: looking for cache at {cache_path}", True) - if os.path.exists(cache_path): - os.remove(cache_path) - logx(f"repom.removeRepository: deleted cache {cache_path}", True) - else: - logx(f"repom.removeRepository: cache not found at {cache_path}", True) - else: - logx("repom.removeRepository: repo has no id, skipping cache delete", True) - except Exception as e: - logx(f"repom.removeRepository: failed to delete cache: {e}", False) + if repo_id: + dropped = Storage.forget_repomap(repo_id) + logx(f"repom.removeRepository: cache for '{repo_id}' " + f"{'deleted' if dropped else 'was not there'}", True) repos.pop(idx) self.setRepositories(repos) @@ -399,8 +274,6 @@ def updateAllCaches(self, on_complete=None): def task(): try: repos = self.getRepositories() - cache_dir = _get_cache_dir() - os.makedirs(cache_dir, exist_ok=True) changed = False to_remove = [] seen_rids = set() @@ -410,20 +283,21 @@ def task(): if not url: continue try: - r = requests.get(url, timeout=10, headers=_HEADERS) - if r.status_code != 200: - logx(f"updateAllCaches: HTTP {r.status_code} for {url}", True) - continue - data = _jsonx.loads(r.text) - repometa = data.get("repometa") - rm_rid = repometa.get("rm_rid") if repometa else None - - # No repometa — remove repo - if not repometa or not rm_rid: - logx(f"updateAllCaches: no repometa for '{url}', removing repo", True) + data, error = Storage.fetch_repomap(url) + if error in ("missing repometa", "missing rm_rid"): + # it answered, and what it answered with is not a + # repository — drop it + logx(f"updateAllCaches: '{url}' is not a repomap ({error}), removing", True) to_remove.append(i) changed = True continue + if error: + # unreachable or unparseable: keep the repository and + # whatever is already cached for it + logx(f"updateAllCaches: {error} for {url}", True) + continue + repometa = data.get("repometa") + rm_rid = repometa.get("rm_rid") # Duplicate check if rm_rid in seen_rids: @@ -448,9 +322,7 @@ def task(): changed = True logx(f"updateAllCaches: restored name '{rm_name}' for '{rm_rid}'", True) - cache_path = os.path.join(cache_dir, f"{rm_rid}.json") - with open(cache_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) + Storage.write_repomap(rm_rid, data) logx(f"updateAllCaches: updated cache for '{rm_rid}'", True) except Exception as e: logx(f"updateAllCaches: error for {url}: {e}", False) diff --git a/packit/src/deeplinks/install.py b/packit/src/deeplinks/install.py index e5a3f05..430c84c 100644 --- a/packit/src/deeplinks/install.py +++ b/packit/src/deeplinks/install.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ..network import Storage from ..utils.bulletins import factory as _pbf from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue @@ -30,11 +31,6 @@ _INSTALL_ALL = _INSTALL_REQUIRED | _INSTALL_OPTIONAL -def _getCachePath(repoId: str) -> str: - from ..utils.paths import getRepoCachePath - return getRepoCachePath(repoId) - - def _findRepo(repoManager, repoId: str) -> dict | None: try: for r in (repoManager.getRepositories() or []): @@ -46,19 +42,7 @@ def _findRepo(repoManager, repoId: str) -> dict | None: def _resolvePluginsUrl(repo: dict) -> str: - repoId = (repo.get("id") or "").strip() - fallback = (repo.get("url") or "").strip() - if not repoId: - return fallback - try: - cachePath = _getCachePath(repoId) - if os.path.exists(cachePath): - with open(cachePath, "r", encoding="utf-8") as f: - cached = json.load(f) - return cached.get("repomap", {}).get("plugins") or fallback - except Exception: - pass - return fallback + return Storage.plugins_url(repo) def handle(url, repoManager): @@ -376,19 +360,7 @@ def _show_loading_bulletin(): def _resolveIconsUrl(repo: dict) -> str: - repoId = (repo.get("id") or "").strip() - fallback = (repo.get("url") or "").strip() - if not repoId: - return fallback - try: - cachePath = _getCachePath(repoId) - if os.path.exists(cachePath): - with open(cachePath, "r", encoding="utf-8") as f: - cached = json.load(f) - return cached.get("repomap", {}).get("icons") or fallback - except Exception: - pass - return fallback + return Storage.icons_url(repo) def _handleInstallIconPack(repo: dict, iconId: str): diff --git a/packit/src/deeplinks/plugin.py b/packit/src/deeplinks/plugin.py index 175bc69..a5075c6 100644 --- a/packit/src/deeplinks/plugin.py +++ b/packit/src/deeplinks/plugin.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ..network import Storage from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -24,25 +25,8 @@ _REQUIRED = {"plugin", "repo"} -def _getCachePath(repoId: str) -> str: - from ..utils.paths import getRepoCachePath - return getRepoCachePath(repoId) - - def _resolvePluginsUrl(repo: dict) -> str: - repoId = (repo.get("id") or "").strip() - fallback = (repo.get("url") or "").strip() - if not repoId: - return fallback - try: - cachePath = _getCachePath(repoId) - if os.path.exists(cachePath): - with open(cachePath, "r", encoding="utf-8") as f: - cached = json.load(f) - return cached.get("repomap", {}).get("plugins") or fallback - except Exception: - pass - return fallback + return Storage.plugins_url(repo) def _findRepo(repoManager, repoId: str) -> dict | None: diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index 4d934c0..884b386 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -35,8 +35,7 @@ from urllib.parse import urlparse, parse_qs import requests import json -from ..utils import jsonx as _jsonx -import os +from ..network import Storage BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") @@ -52,11 +51,6 @@ _REPO_ADD_ALL = _REPO_ADD_REQUIRED | _REPO_ADD_OPTIONAL -def _get_cache_dir() -> str: - from ..utils.paths import getReposCacheDir - return getReposCacheDir() - - def _sheet_chip(act, text: str): # the same pill the source cards use, so the sheet that adds a source and # the card it becomes are recognisably the same thing @@ -129,36 +123,21 @@ def fetch_task(): repometa = None pluginCount = 0 try: - response = requests.get(link, timeout=10) - if response.status_code == 200: - data = _jsonx.loads(response.text) + data, error = Storage.fetch_repomap(link) + if error: + logx(f"repo deeplink: {error} for '{link}'", True) + else: repometa = data.get("repometa") - - if repometa and repometa.get("rm_rid"): - try: - cache_dir = _get_cache_dir() - os.makedirs(cache_dir, exist_ok=True) - cache_path = os.path.join(cache_dir, f"{repometa['rm_rid']}.json") - with open(cache_path, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - except Exception as e: - logx(f"repo deeplink: cache error: {e}", False) - - repomap = data.get("repomap", {}) - plugins_url = repomap.get("plugins") if repomap else None - if plugins_url: - try: - pr = requests.get(plugins_url, timeout=10) - if pr.status_code == 200: - pdata = _jsonx.loads(pr.text) - plugins = pdata.get("plugins", []) - pluginCount = len(plugins) if isinstance(plugins, (list, dict)) else 0 - except Exception as e: - logx(f"repo deeplink: plugins count error: {e}", False) + # cached now, so the sheet's avatar and everything the + # source screen shows are there the moment it is added + Storage.write_repomap(repometa.get("rm_rid"), data) + + plugins_url = Storage.plugins_url(repometa.get("rm_rid"), link) + entries, list_error = Storage.fetch_plugins(plugins_url) + if list_error: + logx(f"repo deeplink: plugin count unavailable: {list_error}", True) else: - plugins = data.get("plugins", []) - if isinstance(plugins, (list, dict)): - pluginCount = len(plugins) + pluginCount = len(entries) except Exception as e: logx(f"repo deeplink: fetch error: {e}", False) diff --git a/packit/src/deeplinks/suggestion.py b/packit/src/deeplinks/suggestion.py index ce0b550..e488f1d 100644 --- a/packit/src/deeplinks/suggestion.py +++ b/packit/src/deeplinks/suggestion.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ..network import Storage from urllib.parse import urlparse, parse_qs from android_utils import run_on_ui_thread from client_utils import get_last_fragment @@ -14,21 +15,8 @@ import os -def _get_cache_path(rm_rid: str) -> str: - from ..utils.paths import getRepoCachePath - return getRepoCachePath(rm_rid) - - -def _load_repomap(rm_rid: str) -> dict | None: - path = _get_cache_path(rm_rid) - if not os.path.exists(path): - return None - try: - with open(path, "r", encoding="utf-8") as f: - return json.load(f) - except Exception as e: - logx(f"suggestion deeplink: _load_repomap error: {e}", False) - return None +def _load_repomap(rm_rid: str): + return Storage.read_repomap(rm_rid) def _has_required_fields(data: dict) -> bool: diff --git a/packit/src/deeplinks/update.py b/packit/src/deeplinks/update.py index 9cf935f..05623a0 100644 --- a/packit/src/deeplinks/update.py +++ b/packit/src/deeplinks/update.py @@ -2,6 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx +from ..network import Storage from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -18,21 +19,13 @@ from urllib.parse import urlparse, parse_qs import requests import json -from ..utils import jsonx as _jsonx import os -def _get_cache_dir() -> str: - from ..utils.paths import getCacheRoot - return getCacheRoot() - - def _run_update(repoManager): def task(): try: repos = repoManager.getRepositories() - cacheDir = _get_cache_dir() - os.makedirs(cacheDir, exist_ok=True) changed = False toRemove = [] seenRids = set() @@ -42,19 +35,17 @@ def task(): if not url: continue try: - r = requests.get(url, timeout=10) - if r.status_code != 200: - logx(f"update deeplink: HTTP {r.status_code} for {url}", True) - continue - data = _jsonx.loads(r.text) - repometa = data.get("repometa") - rmRid = repometa.get("rm_rid") if repometa else None - - if not repometa or not rmRid: - logx(f"update deeplink: no repometa for '{url}', removing repo", True) + data, error = Storage.fetch_repomap(url) + if error in ("missing repometa", "missing rm_rid"): + logx(f"update deeplink: '{url}' is not a repomap ({error}), removing", True) toRemove.append(i) changed = True continue + if error: + logx(f"update deeplink: {error} for {url}", True) + continue + repometa = data.get("repometa") + rmRid = repometa.get("rm_rid") if rmRid in seenRids: logx(f"update deeplink: duplicate rm_rid='{rmRid}', removing repo", True) @@ -68,9 +59,7 @@ def task(): changed = True logx(f"update deeplink: set id='{rmRid}' for repo '{repo.get('name')}'", True) - cachePath = os.path.join(cacheDir, f"{rmRid}.json") - with open(cachePath, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) + Storage.write_repomap(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) except Exception as e: logx(f"update deeplink: error for {url}: {e}", False) @@ -92,8 +81,6 @@ def _run_update_single(repoManager, repoId: str): def task(): try: repos = repoManager.getRepositories() - cacheDir = _get_cache_dir() - os.makedirs(cacheDir, exist_ok=True) target = next((r for r in repos if r.get("id") == repoId), None) if not target: @@ -106,23 +93,18 @@ def task(): return try: - r = requests.get(url, timeout=10) - if r.status_code != 200: - logx(f"update deeplink: HTTP {r.status_code} for {url}", True) - run_on_ui_thread(lambda: BulletinHelper.show_error(str(strings("dl_update_repo_http_error", code=r.status_code)))) - return - data = _jsonx.loads(r.text) - repometa = data.get("repometa") - rmRid = repometa.get("rm_rid") if repometa else None - - if not repometa or not rmRid: + data, error = Storage.fetch_repomap(url) + if error in ("missing repometa", "missing rm_rid"): logx(f"update deeplink: no repometa for '{url}'", True) run_on_ui_thread(lambda: BulletinHelper.show_error(str(strings["dl_update_repo_no_meta"]))) return - - cachePath = os.path.join(cacheDir, f"{rmRid}.json") - with open(cachePath, "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) + if error: + logx(f"update deeplink: {error} for {url}", True) + run_on_ui_thread(lambda e=error: BulletinHelper.show_error(str(strings("dl_update_repo_http_error", code=e)))) + return + repometa = data.get("repometa") + rmRid = repometa.get("rm_rid") + Storage.write_repomap(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) idx = next((i for i, rp in enumerate(repos) if rp.get("id") == repoId), None) diff --git a/packit/src/network/Storage.py b/packit/src/network/Storage.py new file mode 100644 index 0000000..82d8920 --- /dev/null +++ b/packit/src/network/Storage.py @@ -0,0 +1,389 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# One way to ask a repository for anything: its repomap, its plugin list, its +# icon packs, its avatar. +# +# Before this, every screen that wanted a repository's plugin list wrote the +# same twenty lines — open /packit/reposCache/{rm_rid}.json, walk down to +# repomap.plugins, fall back to the stored url, GET it, and unpack a "plugins" +# value that is a dict in some repositories and a list in others. Nine copies of +# that walk existed, no two identical: some used json.loads and choked on a +# repomap with a trailing comma, some sent the plugin's User-Agent and some sent +# python-requests', and the timeouts ranged from 10 to 20 seconds for the same +# file. A repository is one thing and it is read from one place. +# +# Two layers, and callers should know which they are using: +# read_* — off disk, no network, safe to call anywhere +# fetch_* — over the network, must not be called on the ui thread + +from packutil import logx +import json +import os + +import requests + +from ..utils import jsonx as _jsonx +from ..utils.paths import ( + getRepoCachePath, getReposCacheDir, + getRepoIconCachePath, getRepoIconCacheDir, +) + +# repositories are served from github raw and the like; some of them log this +HEADERS = {"User-Agent": "PackIt/1.0 (Android; github.com/shareui/packit)"} + +TIMEOUT = 15 +TIMEOUT_LIST = 20 # plugin and icon lists run to hundreds of kilobytes + + +# ---------------------------------------------------------------- repomap cache + +def repomap_path(rm_rid) -> str: + return getRepoCachePath(str(rm_rid or "")) + + +def read_repomap(rm_rid): + """The cached repomap for a repository, or None. Never touches the network. + + Parsed leniently: the official repomap has shipped with a trailing comma + more than once, and a screen that cannot draw a repository because of one + character is worse than a screen that tolerates it. + """ + rm_rid = str(rm_rid or "") + if not rm_rid: + return None + path = repomap_path(rm_rid) + try: + if not os.path.isfile(path): + return None + with open(path, "r", encoding="utf-8") as f: + data = _jsonx.loads(f.read()) + return data if isinstance(data, dict) else None + except Exception as e: + logx(f"Storage: unreadable repomap cache for '{rm_rid}': {e}", True) + return None + + +def write_repomap(rm_rid, data) -> bool: + rm_rid = str(rm_rid or "") + if not rm_rid or not isinstance(data, dict): + return False + try: + os.makedirs(getReposCacheDir(), exist_ok=True) + with open(repomap_path(rm_rid), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logx(f"Storage: cannot write repomap cache for '{rm_rid}': {e}", False) + return False + + +def forget_repomap(rm_rid) -> bool: + """Drop a repository's cached repomap. True when there was one to drop.""" + rm_rid = str(rm_rid or "") + if not rm_rid: + return False + try: + path = repomap_path(rm_rid) + if not os.path.isfile(path): + return False + os.remove(path) + return True + except Exception as e: + logx(f"Storage: cannot delete repomap cache for '{rm_rid}': {e}", False) + return False + + +def repomap_mtime(rm_rid) -> float: + try: + return os.path.getmtime(repomap_path(rm_rid)) + except Exception: + return 0.0 + + +def all_cached() -> list: + """[(rm_rid, repomap), …] for every repository with a cache on disk.""" + out = [] + try: + cache_dir = getReposCacheDir() + if not os.path.isdir(cache_dir): + return out + for name in sorted(os.listdir(cache_dir)): + if not name.endswith(".json"): + continue + # the installed-plugin index and the counts live in this directory + # too, and neither of them is a repomap + if name.endswith("-index.json") or name.endswith("-stats.json"): + continue + rm_rid = name[:-len(".json")] + data = read_repomap(rm_rid) + if isinstance(data, dict): + out.append((rm_rid, data)) + except Exception as e: + logx(f"Storage: cannot list the repomap cache: {e}", False) + return out + + +# -------------------------------------------------------------- repomap fields + +def _repo_id_of(repo) -> str: + if isinstance(repo, dict): + return str(repo.get("id") or "") + return str(repo or "") + + +def repometa(rm_rid) -> dict: + data = read_repomap(_repo_id_of(rm_rid)) + meta = data.get("repometa") if isinstance(data, dict) else None + return meta if isinstance(meta, dict) else {} + + +def section_url(repo, key: str, fallback: str = "") -> str: + """repomap. out of the cache, falling back to the stored url. + + `repo` is either the stored dict or a bare rm_rid. The fallback matters: + a repomap that is itself the plugin list has no repomap section at all, + and then the repository's own url is the list. + """ + if isinstance(repo, dict) and not fallback: + fallback = str(repo.get("url") or "").strip() + data = read_repomap(_repo_id_of(repo)) + repomap = data.get("repomap") if isinstance(data, dict) else None + if isinstance(repomap, dict): + url = str(repomap.get(key) or "").strip() + if url: + return url + return fallback + + +def plugins_url(repo, fallback: str = "") -> str: + return section_url(repo, "plugins", fallback) + + +def icons_url(repo, fallback: str = "") -> str: + return section_url(repo, "icons", fallback) + + +def icon_url(repo) -> str: + """repometa.rm_icon, but only when it is a picture. + + Repositories written before rm_icon was a link put an R.drawable name here. + Nothing resolves those any more, so anything that is not http(s) is no icon. + """ + url = str(repometa(repo).get("rm_icon") or "").strip() + return url if url.lower().startswith(("http://", "https://")) else "" + + +def reasons(rm_rid) -> list: + data = read_repomap(_repo_id_of(rm_rid)) + block = data.get("reasons") if isinstance(data, dict) else None + items = block.get("reasons") if isinstance(block, dict) else None + if not isinstance(items, list): + return [] + return [str(r) for r in items if r] + + +def report_settings(rm_rid): + """(forum_username, topic_msg_id), or (None, None) when the repo has none.""" + data = read_repomap(_repo_id_of(rm_rid)) + block = data.get("reasons") if isinstance(data, dict) else None + values = block.get("settings") if isinstance(block, dict) else None + if isinstance(values, list) and len(values) >= 2: + try: + return str(values[0]), int(values[1]) + except Exception: + return None, None + return None, None + + +def suggest_config(rm_rid): + data = read_repomap(_repo_id_of(rm_rid)) + block = data.get("suggest_plugins") if isinstance(data, dict) else None + return block if isinstance(block, dict) else None + + +# ------------------------------------------------------------------- shapes + +def normalize_entries(raw) -> list: + """A repository's "plugins"/"icons" value as a list of dicts, each with an id. + + Both shapes are in the wild: an object keyed by id, and an array of objects + that carry their own. Every caller used to unpack this itself, and they did + not agree on what to do with a malformed entry. + """ + out = [] + if isinstance(raw, dict): + for entry_id, info in raw.items(): + if isinstance(info, dict): + out.append({"id": entry_id, **info}) + elif isinstance(raw, list): + for item in raw: + if isinstance(item, dict) and item.get("id"): + out.append(item) + return out + + +# ------------------------------------------------------------------- network + +# The add dialog localises a failure by matching these exact strings, so they +# are the vocabulary of this module and not free text. +_STATUS_REASONS = { + 301: "permanently redirected", + 302: "redirected", + 303: "see other", + 307: "temporarily redirected", + 308: "permanently redirected", + 400: "bad request", + 401: "unauthorized", + 403: "forbidden", + 404: "file not found", + 408: "request timeout", + 410: "resource gone", + 429: "rate limited, try again later", + 451: "unavailable for legal reasons", + 500: "server error", + 502: "bad gateway", + 503: "service unavailable", + 504: "gateway timeout", +} + + +def fetch_json(url: str, timeout: int = TIMEOUT): + """(data, error). error is a lowercase english reason, or None on success.""" + url = str(url or "").strip() + if not url: + return None, "file not found" + try: + r = requests.get(url, timeout=timeout, headers=HEADERS) + except Exception as e: + logx(f"Storage: request failed for '{url}': {e}", False) + return None, str(e) + if r.status_code != 200: + logx(f"Storage: HTTP {r.status_code} for '{url}'", True) + return None, _STATUS_REASONS.get(r.status_code, f"HTTP {r.status_code}") + try: + return _jsonx.loads(r.text), None + except Exception as e: + logx(f"Storage: bad json at '{url}': {e}", True) + return None, "invalid json" + + +def fetch_repomap(url: str, timeout: int = TIMEOUT): + """(repomap, error) — a repomap is only one if it declares who it is.""" + data, error = fetch_json(url, timeout) + if error: + return None, error + meta = data.get("repometa") if isinstance(data, dict) else None + if not isinstance(meta, dict) or not meta: + return None, "missing repometa" + if not meta.get("rm_rid"): + return None, "missing rm_rid" + return data, None + + +def fetch_entries(url: str, key: str, timeout: int = TIMEOUT_LIST): + """(entries, error) for a plugin or icon list.""" + data, error = fetch_json(url, timeout) + if error: + return None, error + raw = data.get(key, []) if isinstance(data, dict) else [] + return normalize_entries(raw), None + + +def fetch_plugins(url: str, timeout: int = TIMEOUT_LIST): + return fetch_entries(url, "plugins", timeout) + + +def fetch_icons(url: str, timeout: int = TIMEOUT_LIST): + return fetch_entries(url, "icons", timeout) + + +# ------------------------------------------------------- repository avatars + +_MEM_CAP = 64 +_mem = None +_mem_lock = None + + +def _mem_store(): + global _mem + if _mem is None: + from collections import OrderedDict + _mem = OrderedDict() + return _mem + + +def _lock(): + global _mem_lock + if _mem_lock is None: + import threading + _mem_lock = threading.Lock() + return _mem_lock + + +def _mem_key(url: str, px: int) -> str: + # px is part of the key: the same icon is decoded at different sizes for a + # card and for a sheet, and the smaller decode looks soft blown up + return f"{url}|{px}" + + +def peek_icon(url: str, px: int): + """The already-decoded avatar, or None. Cheap enough for the ui thread. + + Routing a bitmap that is already in memory through the worker pool costs a + hop out and a hop back, and a card rebuilt in those two frames shows its + monogram — which is what made avatars blink whenever the list repainted. + """ + if not url: + return None + key = _mem_key(url, px) + with _lock(): + store = _mem_store() + bmp = store.get(key) + if bmp is not None: + store.move_to_end(key) + return bmp + + +def load_icon(url: str, px: int): + """memory -> disk -> network, decoded to a px-sized bitmap. Off the ui thread.""" + from ..utils import imagePool + + bmp = peek_icon(url, px) + if bmp is not None: + return bmp + + path = getRepoIconCachePath(url) + data = None + try: + if os.path.isfile(path): + with open(path, "rb") as f: + data = f.read() + except Exception: + data = None + + if not data: + data = imagePool.fetch(url) + if not data: + return None + try: + os.makedirs(getRepoIconCacheDir(), exist_ok=True) + with open(path, "wb") as f: + f.write(data) + except Exception as e: + logx(f"Storage: icon cache write failed: {e}", True) + + bmp = imagePool.decode(data, px, imagePool.looks_like_svg(url, data)) + if bmp is None: + # a corrupted cache entry would keep failing forever + try: + os.unlink(path) + except Exception: + pass + return None + with _lock(): + store = _mem_store() + store[_mem_key(url, px)] = bmp + while len(store) > _MEM_CAP: + store.popitem(last=False) + return bmp diff --git a/packit/src/network/__init__.py b/packit/src/network/__init__.py new file mode 100644 index 0000000..c5d406e --- /dev/null +++ b/packit/src/network/__init__.py @@ -0,0 +1,4 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Everything the plugin asks a repository for. diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 00d2594..6dc31f9 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -685,40 +685,17 @@ def load_task(): continue logx(f"IconList._open_all_repos_icons: loading repo '{repo.get('name')}' id='{repo_id}' url='{repo_url}'", True) try: - icons_url = repo_url - if repo_id: - try: - from org.telegram.messenger import ApplicationLoader - except Exception as e: - import android_utils as _au; _au.log(f"import ApplicationLoader failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() - import os - from ...utils.paths import getRepoCachePath - cache_path = getRepoCachePath(repo_id) - logx(f"IconList._open_all_repos_icons: cache_path='{cache_path}' exists={os.path.exists(cache_path)}", True) - if os.path.exists(cache_path): - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - icons_url = cached.get("repomap", {}).get("icons") or repo_url - logx(f"IconList._open_all_repos_icons: resolved icons_url from cache='{icons_url}'", True) - + from ...network import Storage + icons_url = Storage.icons_url(repo, repo_url) logx(f"IconList._open_all_repos_icons: fetching icons from '{icons_url}'", True) - response = requests.get(icons_url, timeout=10) - logx(f"IconList._open_all_repos_icons: repo '{repo.get('name')}' HTTP {response.status_code}", True) - if response.status_code != 200: - logx(f"IconList._open_all_repos_icons: repo '{repo.get('name')}': HTTP {response.status_code}, skipping", True) + entries, error = Storage.fetch_icons(icons_url) + if error: + logx(f"IconList._open_all_repos_icons: repo '{repo.get('name')}': {error}, skipping", True) continue - config = response.json() - icons = config.get("icons", {}) - logx(f"IconList._open_all_repos_icons: repo '{repo.get('name')}' icons type={type(icons).__name__} len={len(icons) if icons else 0}", True) - if isinstance(icons, dict): - for iconId, info in icons.items(): - if isinstance(info, dict): - all_icons.append({"id": iconId, "repo_name": repo.get("name", "Unknown"), **info}) - elif isinstance(icons, list): - for item in icons: - if isinstance(item, dict) and item.get("id"): - all_icons.append({"id": item.get("id"), "repo_name": repo.get("name", "Unknown"), **item}) + logx(f"IconList._open_all_repos_icons: repo '{repo.get('name')}' icons={len(entries)}", True) + repo_name = repo.get("name", "Unknown") + for entry in entries: + all_icons.append({"repo_name": repo_name, **entry}) except Exception as e: logx(f"IconList._open_all_repos_icons: failed to load repo '{repo.get('name')}': {e}", False) @@ -762,43 +739,12 @@ def _open_repo_icons(self, repo): def load_task(): try: - icons_url = repo_url - if repo_id: - try: - from org.telegram.messenger import ApplicationLoader - except Exception as e: - import android_utils as _au; _au.log(f"import ApplicationLoader failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() - import os - from ...utils.paths import getRepoCachePath - cache_path = getRepoCachePath(repo_id) - logx(f"IconList._open_repo_icons: cache_path='{cache_path}' exists={os.path.exists(cache_path)}", True) - if os.path.exists(cache_path): - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - icons_url = cached.get("repomap", {}).get("icons") or repo_url - logx(f"IconList._open_repo_icons: resolved icons_url from cache='{icons_url}'", True) - + from ...network import Storage + icons_url = Storage.icons_url(repo_id, repo_url) logx(f"IconList._open_repo_icons: fetching '{icons_url}'", True) - r = requests.get(icons_url, timeout=20) - logx(f"IconList._open_repo_icons: HTTP {r.status_code}", True) - if r.status_code != 200: - raise Exception(f"HTTP {r.status_code}") - config = r.json() - icons_raw = config.get("icons", []) - logx(f"IconList._open_repo_icons: icons_raw type={type(icons_raw).__name__} len={len(icons_raw) if icons_raw else 0}", True) - icons = [] - if isinstance(icons_raw, dict): - for iid, info in icons_raw.items(): - if isinstance(info, dict): - icons.append({"id": iid, **info}) - elif isinstance(icons_raw, list): - for item in icons_raw: - if isinstance(item, dict) and item.get("id"): - icons.append(item) - else: - logx(f"IconList._open_repo_icons: skipping list item (no id or not dict): {item}", True) - + icons, error = Storage.fetch_icons(icons_url) + if error: + raise Exception(error) logx(f"IconList._open_repo_icons: parsed icons count={len(icons)}", True) # left behind for the sources screen: repomap points at this # file by url and carries no count of its own diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index 896103f..5dfc1ff 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -18,7 +18,6 @@ from java import dynamic_proxy import os from hook_utils import find_class -import requests from android_utils import run_on_ui_thread from client_utils import get_last_fragment, run_on_queue from ui.bulletin import BulletinHelper @@ -246,69 +245,26 @@ def load_task(): repo_url = (repo.get("url") or "").strip() if not repo_url: continue try: - plugins_url = repo_url - if r_id: - try: - from org.telegram.messenger import ApplicationLoader - except Exception as e: - pass - import os - from ...utils.paths import getRepoCachePath - cache_path = getRepoCachePath(r_id) - if os.path.exists(cache_path): - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - resolved = cached.get("repomap", {}).get("plugins") or repo_url - plugins_url = resolved - - response = requests.get(plugins_url, timeout=10) - if response.status_code != 200: continue - config = response.json() - plugins = config.get("plugins", {}) - if isinstance(plugins, dict): - for pluginId, info in plugins.items(): - if isinstance(info, dict): - all_plugins.append({"id": pluginId, "repo_name": repo.get("name", "Unknown"), "_repo_id": r_id, **info}) - elif isinstance(plugins, list): - for item in plugins: - if isinstance(item, dict) and item.get("id"): - all_plugins.append({"id": item.get("id"), "repo_name": repo.get("name", "Unknown"), "_repo_id": r_id, **item}) + from ...network import Storage + entries, error = Storage.fetch_plugins( + Storage.plugins_url(repo, repo_url)) + if error: + continue + repo_name = repo.get("name", "Unknown") + for entry in entries: + all_plugins.append( + {"repo_name": repo_name, "_repo_id": r_id, **entry}) except Exception as e: - pass + logx(f"InstallUI: repo '{r_id}' load failed: {e}", True) run_on_ui_thread(lambda: self._update_current_fragment_plugins(all_plugins)) else: repos = self.repoManager.getRepositories() repo = next((r for r in repos if r.get("id") == repo_id), None) if not repo: return - repo_url = (repo.get("url") or "").strip() - plugins_url = repo_url - try: - from org.telegram.messenger import ApplicationLoader - except Exception as e: - pass - import os - from ...utils.paths import getRepoCachePath - cache_path = getRepoCachePath(repo_id) - if os.path.exists(cache_path): - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - resolved = cached.get("repomap", {}).get("plugins") or repo_url - plugins_url = resolved - - r = requests.get(plugins_url, timeout=20) - if r.status_code != 200: - raise Exception(f"HTTP {r.status_code}") - config = r.json() - plugins_raw = config.get("plugins", []) - plugins = [] - if isinstance(plugins_raw, dict): - for pid, info in plugins_raw.items(): - if isinstance(info, dict): - plugins.append({"id": pid, **info}) - elif isinstance(plugins_raw, list): - for item in plugins_raw: - if isinstance(item, dict) and item.get("id"): - plugins.append(item) + from ...network import Storage + plugins, error = Storage.fetch_plugins(Storage.plugins_url(repo)) + if error: + raise Exception(error) # the sources screen has no other way to know how big a # source is: repomap only points at this file by url try: diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index 479c02b..a92a55d 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -45,7 +45,7 @@ from . import register, unregister from .card import make_repo_card from ..viewUtils import applyFontToTree -from ...utils.paths import getRepoCachePath +from ...network import Storage def _c(color: int) -> int: @@ -85,28 +85,17 @@ def read_repo_info(repo: dict) -> dict: except Exception as e: logx(f"repos: stats unavailable for '{repo_id}': {e}", True) - path = getRepoCachePath(repo_id) - try: - if not os.path.isfile(path): - return info - with open(path, "r", encoding="utf-8") as f: - cached = json.load(f) - except Exception as e: - logx(f"repos: cache unreadable for '{repo_id}': {e}", True) + cached = Storage.read_repomap(repo_id) + if not cached: return info meta = cached.get("repometa") or {} info["maintainer"] = str(meta.get("rm_maintainer") or "") info["telegram"] = str(meta.get("rm_telegram") or "") info["source"] = str(meta.get("rm_source") or "") - icon_url = str(meta.get("rm_icon") or "").strip() - # older repositories put an R.drawable name in rm_icon; only a link is an icon - info["icon_url"] = icon_url if icon_url.lower().startswith(("http://", "https://")) else "" + info["icon_url"] = Storage.icon_url(repo_id) info["status"] = "loaded" - try: - info["updated_at"] = os.path.getmtime(path) - except Exception: - info["updated_at"] = 0.0 + info["updated_at"] = Storage.repomap_mtime(repo_id) # a repomap that is itself the plugin list carries the count inline; the # usual shape only points at it by url, and that count comes from repoStats diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index fde54c5..e2952e4 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -1,12 +1,12 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -# Repository avatar. +# Repository avatar — the view only. Where the picture comes from, and the +# memory and disk caches it comes through, are network/Storage's business. # # repomap declares the icon as a plain image url (repometa.rm_icon), so there is -# nothing to look up in R.drawable any more — the file is downloaded once, kept -# on disk and in memory, and drawn over a monogram that stands in until it -# arrives (and stays for repositories that declare no icon at all). +# nothing to look up in R.drawable any more: the bitmap is drawn over a monogram +# that stands in until it arrives, and stays for repositories that declare none. # # The view is a FrameLayout of two layers, monogram below and bitmap above, # because a Drawable subclass would have to be proxied into java just to paint @@ -15,7 +15,6 @@ from packutil import logx import ctypes -from collections import OrderedDict from android.widget import FrameLayout, TextView, ImageView from android.view import Gravity @@ -32,11 +31,7 @@ Theme = None from ...utils import imagePool -from ...utils.paths import getRepoIconCachePath, getRepoIconCacheDir, getRepoCachePath - -_MEM_CAP = 64 -_mem = OrderedDict() -_mem_lock = None +from ...network import Storage def _c(color: int) -> int: # java setColor(int) rejects python ints >= 0x80000000 @@ -65,14 +60,6 @@ def tonal(accent: int, surface: int, fraction: float) -> int: return _c(out) -def _lock(): - global _mem_lock - if _mem_lock is None: - import threading - _mem_lock = threading.Lock() - return _mem_lock - - def _seed(repo: dict) -> int: key = str(repo.get("id") or repo.get("url") or repo.get("name") or "") total = 0 @@ -106,88 +93,7 @@ def _letter(repo: dict) -> str: def icon_url_for(repo: dict): - # rm_icon out of the cached repomap; anything that is not an http(s) link is - # ignored — older repositories put an R.drawable name there - try: - repo_id = str(repo.get("id") or "") - if not repo_id: - return None - import json - import os - path = getRepoCachePath(repo_id) - if not os.path.isfile(path): - return None - with open(path, "r", encoding="utf-8") as f: - cached = json.load(f) - url = str((cached.get("repometa") or {}).get("rm_icon") or "").strip() - return url if url.lower().startswith(("http://", "https://")) else None - except Exception as e: - logx(f"repoIcon: icon_url_for error: {e}", True) - return None - - -def peek_bitmap(url: str, px: int): - # The already-decoded answer, or None. Card rebuilds go through here first: - # routing a known bitmap through the worker pool costs a hop to the pool and - # back to the ui thread, and in those two frames the card shows its - # monogram — which is what made an avatar blink every time the list was - # rebuilt after a toggle. - if not url: - return None - key = _mem_key(url, px) - with _lock(): - bmp = _mem.get(key) - if bmp is not None: - _mem.move_to_end(key) - return bmp - - -def _mem_key(url: str, px: int) -> str: - # px is part of the key: the same icon is decoded at different sizes for the - # card and for the deeplink sheet, and the smaller decode looks soft blown up - return f"{url}|{px}" - - -def _load_bitmap(url: str, px: int): - # memory -> disk -> network, decoded to a px-sized bitmap - bmp = peek_bitmap(url, px) - if bmp is not None: - return bmp - - import os - path = getRepoIconCachePath(url) - data = None - try: - if os.path.isfile(path): - with open(path, "rb") as f: - data = f.read() - except Exception: - data = None - - if not data: - data = imagePool.fetch(url) - if not data: - return None - try: - os.makedirs(getRepoIconCacheDir(), exist_ok=True) - with open(path, "wb") as f: - f.write(data) - except Exception as e: - logx(f"repoIcon: cache write failed: {e}", True) - - bmp = imagePool.decode(data, px, imagePool.looks_like_svg(url, data)) - if bmp is None: - # a corrupted cache entry would keep failing forever - try: - os.unlink(path) - except Exception: - pass - return None - with _lock(): - _mem[_mem_key(url, px)] = bmp - while len(_mem) > _MEM_CAP: - _mem.popitem(last=False) - return bmp + return Storage.icon_url(repo) or None def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url=None): @@ -240,7 +146,7 @@ def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url # is an answer, unlike None, so no worker goes and reads it again return holder - cached = peek_bitmap(str(url or ""), size_px) + cached = Storage.peek_icon(str(url or ""), size_px) if cached is not None: # straight onto the view, no fade: the icon was already on screen a # moment ago and fading it back in is exactly what reads as a blink @@ -256,7 +162,7 @@ def _task(): target = url if url else icon_url_for(repo) if not target: return - bmp = _load_bitmap(target, size_px) + bmp = Storage.load_icon(target, size_px) if bmp is None: return diff --git a/packit/src/ui/pluginsUpdates/fragment.py b/packit/src/ui/pluginsUpdates/fragment.py index b59615c..4e4e4f4 100644 --- a/packit/src/ui/pluginsUpdates/fragment.py +++ b/packit/src/ui/pluginsUpdates/fragment.py @@ -51,11 +51,6 @@ def _get_index_path(pkg: str, rm_rid: str) -> str: return getRepoIndexPath(rm_rid) -def _get_repo_cache_path(pkg: str, rm_rid: str) -> str: - from ...utils.paths import getRepoCachePath - return getRepoCachePath(rm_rid) - - def _get_repos() -> list: try: raw = settings.get("repositories", "[]") @@ -82,42 +77,18 @@ def _read_index(pkg: str, rm_rid: str) -> list: def _get_repo_plugins_url(pkg: str, rm_rid: str, fallback_url: str) -> str: - # resolves plugins url from cached repomap, falls back to repo url - cache_path = _get_repo_cache_path(pkg, rm_rid) - if os.path.exists(cache_path): - try: - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - resolved = cached.get("repomap", {}).get("plugins") - if resolved: - return resolved - except Exception as e: - logx(f"pluginsUpdates: _get_repo_plugins_url error for '{rm_rid}': {e}", False) - return fallback_url + from ...network import Storage + return Storage.plugins_url(rm_rid, fallback_url) def _fetch_repo_plugins(url: str) -> dict: - # returns dict: plugin_id plugin_info - try: - r = requests.get(url, timeout=20, headers={"User-Agent": "PackIt/1.0"}) - if r.status_code != 200: - logx(f"pluginsUpdates: HTTP {r.status_code} for {url}", True) - return {} - config = r.json() - raw = config.get("plugins", {}) - result = {} - if isinstance(raw, dict): - for pid, info in raw.items(): - if isinstance(info, dict): - result[pid] = info - elif isinstance(raw, list): - for item in raw: - if isinstance(item, dict) and item.get("id"): - result[item["id"]] = item - return result - except Exception as e: - logx(f"pluginsUpdates: _fetch_repo_plugins error for '{url}': {e}", False) + # {plugin_id: plugin_info} — this screen looks plugins up by id + from ...network import Storage + entries, error = Storage.fetch_plugins(url) + if error: + logx(f"pluginsUpdates: {error} for {url}", True) return {} + return {entry["id"]: entry for entry in entries if entry.get("id")} def _version_tuple(v: str): @@ -1564,40 +1535,22 @@ def set_btn_state(state: str): def task(): try: - from ...deeplinks.install import _resolvePluginsUrl from ...core import install_plugin - import requests as _requests + from ...network import Storage - plugins_url = _resolvePluginsUrl(repo) + plugins_url = Storage.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) return - r = _requests.get(plugins_url, timeout=20, headers={"User-Agent": "PackIt/1.0"}) - if r.status_code != 200: - logx(f"pluginsUpdates: _install_update HTTP {r.status_code}", True) + all_plugins, error = Storage.fetch_plugins(plugins_url) + if error: + logx(f"pluginsUpdates: _install_update {error}", True) run_on_ui_thread(lambda: set_btn_state("idle")) return - data = r.json() - plugins_raw = data.get("plugins", {}) - - plugin = None - all_plugins = [] - if isinstance(plugins_raw, dict): - for _pid, info in plugins_raw.items(): - if isinstance(info, dict): - all_plugins.append({"id": _pid, **info}) - info = plugins_raw.get(pid) - if isinstance(info, dict): - plugin = {"id": pid, **info} - elif isinstance(plugins_raw, list): - all_plugins = [p for p in plugins_raw if isinstance(p, dict)] - for p in plugins_raw: - if isinstance(p, dict) and p.get("id") == pid: - plugin = p - break + plugin = next((p for p in all_plugins if p.get("id") == pid), None) if not plugin: logx(f"pluginsUpdates: _install_update plugin '{pid}' not found in repo", True) @@ -2057,13 +2010,13 @@ def set_btn_state(state: str): def task(): try: - from ...deeplinks.install import _resolvePluginsUrl from ...core import install_plugin_silent from ...utils.paths import getPluginsDir + from ...network import Storage import requests as _requests import os as _os - plugins_url = _resolvePluginsUrl(repo) + plugins_url = Storage.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update_silent no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) @@ -2071,26 +2024,15 @@ def task(): on_done() return - r = _requests.get(plugins_url, timeout=20, headers={"User-Agent": "PackIt/1.0"}) - if r.status_code != 200: - logx(f"pluginsUpdates: _install_update_silent plugins list HTTP {r.status_code} for '{pid}'", True) + entries, error = Storage.fetch_plugins(plugins_url) + if error: + logx(f"pluginsUpdates: _install_update_silent {error} for '{pid}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) if on_done: on_done() return - data = r.json() - plugins_raw = data.get("plugins", {}) - plugin = None - if isinstance(plugins_raw, dict): - info = plugins_raw.get(pid) - if isinstance(info, dict): - plugin = {"id": pid, **info} - elif isinstance(plugins_raw, list): - for p in plugins_raw: - if isinstance(p, dict) and p.get("id") == pid: - plugin = p - break + plugin = next((p for p in entries if p.get("id") == pid), None) if not plugin: logx(f"pluginsUpdates: _install_update_silent plugin '{pid}' not found in repo", True) diff --git a/packit/src/ui/reportDialog.py b/packit/src/ui/reportDialog.py index ff48ebf..64fe649 100644 --- a/packit/src/ui/reportDialog.py +++ b/packit/src/ui/reportDialog.py @@ -122,43 +122,14 @@ def onAnimationRepeat(self, a, *args): pass def _load_reasons(repo_id: str) -> list: - # loads reasons from cached repomap for given repo_id - if not repo_id: - return [] - try: - from ..utils.paths import getRepoCachePath - cache_path = getRepoCachePath(repo_id) - if not os.path.exists(cache_path): - return [] - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - reasons = cached.get("reasons", {}).get("reasons", []) - if isinstance(reasons, list): - return [str(r) for r in reasons if r] - return [] - except Exception as e: - logx(f"reportDialog: _load_reasons error: {e}", False) - return [] + from ..network import Storage + return Storage.reasons(repo_id) def _load_report_settings(repo_id: str): - # returns (forum_username, topic_msg_id) or (None, None) - if not repo_id: - return None, None - try: - from ..utils.paths import getRepoCachePath - cache_path = getRepoCachePath(repo_id) - if not os.path.exists(cache_path): - return None, None - with open(cache_path, "r", encoding="utf-8") as f: - cached = json.load(f) - settings = cached.get("reasons", {}).get("settings", []) - if isinstance(settings, list) and len(settings) >= 2: - return str(settings[0]), int(settings[1]) - return None, None - except Exception as e: - logx(f"reportDialog: _load_report_settings error: {e}", False) - return None, None + # (forum_username, topic_msg_id), or (None, None) + from ..network import Storage + return Storage.report_settings(repo_id) def _submit_report(forum_username: str, topic_msg_id: int, plugin_name: str, plugin_id: str, repo_id: str, reason: str, description: str, act, on_done): diff --git a/packit/src/ui/suggest/fragment.py b/packit/src/ui/suggest/fragment.py index 4d5f224..0090136 100644 --- a/packit/src/ui/suggest/fragment.py +++ b/packit/src/ui/suggest/fragment.py @@ -986,39 +986,23 @@ def _worker(): pass repometa = None try: - from ...utils.paths import getRepoCachePath - import json as _json - - # suggest_config may carry rm_rid indirectly; scan all caches - # find by checking suggest_config origin — passed as is, so use paths util - cache_dir_path = getRepoCachePath("") - import os as _os - cache_dir = _os.path.dirname(cache_dir_path) - for fname in (_os.listdir(cache_dir) if _os.path.isdir(cache_dir) else []): - if not fname.endswith(".json"): - continue - fpath = _os.path.join(cache_dir, fname) + from ...network import Storage + + # which repository carries this plugin is not recorded here, so + # every cached one is asked in turn + for rm_rid, _cached in Storage.all_cached(): try: - with open(fpath, "r", encoding="utf-8") as f: - cached = _json.load(f) - repomap = cached.get("repomap", {}) - plugins_url = repomap.get("plugins", "") + plugins_url = Storage.plugins_url(rm_rid) if not plugins_url: continue - import requests as _req - r = _req.get(plugins_url, timeout=10) - if r.status_code != 200: + entries, error = Storage.fetch_plugins(plugins_url) + if error: continue - data = r.json() - plugins = data.get("plugins", {}) repo_plugin = None - if isinstance(plugins, dict): - repo_plugin = plugins.get(plugin_id) - elif isinstance(plugins, list): - for item in plugins: - if isinstance(item, dict) and item.get("id") == plugin_id: - repo_plugin = item - break + for item in entries: + if item.get("id") == plugin_id: + repo_plugin = item + break if repo_plugin is not None: repo_version = repo_plugin.get("version", "?") meta_version = meta.get("version", "?") @@ -1055,30 +1039,12 @@ def _load_forked_plugins(repo_data: dict) -> list: rm_rid = repometa.get("rm_rid") if isinstance(repometa, dict) else None if not rm_rid: return [] - from ...utils.paths import getRepoCachePath - path = getRepoCachePath(rm_rid) - if not _os.path.exists(path): - return [] - with open(path, "r", encoding="utf-8") as f: - cached = _json.load(f) - plugins_url = cached.get("repomap", {}).get("plugins", "") + from ...network import Storage + plugins_url = Storage.plugins_url(rm_rid) if not plugins_url: return [] - r = _req.get(plugins_url, timeout=10) - if r.status_code != 200: - return [] - data = r.json() - plugins_raw = data.get("plugins", []) - plugins = [] - if isinstance(plugins_raw, dict): - for pid, info in plugins_raw.items(): - if isinstance(info, dict): - plugins.append({"id": pid, **info}) - elif isinstance(plugins_raw, list): - for item in plugins_raw: - if isinstance(item, dict) and item.get("id"): - plugins.append(item) - return plugins + plugins, error = Storage.fetch_plugins(plugins_url) + return [] if error else plugins except Exception as e: logx(f"suggest: _load_forked_plugins error: {e}", False) return [] @@ -1524,20 +1490,13 @@ def onFragmentCreate(self, *_): rm_rid = repometa.get("rm_rid") self._rm_rid = rm_rid or "default" if rm_rid: - import json, os - from ...utils.paths import getRepoCachePath - path = getRepoCachePath(rm_rid) - if os.path.exists(path): - with open(path, "r", encoding="utf-8") as f: - data = json.load(f) - sp = data.get("suggest_plugins") - if isinstance(sp, dict): - self._suggest_config = sp - logx(f"suggest: loaded suggest_plugins for {rm_rid}", True) - else: - logx(f"suggest: suggest_plugins missing in cache for {rm_rid}", True) + from ...network import Storage + sp = Storage.suggest_config(rm_rid) + if sp is not None: + self._suggest_config = sp + logx(f"suggest: loaded suggest_plugins for {rm_rid}", True) else: - logx(f"suggest: cache file not found for {rm_rid}", True) + logx(f"suggest: no suggest_plugins cached for {rm_rid}", True) else: sp = self._repo_data.get("suggest_plugins") if isinstance(self._repo_data, dict) else None if isinstance(sp, dict): From e183caabb5573e25e4f638c57019014c12f7b0d2 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:05:10 +0000 Subject: [PATCH 41/46] Split the repomap cache out into utils/cachedRepos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storage had grown two jobs. Reading reposCache/{rm_rid}.json has nothing to do with the network — it is the file every screen consults before deciding whether it needs the network at all — so it moves to utils/cachedRepos: the file itself (read, write, forget, mtime, all_cached) and everything read out of it (repometa, the plugin and icon list urls, the avatar url, report reasons, suggestion config). network/Storage keeps what actually goes out: headers, timeouts, the http reason table, fetch_json/repomap/plugins/icons, the shape normaliser, and the avatar bitmaps — which live in a different cache directory and stay with the code that downloads them. Neither module imports the other. One more copy turned up while moving: hashBottomSheet fetched a plugin list itself and walked it as a list only, so in a repository that keys its plugins by id it matched nothing at all. Both of its fetches go through Storage now and it handles either shape. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- .../SecurityBottomSheets/hashBottomSheet.py | 34 ++- packit/src/ChatActivity/inline/enterView.py | 3 +- packit/src/RepositoryManager.py | 9 +- packit/src/deeplinks/install.py | 6 +- packit/src/deeplinks/plugin.py | 4 +- packit/src/deeplinks/repo.py | 5 +- packit/src/deeplinks/suggestion.py | 4 +- packit/src/deeplinks/update.py | 5 +- packit/src/network/Storage.py | 195 ++---------------- packit/src/ui/IconsListActivity/fragment.py | 6 +- packit/src/ui/PluginListActivity/fragment.py | 6 +- packit/src/ui/ReposActivity/fragment.py | 8 +- packit/src/ui/ReposActivity/repoIcon.py | 3 +- packit/src/ui/pluginsUpdates/fragment.py | 10 +- packit/src/ui/reportDialog.py | 8 +- packit/src/ui/suggest/fragment.py | 12 +- packit/src/utils/cachedRepos.py | 185 +++++++++++++++++ 18 files changed, 264 insertions(+), 241 deletions(-) create mode 100644 packit/src/utils/cachedRepos.py diff --git a/packit/meta.yml b/packit/meta.yml index eff0fd8..acdb814 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.1.2-dev.30" +version: "0.1.2-dev.31" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py b/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py index 5dcc7ad..f49e45e 100644 --- a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py +++ b/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py @@ -63,10 +63,10 @@ def _extractPluginId(filePath: str) -> str | None: def _loadCachedRepos() -> list: # [(name, pluginsUrl, repoId), …] for every repository with a usable cache - from ...network import Storage + from ...utils import cachedRepos result = [] - for rm_rid, cached in Storage.all_cached(): - pluginsUrl = Storage.plugins_url(rm_rid) + for rm_rid, cached in cachedRepos.all_cached(): + pluginsUrl = cachedRepos.plugins_url(rm_rid) if not pluginsUrl: continue meta = cached.get("repometa") or {} @@ -75,16 +75,15 @@ def _loadCachedRepos() -> list: def _getRepoPluginInfo(pluginId: str, pluginsUrl: str) -> dict | None: - import requests - r = requests.get(pluginsUrl, timeout=10) - if r.status_code != 200: + # this walked r.json()["plugins"] as a list, so it found nothing at all in a + # repository that keys its plugins by id — Storage answers in one shape + from ...network import Storage + entries, error = Storage.fetch_plugins(pluginsUrl) + if error: if DEBUG_LOGS: - logx(f"hashBottomSheet: HTTP {r.status_code} for {pluginsUrl}", True) + logx(f"hashBottomSheet: {error} for {pluginsUrl}", True) return None - for plugin in r.json().get("plugins", []): - if plugin.get("id") == pluginId: - return plugin - return None + return next((p for p in entries if p.get("id") == pluginId), None) def _installFromRepo(pluginId: str, pluginsUrl: str, repoManager, act): @@ -117,17 +116,14 @@ def action(): def task(): try: - r = requests.get(pluginsUrl, timeout=15) - if r.status_code != 200: + from ...network import Storage + entries, error = Storage.fetch_plugins(pluginsUrl) + if error: dismissDlg() - run_on_ui_thread(lambda: BulletinHelper.show_error(strings("sec_repo_load_failed", code=r.status_code))) + run_on_ui_thread(lambda e=error: BulletinHelper.show_error(strings("sec_repo_load_failed", code=e))) return - plugin = None - for item in r.json().get("plugins", []): - if isinstance(item, dict) and item.get("id") == pluginId: - plugin = item - break + plugin = next((p for p in entries if p.get("id") == pluginId), None) if not plugin: dismissDlg() diff --git a/packit/src/ChatActivity/inline/enterView.py b/packit/src/ChatActivity/inline/enterView.py index ef11a04..2a1418c 100644 --- a/packit/src/ChatActivity/inline/enterView.py +++ b/packit/src/ChatActivity/inline/enterView.py @@ -242,13 +242,14 @@ def do_search(): def _packit_load_plugins_from_cache(self): from ...network import Storage + from ...utils import cachedRepos plugins_list = [] try: for repo in self.repoManager.getRepositories(): repo_id = repo.get("id") if not repo_id: continue - plugins_url = Storage.plugins_url(repo) + plugins_url = cachedRepos.plugins_url(repo) if not plugins_url: continue entries, error = Storage.fetch_plugins(plugins_url) diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 6ed9405..4cebd7b 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -4,6 +4,7 @@ from packutil import logx from .utils.netQueue import run_serial_io from .network import Storage +from .utils import cachedRepos import json from client_utils import get_last_fragment, run_on_queue try: @@ -88,7 +89,7 @@ def _fetch_and_save_repomap(self, url: str) -> dict | None: logx(f"repom: cannot fetch repomap from '{url}': {error}", True) return None repometa = data.get("repometa") - if not Storage.write_repomap(repometa.get("rm_rid"), data): + if not cachedRepos.write(repometa.get("rm_rid"), data): return None return repometa @@ -112,7 +113,7 @@ def addRepositoryWithUrl(self, url: str): if not rm_name: return None, "missing rm_name" - if not Storage.write_repomap(rm_rid, data): + if not cachedRepos.write(rm_rid, data): return None, "cache write failed" repos = self.getRepositories() @@ -180,7 +181,7 @@ def removeRepository(self, idx): logx(f"repom.removeRepository: removing idx={idx}, id={repr(repo_id)}, name={repr(repo.get('name'))}", True) if repo_id: - dropped = Storage.forget_repomap(repo_id) + dropped = cachedRepos.forget(repo_id) logx(f"repom.removeRepository: cache for '{repo_id}' " f"{'deleted' if dropped else 'was not there'}", True) @@ -322,7 +323,7 @@ def task(): changed = True logx(f"updateAllCaches: restored name '{rm_name}' for '{rm_rid}'", True) - Storage.write_repomap(rm_rid, data) + cachedRepos.write(rm_rid, data) logx(f"updateAllCaches: updated cache for '{rm_rid}'", True) except Exception as e: logx(f"updateAllCaches: error for {url}: {e}", False) diff --git a/packit/src/deeplinks/install.py b/packit/src/deeplinks/install.py index 430c84c..ecb113b 100644 --- a/packit/src/deeplinks/install.py +++ b/packit/src/deeplinks/install.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..network import Storage +from ..utils import cachedRepos from ..utils.bulletins import factory as _pbf from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue @@ -42,7 +42,7 @@ def _findRepo(repoManager, repoId: str) -> dict | None: def _resolvePluginsUrl(repo: dict) -> str: - return Storage.plugins_url(repo) + return cachedRepos.plugins_url(repo) def handle(url, repoManager): @@ -360,7 +360,7 @@ def _show_loading_bulletin(): def _resolveIconsUrl(repo: dict) -> str: - return Storage.icons_url(repo) + return cachedRepos.icons_url(repo) def _handleInstallIconPack(repo: dict, iconId: str): diff --git a/packit/src/deeplinks/plugin.py b/packit/src/deeplinks/plugin.py index a5075c6..878a6fe 100644 --- a/packit/src/deeplinks/plugin.py +++ b/packit/src/deeplinks/plugin.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..network import Storage +from ..utils import cachedRepos from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -26,7 +26,7 @@ def _resolvePluginsUrl(repo: dict) -> str: - return Storage.plugins_url(repo) + return cachedRepos.plugins_url(repo) def _findRepo(repoManager, repoId: str) -> dict | None: diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/repo.py index 884b386..15092d6 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/repo.py @@ -36,6 +36,7 @@ import requests import json from ..network import Storage +from ..utils import cachedRepos BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") @@ -130,9 +131,9 @@ def fetch_task(): repometa = data.get("repometa") # cached now, so the sheet's avatar and everything the # source screen shows are there the moment it is added - Storage.write_repomap(repometa.get("rm_rid"), data) + cachedRepos.write(repometa.get("rm_rid"), data) - plugins_url = Storage.plugins_url(repometa.get("rm_rid"), link) + plugins_url = cachedRepos.plugins_url(repometa.get("rm_rid"), link) entries, list_error = Storage.fetch_plugins(plugins_url) if list_error: logx(f"repo deeplink: plugin count unavailable: {list_error}", True) diff --git a/packit/src/deeplinks/suggestion.py b/packit/src/deeplinks/suggestion.py index e488f1d..0d25dfc 100644 --- a/packit/src/deeplinks/suggestion.py +++ b/packit/src/deeplinks/suggestion.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..network import Storage +from ..utils import cachedRepos from urllib.parse import urlparse, parse_qs from android_utils import run_on_ui_thread from client_utils import get_last_fragment @@ -16,7 +16,7 @@ def _load_repomap(rm_rid: str): - return Storage.read_repomap(rm_rid) + return cachedRepos.read(rm_rid) def _has_required_fields(data: dict) -> bool: diff --git a/packit/src/deeplinks/update.py b/packit/src/deeplinks/update.py index 05623a0..b986002 100644 --- a/packit/src/deeplinks/update.py +++ b/packit/src/deeplinks/update.py @@ -3,6 +3,7 @@ from packutil import logx from ..network import Storage +from ..utils import cachedRepos from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -59,7 +60,7 @@ def task(): changed = True logx(f"update deeplink: set id='{rmRid}' for repo '{repo.get('name')}'", True) - Storage.write_repomap(rmRid, data) + cachedRepos.write(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) except Exception as e: logx(f"update deeplink: error for {url}: {e}", False) @@ -104,7 +105,7 @@ def task(): return repometa = data.get("repometa") rmRid = repometa.get("rm_rid") - Storage.write_repomap(rmRid, data) + cachedRepos.write(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) idx = next((i for i, rp in enumerate(repos) if rp.get("id") == repoId), None) diff --git a/packit/src/network/Storage.py b/packit/src/network/Storage.py index 82d8920..4c2746f 100644 --- a/packit/src/network/Storage.py +++ b/packit/src/network/Storage.py @@ -1,21 +1,19 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -# One way to ask a repository for anything: its repomap, its plugin list, its -# icon packs, its avatar. +# Everything the plugin fetches from a repository over the network: its repomap, +# its plugin list, its icon list, its avatar. # -# Before this, every screen that wanted a repository's plugin list wrote the -# same twenty lines — open /packit/reposCache/{rm_rid}.json, walk down to -# repomap.plugins, fall back to the stored url, GET it, and unpack a "plugins" -# value that is a dict in some repositories and a list in others. Nine copies of -# that walk existed, no two identical: some used json.loads and choked on a -# repomap with a trailing comma, some sent the plugin's User-Agent and some sent -# python-requests', and the timeouts ranged from 10 to 20 seconds for the same -# file. A repository is one thing and it is read from one place. +# Every screen that wanted a plugin list used to GET it itself and unpack a +# "plugins" value that is an object in some repositories and an array in +# others. Nine copies of that existed, no two identical: the User-Agent was +# sent by one of them, the timeout for the same file ranged from 10 to 20 +# seconds, and only one of the three places that fetched a repomap had the full +# table of http reasons the add dialog localises. A repository is asked once, +# from here. # -# Two layers, and callers should know which they are using: -# read_* — off disk, no network, safe to call anywhere -# fetch_* — over the network, must not be called on the ui thread +# Where a fetched repomap is kept, and everything read back out of it, is +# utils/cachedRepos — this module does not touch that file. from packutil import logx import json @@ -24,10 +22,7 @@ import requests from ..utils import jsonx as _jsonx -from ..utils.paths import ( - getRepoCachePath, getReposCacheDir, - getRepoIconCachePath, getRepoIconCacheDir, -) +from ..utils.paths import getRepoIconCachePath, getRepoIconCacheDir # repositories are served from github raw and the like; some of them log this HEADERS = {"User-Agent": "PackIt/1.0 (Android; github.com/shareui/packit)"} @@ -36,172 +31,6 @@ TIMEOUT_LIST = 20 # plugin and icon lists run to hundreds of kilobytes -# ---------------------------------------------------------------- repomap cache - -def repomap_path(rm_rid) -> str: - return getRepoCachePath(str(rm_rid or "")) - - -def read_repomap(rm_rid): - """The cached repomap for a repository, or None. Never touches the network. - - Parsed leniently: the official repomap has shipped with a trailing comma - more than once, and a screen that cannot draw a repository because of one - character is worse than a screen that tolerates it. - """ - rm_rid = str(rm_rid or "") - if not rm_rid: - return None - path = repomap_path(rm_rid) - try: - if not os.path.isfile(path): - return None - with open(path, "r", encoding="utf-8") as f: - data = _jsonx.loads(f.read()) - return data if isinstance(data, dict) else None - except Exception as e: - logx(f"Storage: unreadable repomap cache for '{rm_rid}': {e}", True) - return None - - -def write_repomap(rm_rid, data) -> bool: - rm_rid = str(rm_rid or "") - if not rm_rid or not isinstance(data, dict): - return False - try: - os.makedirs(getReposCacheDir(), exist_ok=True) - with open(repomap_path(rm_rid), "w", encoding="utf-8") as f: - json.dump(data, f, indent=2, ensure_ascii=False) - return True - except Exception as e: - logx(f"Storage: cannot write repomap cache for '{rm_rid}': {e}", False) - return False - - -def forget_repomap(rm_rid) -> bool: - """Drop a repository's cached repomap. True when there was one to drop.""" - rm_rid = str(rm_rid or "") - if not rm_rid: - return False - try: - path = repomap_path(rm_rid) - if not os.path.isfile(path): - return False - os.remove(path) - return True - except Exception as e: - logx(f"Storage: cannot delete repomap cache for '{rm_rid}': {e}", False) - return False - - -def repomap_mtime(rm_rid) -> float: - try: - return os.path.getmtime(repomap_path(rm_rid)) - except Exception: - return 0.0 - - -def all_cached() -> list: - """[(rm_rid, repomap), …] for every repository with a cache on disk.""" - out = [] - try: - cache_dir = getReposCacheDir() - if not os.path.isdir(cache_dir): - return out - for name in sorted(os.listdir(cache_dir)): - if not name.endswith(".json"): - continue - # the installed-plugin index and the counts live in this directory - # too, and neither of them is a repomap - if name.endswith("-index.json") or name.endswith("-stats.json"): - continue - rm_rid = name[:-len(".json")] - data = read_repomap(rm_rid) - if isinstance(data, dict): - out.append((rm_rid, data)) - except Exception as e: - logx(f"Storage: cannot list the repomap cache: {e}", False) - return out - - -# -------------------------------------------------------------- repomap fields - -def _repo_id_of(repo) -> str: - if isinstance(repo, dict): - return str(repo.get("id") or "") - return str(repo or "") - - -def repometa(rm_rid) -> dict: - data = read_repomap(_repo_id_of(rm_rid)) - meta = data.get("repometa") if isinstance(data, dict) else None - return meta if isinstance(meta, dict) else {} - - -def section_url(repo, key: str, fallback: str = "") -> str: - """repomap. out of the cache, falling back to the stored url. - - `repo` is either the stored dict or a bare rm_rid. The fallback matters: - a repomap that is itself the plugin list has no repomap section at all, - and then the repository's own url is the list. - """ - if isinstance(repo, dict) and not fallback: - fallback = str(repo.get("url") or "").strip() - data = read_repomap(_repo_id_of(repo)) - repomap = data.get("repomap") if isinstance(data, dict) else None - if isinstance(repomap, dict): - url = str(repomap.get(key) or "").strip() - if url: - return url - return fallback - - -def plugins_url(repo, fallback: str = "") -> str: - return section_url(repo, "plugins", fallback) - - -def icons_url(repo, fallback: str = "") -> str: - return section_url(repo, "icons", fallback) - - -def icon_url(repo) -> str: - """repometa.rm_icon, but only when it is a picture. - - Repositories written before rm_icon was a link put an R.drawable name here. - Nothing resolves those any more, so anything that is not http(s) is no icon. - """ - url = str(repometa(repo).get("rm_icon") or "").strip() - return url if url.lower().startswith(("http://", "https://")) else "" - - -def reasons(rm_rid) -> list: - data = read_repomap(_repo_id_of(rm_rid)) - block = data.get("reasons") if isinstance(data, dict) else None - items = block.get("reasons") if isinstance(block, dict) else None - if not isinstance(items, list): - return [] - return [str(r) for r in items if r] - - -def report_settings(rm_rid): - """(forum_username, topic_msg_id), or (None, None) when the repo has none.""" - data = read_repomap(_repo_id_of(rm_rid)) - block = data.get("reasons") if isinstance(data, dict) else None - values = block.get("settings") if isinstance(block, dict) else None - if isinstance(values, list) and len(values) >= 2: - try: - return str(values[0]), int(values[1]) - except Exception: - return None, None - return None, None - - -def suggest_config(rm_rid): - data = read_repomap(_repo_id_of(rm_rid)) - block = data.get("suggest_plugins") if isinstance(data, dict) else None - return block if isinstance(block, dict) else None - - # ------------------------------------------------------------------- shapes def normalize_entries(raw) -> list: diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/IconsListActivity/fragment.py index 6dc31f9..9040d8f 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/IconsListActivity/fragment.py @@ -686,7 +686,8 @@ def load_task(): logx(f"IconList._open_all_repos_icons: loading repo '{repo.get('name')}' id='{repo_id}' url='{repo_url}'", True) try: from ...network import Storage - icons_url = Storage.icons_url(repo, repo_url) + from ...utils import cachedRepos + icons_url = cachedRepos.icons_url(repo, repo_url) logx(f"IconList._open_all_repos_icons: fetching icons from '{icons_url}'", True) entries, error = Storage.fetch_icons(icons_url) if error: @@ -740,7 +741,8 @@ def _open_repo_icons(self, repo): def load_task(): try: from ...network import Storage - icons_url = Storage.icons_url(repo_id, repo_url) + from ...utils import cachedRepos + icons_url = cachedRepos.icons_url(repo_id, repo_url) logx(f"IconList._open_repo_icons: fetching '{icons_url}'", True) icons, error = Storage.fetch_icons(icons_url) if error: diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/PluginListActivity/fragment.py index 5dfc1ff..179b571 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/PluginListActivity/fragment.py @@ -246,8 +246,9 @@ def load_task(): if not repo_url: continue try: from ...network import Storage + from ...utils import cachedRepos entries, error = Storage.fetch_plugins( - Storage.plugins_url(repo, repo_url)) + cachedRepos.plugins_url(repo, repo_url)) if error: continue repo_name = repo.get("name", "Unknown") @@ -262,7 +263,8 @@ def load_task(): repo = next((r for r in repos if r.get("id") == repo_id), None) if not repo: return from ...network import Storage - plugins, error = Storage.fetch_plugins(Storage.plugins_url(repo)) + from ...utils import cachedRepos + plugins, error = Storage.fetch_plugins(cachedRepos.plugins_url(repo)) if error: raise Exception(error) # the sources screen has no other way to know how big a diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/ReposActivity/fragment.py index a92a55d..1d8006c 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/ReposActivity/fragment.py @@ -45,7 +45,7 @@ from . import register, unregister from .card import make_repo_card from ..viewUtils import applyFontToTree -from ...network import Storage +from ...utils import cachedRepos def _c(color: int) -> int: @@ -85,7 +85,7 @@ def read_repo_info(repo: dict) -> dict: except Exception as e: logx(f"repos: stats unavailable for '{repo_id}': {e}", True) - cached = Storage.read_repomap(repo_id) + cached = cachedRepos.read(repo_id) if not cached: return info @@ -93,9 +93,9 @@ def read_repo_info(repo: dict) -> dict: info["maintainer"] = str(meta.get("rm_maintainer") or "") info["telegram"] = str(meta.get("rm_telegram") or "") info["source"] = str(meta.get("rm_source") or "") - info["icon_url"] = Storage.icon_url(repo_id) + info["icon_url"] = cachedRepos.icon_url(repo_id) info["status"] = "loaded" - info["updated_at"] = Storage.repomap_mtime(repo_id) + info["updated_at"] = cachedRepos.mtime(repo_id) # a repomap that is itself the plugin list carries the count inline; the # usual shape only points at it by url, and that count comes from repoStats diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/ReposActivity/repoIcon.py index e2952e4..cd29211 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/ReposActivity/repoIcon.py @@ -32,6 +32,7 @@ from ...utils import imagePool from ...network import Storage +from ...utils import cachedRepos def _c(color: int) -> int: # java setColor(int) rejects python ints >= 0x80000000 @@ -93,7 +94,7 @@ def _letter(repo: dict) -> str: def icon_url_for(repo: dict): - return Storage.icon_url(repo) or None + return cachedRepos.icon_url(repo) or None def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url=None): diff --git a/packit/src/ui/pluginsUpdates/fragment.py b/packit/src/ui/pluginsUpdates/fragment.py index 4e4e4f4..b390984 100644 --- a/packit/src/ui/pluginsUpdates/fragment.py +++ b/packit/src/ui/pluginsUpdates/fragment.py @@ -77,8 +77,8 @@ def _read_index(pkg: str, rm_rid: str) -> list: def _get_repo_plugins_url(pkg: str, rm_rid: str, fallback_url: str) -> str: - from ...network import Storage - return Storage.plugins_url(rm_rid, fallback_url) + from ...utils import cachedRepos + return cachedRepos.plugins_url(rm_rid, fallback_url) def _fetch_repo_plugins(url: str) -> dict: @@ -1537,8 +1537,9 @@ def task(): try: from ...core import install_plugin from ...network import Storage + from ...utils import cachedRepos - plugins_url = Storage.plugins_url(repo) + plugins_url = cachedRepos.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) @@ -2013,10 +2014,11 @@ def task(): from ...core import install_plugin_silent from ...utils.paths import getPluginsDir from ...network import Storage + from ...utils import cachedRepos import requests as _requests import os as _os - plugins_url = Storage.plugins_url(repo) + plugins_url = cachedRepos.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update_silent no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) diff --git a/packit/src/ui/reportDialog.py b/packit/src/ui/reportDialog.py index 64fe649..108f4c8 100644 --- a/packit/src/ui/reportDialog.py +++ b/packit/src/ui/reportDialog.py @@ -122,14 +122,14 @@ def onAnimationRepeat(self, a, *args): pass def _load_reasons(repo_id: str) -> list: - from ..network import Storage - return Storage.reasons(repo_id) + from ..utils import cachedRepos + return cachedRepos.reasons(repo_id) def _load_report_settings(repo_id: str): # (forum_username, topic_msg_id), or (None, None) - from ..network import Storage - return Storage.report_settings(repo_id) + from ..utils import cachedRepos + return cachedRepos.report_settings(repo_id) def _submit_report(forum_username: str, topic_msg_id: int, plugin_name: str, plugin_id: str, repo_id: str, reason: str, description: str, act, on_done): diff --git a/packit/src/ui/suggest/fragment.py b/packit/src/ui/suggest/fragment.py index 0090136..a7908a2 100644 --- a/packit/src/ui/suggest/fragment.py +++ b/packit/src/ui/suggest/fragment.py @@ -987,12 +987,13 @@ def _worker(): repometa = None try: from ...network import Storage + from ...utils import cachedRepos # which repository carries this plugin is not recorded here, so # every cached one is asked in turn - for rm_rid, _cached in Storage.all_cached(): + for rm_rid, _cached in cachedRepos.all_cached(): try: - plugins_url = Storage.plugins_url(rm_rid) + plugins_url = cachedRepos.plugins_url(rm_rid) if not plugins_url: continue entries, error = Storage.fetch_plugins(plugins_url) @@ -1040,7 +1041,8 @@ def _load_forked_plugins(repo_data: dict) -> list: if not rm_rid: return [] from ...network import Storage - plugins_url = Storage.plugins_url(rm_rid) + from ...utils import cachedRepos + plugins_url = cachedRepos.plugins_url(rm_rid) if not plugins_url: return [] plugins, error = Storage.fetch_plugins(plugins_url) @@ -1490,8 +1492,8 @@ def onFragmentCreate(self, *_): rm_rid = repometa.get("rm_rid") self._rm_rid = rm_rid or "default" if rm_rid: - from ...network import Storage - sp = Storage.suggest_config(rm_rid) + from ...utils import cachedRepos + sp = cachedRepos.suggest_config(rm_rid) if sp is not None: self._suggest_config = sp logx(f"suggest: loaded suggest_plugins for {rm_rid}", True) diff --git a/packit/src/utils/cachedRepos.py b/packit/src/utils/cachedRepos.py new file mode 100644 index 0000000..47f226b --- /dev/null +++ b/packit/src/utils/cachedRepos.py @@ -0,0 +1,185 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# /packit/reposCache/{rm_rid}.json — the repomap a repository last +# served, and everything read out of it. +# +# The file is written whenever a repomap is fetched and read by nearly every +# screen: the plugin catalogue and the icon catalogue resolve their list urls +# through it, the sources screen draws a card from it, the report dialog takes +# its reasons from it, autocomplete and the deeplinks all start here. Reading it +# is the single most copied piece of code in the plugin, so it lives in one +# place and nowhere else. +# +# Nothing here touches the network. What fills the file is network/Storage. + +from packutil import logx +import json +import os + +from . import jsonx as _jsonx +from .paths import getRepoCachePath, getReposCacheDir + + +def path(rm_rid) -> str: + return getRepoCachePath(str(rm_rid or "")) + + +def read(rm_rid): + """The cached repomap for a repository, or None. + + Parsed leniently: the official repomap has shipped with a trailing comma + more than once, and a screen that cannot draw a repository because of one + character is worse than a screen that tolerates it. + """ + rm_rid = str(rm_rid or "") + if not rm_rid: + return None + file_path = path(rm_rid) + try: + if not os.path.isfile(file_path): + return None + with open(file_path, "r", encoding="utf-8") as f: + data = _jsonx.loads(f.read()) + return data if isinstance(data, dict) else None + except Exception as e: + logx(f"cachedRepos: unreadable cache for '{rm_rid}': {e}", True) + return None + + +def write(rm_rid, data) -> bool: + rm_rid = str(rm_rid or "") + if not rm_rid or not isinstance(data, dict): + return False + try: + os.makedirs(getReposCacheDir(), exist_ok=True) + with open(path(rm_rid), "w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + return True + except Exception as e: + logx(f"cachedRepos: cannot write cache for '{rm_rid}': {e}", False) + return False + + +def forget(rm_rid) -> bool: + """Drop a repository's cached repomap. True when there was one to drop.""" + rm_rid = str(rm_rid or "") + if not rm_rid: + return False + try: + file_path = path(rm_rid) + if not os.path.isfile(file_path): + return False + os.remove(file_path) + return True + except Exception as e: + logx(f"cachedRepos: cannot delete cache for '{rm_rid}': {e}", False) + return False + + +def mtime(rm_rid) -> float: + try: + return os.path.getmtime(path(rm_rid)) + except Exception: + return 0.0 + + +def all_cached() -> list: + """[(rm_rid, repomap), …] for every repository with a cache on disk.""" + out = [] + try: + cache_dir = getReposCacheDir() + if not os.path.isdir(cache_dir): + return out + for name in sorted(os.listdir(cache_dir)): + if not name.endswith(".json"): + continue + # the installed-plugin index and the counts live in this directory + # too, and neither of them is a repomap + if name.endswith("-index.json") or name.endswith("-stats.json"): + continue + rm_rid = name[:-len(".json")] + data = read(rm_rid) + if isinstance(data, dict): + out.append((rm_rid, data)) + except Exception as e: + logx(f"cachedRepos: cannot list the cache: {e}", False) + return out + + +# ----------------------------------------------------------- what is inside + +def _repo_id_of(repo) -> str: + if isinstance(repo, dict): + return str(repo.get("id") or "") + return str(repo or "") + + +def repometa(repo) -> dict: + data = read(_repo_id_of(repo)) + meta = data.get("repometa") if isinstance(data, dict) else None + return meta if isinstance(meta, dict) else {} + + +def section_url(repo, key: str, fallback: str = "") -> str: + """repomap. out of the cache, falling back to the stored url. + + `repo` is either the stored dict or a bare rm_rid. The fallback matters: + a repomap that is itself the plugin list has no repomap section at all, + and then the repository's own url is the list. + """ + if isinstance(repo, dict) and not fallback: + fallback = str(repo.get("url") or "").strip() + data = read(_repo_id_of(repo)) + repomap = data.get("repomap") if isinstance(data, dict) else None + if isinstance(repomap, dict): + url = str(repomap.get(key) or "").strip() + if url: + return url + return fallback + + +def plugins_url(repo, fallback: str = "") -> str: + return section_url(repo, "plugins", fallback) + + +def icons_url(repo, fallback: str = "") -> str: + return section_url(repo, "icons", fallback) + + +def icon_url(repo) -> str: + """repometa.rm_icon, but only when it is a picture. + + Repositories written before rm_icon was a link put an R.drawable name here. + Nothing resolves those any more, so anything that is not http(s) is no icon. + """ + url = str(repometa(repo).get("rm_icon") or "").strip() + return url if url.lower().startswith(("http://", "https://")) else "" + + +def reasons(repo) -> list: + data = read(_repo_id_of(repo)) + block = data.get("reasons") if isinstance(data, dict) else None + items = block.get("reasons") if isinstance(block, dict) else None + if not isinstance(items, list): + return [] + return [str(r) for r in items if r] + + +def report_settings(repo): + """(forum_username, topic_msg_id), or (None, None) when the repo has none.""" + data = read(_repo_id_of(repo)) + block = data.get("reasons") if isinstance(data, dict) else None + values = block.get("settings") if isinstance(block, dict) else None + if isinstance(values, list) and len(values) >= 2: + try: + return str(values[0]), int(values[1]) + except Exception: + return None, None + return None, None + + +def suggest_config(repo): + data = read(_repo_id_of(repo)) + block = data.get("suggest_plugins") if isinstance(data, dict) else None + return block if isinstance(block, dict) else None From c03e8eb588b5b19b77ce0e45f9590c87c6dea31f Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:13:34 +0000 Subject: [PATCH 42/46] Lowercase every package, PascalCase every module MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 171 files move. Directories go to lowercase, which is what PEP 8 asks of a package and what the import lines have to spell out on every use; modules go to PascalCase. BasePlugin.py keeps its name — refmap.yml and the builder's compilationIgnore both point at that path — and __init__.py keeps its, being Python's. Imports were rewritten against the syntax tree rather than by search-and-replace, which matters for the thirty-five modules that are imported by name: `from . import cachedRepos` has to become `from . import CachedRepos` and take its every use with it, without touching a local variable that happens to share the name. Where the new name was already spoken for in the file it is aliased instead — BasePlugin.py imports the module as `main` because `Main` is the class the loader looks for. Eight relative imports turned out to have been broken all along, with one dot too many or too few, each inside an except-ImportError fallback that would have thrown a second ImportError had it ever run. The rename surfaced them and they are corrected. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/BasePlugin.py | 4 +- .../SecurityBottomSheets/__init__.py | 5 -- packit/src/{core.py => Core.py} | 32 +++---- packit/src/{dexLoader.py => DexLoader.py} | 2 +- packit/src/{main.py => Main.py} | 84 ++++++++--------- packit/src/MainActivity.py | 38 ++++---- .../src/{nativeLoader.py => NativeLoader.py} | 4 +- packit/src/RepositoryManager.py | 18 ++-- .../afpFile.py => chatactivity/AfpFile.py} | 6 +- .../ConfirmImportBottomSheet.py | 6 +- .../ImportBottomSheet.py | 2 +- .../__init__.py | 0 .../export/DecryptorBottomSheet.py | 12 +-- .../export/ImportBottomSheet.py | 2 +- .../export/__init__.py | 0 .../export/bin/Reader.py} | 4 +- .../export/bin/Writer.py} | 8 +- .../export/bin/__init__.py | 0 .../inline/EnterView.py} | 28 +++--- .../inline/InlineBtns.py} | 16 ++-- .../inline/InlineState.py} | 0 .../inline/MessageBuilder.py} | 2 +- .../inline/__init__.py | 0 .../linksicons/LinksBottomSheet.py} | 0 .../linksicons}/__init__.py | 2 +- .../securitybottomsheets/HashBottomSheet.py} | 10 +-- .../SignaturesBottomSheet.py} | 4 +- .../securitybottomsheets/__init__.py | 5 ++ .../{contributors.py => Contributors.py} | 0 .../{deepHandler.py => DeepHandler.py} | 72 +++++++-------- .../{deeplinkMenu.py => DeeplinkMenu.py} | 0 packit/src/deeplinks/{docs.py => Docs.py} | 0 packit/src/deeplinks/{forum.py => Forum.py} | 2 +- .../src/deeplinks/{install.py => Install.py} | 22 ++--- .../deeplinks/{mainMenu.py => MainMenu.py} | 2 +- packit/src/deeplinks/{other.py => Other.py} | 0 packit/src/deeplinks/{pkill.py => Pkill.py} | 2 +- packit/src/deeplinks/{plugin.py => Plugin.py} | 8 +- .../deeplinks/{problems.py => Problems.py} | 2 +- packit/src/deeplinks/{repo.py => Repo.py} | 24 ++--- .../deeplinks/{settings.py => Settings.py} | 4 +- .../{suggestion.py => Suggestion.py} | 6 +- packit/src/deeplinks/{update.py => Update.py} | 10 +-- packit/src/deeplinks/__init__.py | 2 +- .../deeplinks/secret/{aytist.py => Aytist.py} | 2 +- .../secret/{premium.py => Premium.py} | 4 +- .../secret/{terraria.py => Terraria.py} | 4 +- .../btnCAB.py => dialogsactivity/BtnCAB.py} | 4 +- .../BtnPluginsMenu.py} | 2 +- .../BuildNotCorrect.py} | 6 +- .../button.py => dialogsactivity/Button.py} | 12 +-- .../ChatDialogButton.py} | 4 +- .../PackitUpdateSheet.py | 6 +- .../PillWidget.py} | 4 +- .../UpdatesWidget.py} | 14 +-- .../__init__.py | 0 packit/src/network/Storage.py | 12 +-- packit/src/other/{badges.py => Badges.py} | 10 +-- .../src/other/{chatBadge.py => ChatBadge.py} | 0 .../{chatTitleIcon.py => ChatTitleIcon.py} | 0 packit/src/other/{everyone.py => Everyone.py} | 2 +- packit/src/other/{isBeta.py => IsBeta.py} | 4 +- ...rofileTitleIcon.py => ProfileTitleIcon.py} | 0 packit/src/other/{text.py => Text.py} | 2 +- packit/src/scl/{doc.py => Doc.py} | 6 +- packit/src/scl/{errors.py => Errors.py} | 0 packit/src/scl/{native.py => Native.py} | 4 +- packit/src/scl/{opts.py => Opts.py} | 2 +- packit/src/scl/{scl.py => Scl.py} | 10 +-- packit/src/scl/{value.py => Value.py} | 2 +- packit/src/scl/__init__.py | 10 +-- .../DebugItems.py} | 10 +-- .../Deeplinks.py} | 4 +- .../docs.py => settingsactivity/Docs.py} | 22 ++--- .../Profile.py} | 14 +-- .../Settings.py} | 42 ++++----- .../Utilities.py} | 4 +- .../__init__.py | 0 .../service/AddKeyDialog.py | 0 .../service/FastExpandableHook.py} | 0 .../service/PluginsExport.py} | 8 +- .../service}/__init__.py | 0 .../subsettings/Apikeys.py} | 16 ++-- .../subsettings/Comps.py} | 0 .../subsettings/Debug.py} | 6 +- .../subsettings/FileSettings.py} | 0 .../subsettings/Hotkeys.py} | 0 .../subsettings/Inline.py} | 4 +- .../subsettings/Interface.py} | 0 .../subsettings/Misc.py} | 0 .../subsettings}/PluginCardEditor.py | 4 +- .../subsettings/PluginProfile.py} | 0 .../subsettings/Sfx.py} | 8 +- .../subsettings/Updplugins.py} | 0 .../subsettings}/__init__.py | 0 .../AddIconsFab.py} | 2 +- .../AddPluginFab.py} | 2 +- .../InstallDismissHook.py | 4 +- .../SettingsActivityHook.py} | 0 .../UniversalFragmentFix.py} | 0 .../__init__.py | 0 .../src/ui/{contextMenu.py => ContextMenu.py} | 0 packit/src/ui/DeeplinkBottomSheets.py | 12 +-- packit/src/ui/ExportBottomSheet.py | 6 +- packit/src/ui/FontPickerBottomSheet.py | 4 +- packit/src/ui/{md3Slider.py => Md3Slider.py} | 0 packit/src/ui/NoInternetBanner.py | 2 +- .../ui/{reportDialog.py => ReportDialog.py} | 12 +-- .../ui/{restartDialog.py => RestartDialog.py} | 6 +- packit/src/ui/{viewUtils.py => ViewUtils.py} | 0 .../Fragment.py} | 6 +- .../__init__.py | 0 .../service/AchivementsEngine.py | 14 +-- .../service/__init__.py | 0 .../contributors/{fragment.py => Fragment.py} | 2 +- .../fragment.py => filesactivity/Fragment.py} | 8 +- .../InfoDialog.py} | 6 +- .../OpenFileFragment.py} | 12 +-- .../Packlight.py} | 2 +- .../__init__.py | 0 .../Fragment.py} | 42 ++++----- .../RepoBottomSheet.py | 10 +-- .../SortBottomSheet.py | 8 +- .../__init__.py | 0 .../Fragment.py} | 48 +++++----- .../VersionPicker.py} | 12 +-- .../__init__.py | 0 .../card.py => pluginlistactivity/Card.py} | 42 ++++----- .../Fragment.py} | 90 +++++++++---------- .../ListView.py} | 14 +-- .../__init__.py | 0 .../filter/FilterDrawer.py} | 6 +- .../filter/FilterEngine.py} | 0 .../filter/TagLayoutListener.py} | 0 .../filter/__init__.py | 0 .../helpers/PluginActions.py | 20 ++--- .../helpers/ReportService.py | 6 +- .../helpers/UiHelpers.py} | 0 .../helpers/Utils.py} | 2 +- .../helpers/__init__.py | 0 .../sheets/AISearchSheet.py | 14 +-- .../sheets/DepsSheet.py} | 8 +- .../sheets/RepoBottomSheet.py | 10 +-- .../sheets/SortBottomSheet.py | 10 +-- .../sheets/TgChannelSheet.py} | 6 +- .../sheets/__init__.py | 0 .../ClearIgnoreListDialog.py} | 4 +- .../Fragment.py} | 56 ++++++------ .../HideAllDialog.py} | 2 +- .../HideDialog.py} | 2 +- .../StartupSheet.py} | 12 +-- .../__init__.py | 0 .../actions.py => reposactivity/Actions.py} | 8 +- .../addSheet.py => reposactivity/AddSheet.py} | 4 +- .../card.py => reposactivity/Card.py} | 20 ++--- .../fragment.py => reposactivity/Fragment.py} | 42 ++++----- .../repoIcon.py => reposactivity/RepoIcon.py} | 10 +-- .../RepoSheet.py} | 8 +- .../__init__.py | 2 +- .../ui/suggest/{fragment.py => Fragment.py} | 24 ++--- .../utils/{app_version.py => AppVersion.py} | 0 .../src/utils/{buildInfo.py => BuildInfo.py} | 0 .../src/utils/{bulletins.py => Bulletins.py} | 0 .../utils/{cachedRepos.py => CachedRepos.py} | 4 +- packit/src/utils/{copy.py => Copy.py} | 6 +- packit/src/utils/{drawable.py => Drawable.py} | 0 .../utils/{globalState.py => GlobalState.py} | 0 packit/src/utils/{hashUtil.py => HashUtil.py} | 2 +- .../src/utils/{imagePool.py => ImagePool.py} | 0 .../{importFailed.py => ImportFailed.py} | 0 .../{installIndex.py => InstallIndex.py} | 12 +-- packit/src/utils/{jsonx.py => Jsonx.py} | 0 .../utils/{localConfig.py => LocalConfig.py} | 8 +- packit/src/utils/{markdown.py => Markdown.py} | 0 packit/src/utils/{media.py => Media.py} | 4 +- packit/src/utils/{netQueue.py => NetQueue.py} | 0 packit/src/utils/{paths.py => Paths.py} | 2 +- .../src/utils/{repoStats.py => RepoStats.py} | 4 +- packit/src/utils/{ripple.py => Ripple.py} | 0 packit/src/utils/{search.py => Search.py} | 2 +- packit/src/utils/{share.py => Share.py} | 4 +- packit/src/utils/{stickers.py => Stickers.py} | 0 .../utils/{translation.py => Translation.py} | 8 +- 184 files changed, 709 insertions(+), 709 deletions(-) delete mode 100644 packit/src/ChatActivity/SecurityBottomSheets/__init__.py rename packit/src/{core.py => Core.py} (96%) rename packit/src/{dexLoader.py => DexLoader.py} (99%) rename packit/src/{main.py => Main.py} (87%) rename packit/src/{nativeLoader.py => NativeLoader.py} (99%) rename packit/src/{ChatActivity/afpFile.py => chatactivity/AfpFile.py} (98%) rename packit/src/{ChatActivity => chatactivity}/ConfirmImportBottomSheet.py (99%) rename packit/src/{ChatActivity => chatactivity}/ImportBottomSheet.py (99%) rename packit/src/{ChatActivity => chatactivity}/__init__.py (100%) rename packit/src/{ChatActivity => chatactivity}/export/DecryptorBottomSheet.py (93%) rename packit/src/{ChatActivity => chatactivity}/export/ImportBottomSheet.py (98%) rename packit/src/{ChatActivity => chatactivity}/export/__init__.py (100%) rename packit/src/{ChatActivity/export/bin/reader.py => chatactivity/export/bin/Reader.py} (96%) rename packit/src/{ChatActivity/export/bin/writer.py => chatactivity/export/bin/Writer.py} (96%) rename packit/src/{ChatActivity => chatactivity}/export/bin/__init__.py (100%) rename packit/src/{ChatActivity/inline/enterView.py => chatactivity/inline/EnterView.py} (98%) rename packit/src/{ChatActivity/inline/inlineBtns.py => chatactivity/inline/InlineBtns.py} (98%) rename packit/src/{ChatActivity/inline/inlineState.py => chatactivity/inline/InlineState.py} (100%) rename packit/src/{ChatActivity/inline/messageBuilder.py => chatactivity/inline/MessageBuilder.py} (99%) rename packit/src/{ChatActivity => chatactivity}/inline/__init__.py (100%) rename packit/src/{ChatActivity/LinksIcons/linksBottomSheet.py => chatactivity/linksicons/LinksBottomSheet.py} (100%) rename packit/src/{ChatActivity/LinksIcons => chatactivity/linksicons}/__init__.py (60%) rename packit/src/{ChatActivity/SecurityBottomSheets/hashBottomSheet.py => chatactivity/securitybottomsheets/HashBottomSheet.py} (99%) rename packit/src/{ChatActivity/SecurityBottomSheets/signaturesBottomSheet.py => chatactivity/securitybottomsheets/SignaturesBottomSheet.py} (99%) create mode 100644 packit/src/chatactivity/securitybottomsheets/__init__.py rename packit/src/deeplinks/{contributors.py => Contributors.py} (100%) rename packit/src/deeplinks/{deepHandler.py => DeepHandler.py} (75%) rename packit/src/deeplinks/{deeplinkMenu.py => DeeplinkMenu.py} (100%) rename packit/src/deeplinks/{docs.py => Docs.py} (100%) rename packit/src/deeplinks/{forum.py => Forum.py} (92%) rename packit/src/deeplinks/{install.py => Install.py} (96%) rename packit/src/deeplinks/{mainMenu.py => MainMenu.py} (89%) rename packit/src/deeplinks/{other.py => Other.py} (100%) rename packit/src/deeplinks/{pkill.py => Pkill.py} (94%) rename packit/src/deeplinks/{plugin.py => Plugin.py} (95%) rename packit/src/deeplinks/{problems.py => Problems.py} (92%) rename packit/src/deeplinks/{repo.py => Repo.py} (95%) rename packit/src/deeplinks/{settings.py => Settings.py} (90%) rename packit/src/deeplinks/{suggestion.py => Suggestion.py} (92%) rename packit/src/deeplinks/{update.py => Update.py} (95%) rename packit/src/deeplinks/secret/{aytist.py => Aytist.py} (98%) rename packit/src/deeplinks/secret/{premium.py => Premium.py} (93%) rename packit/src/deeplinks/secret/{terraria.py => Terraria.py} (94%) rename packit/src/{DialogsActivity/btnCAB.py => dialogsactivity/BtnCAB.py} (98%) rename packit/src/{DialogsActivity/btnPluginsMenu.py => dialogsactivity/BtnPluginsMenu.py} (97%) rename packit/src/{DialogsActivity/buildNotCorrect.py => dialogsactivity/BuildNotCorrect.py} (98%) rename packit/src/{DialogsActivity/button.py => dialogsactivity/Button.py} (90%) rename packit/src/{DialogsActivity/chatDialogButton.py => dialogsactivity/ChatDialogButton.py} (99%) rename packit/src/{DialogsActivity => dialogsactivity}/PackitUpdateSheet.py (98%) rename packit/src/{DialogsActivity/pillWidget.py => dialogsactivity/PillWidget.py} (99%) rename packit/src/{DialogsActivity/updatesWidget.py => dialogsactivity/UpdatesWidget.py} (98%) rename packit/src/{DialogsActivity => dialogsactivity}/__init__.py (100%) rename packit/src/other/{badges.py => Badges.py} (97%) rename packit/src/other/{chatBadge.py => ChatBadge.py} (100%) rename packit/src/other/{chatTitleIcon.py => ChatTitleIcon.py} (100%) rename packit/src/other/{everyone.py => Everyone.py} (98%) rename packit/src/other/{isBeta.py => IsBeta.py} (98%) rename packit/src/other/{profileTitleIcon.py => ProfileTitleIcon.py} (100%) rename packit/src/other/{text.py => Text.py} (94%) rename packit/src/scl/{doc.py => Doc.py} (94%) rename packit/src/scl/{errors.py => Errors.py} (100%) rename packit/src/scl/{native.py => Native.py} (99%) rename packit/src/scl/{opts.py => Opts.py} (93%) rename packit/src/scl/{scl.py => Scl.py} (92%) rename packit/src/scl/{value.py => Value.py} (99%) rename packit/src/{SettingsActivity/debugItems.py => settingsactivity/DebugItems.py} (97%) rename packit/src/{SettingsActivity/deeplinks.py => settingsactivity/Deeplinks.py} (97%) rename packit/src/{SettingsActivity/docs.py => settingsactivity/Docs.py} (93%) rename packit/src/{SettingsActivity/profile.py => settingsactivity/Profile.py} (98%) rename packit/src/{SettingsActivity/settings.py => settingsactivity/Settings.py} (98%) rename packit/src/{SettingsActivity/utilities.py => settingsactivity/Utilities.py} (97%) rename packit/src/{SettingsActivity/SubSettings => settingsactivity}/__init__.py (100%) rename packit/src/{SettingsActivity => settingsactivity}/service/AddKeyDialog.py (100%) rename packit/src/{SettingsActivity/service/fastExpandableHook.py => settingsactivity/service/FastExpandableHook.py} (100%) rename packit/src/{SettingsActivity/service/pluginsExport.py => settingsactivity/service/PluginsExport.py} (98%) rename packit/src/{SettingsActivity => settingsactivity/service}/__init__.py (100%) rename packit/src/{SettingsActivity/SubSettings/apikeys.py => settingsactivity/subsettings/Apikeys.py} (96%) rename packit/src/{SettingsActivity/SubSettings/comps.py => settingsactivity/subsettings/Comps.py} (100%) rename packit/src/{SettingsActivity/SubSettings/debug.py => settingsactivity/subsettings/Debug.py} (99%) rename packit/src/{SettingsActivity/SubSettings/fileSettings.py => settingsactivity/subsettings/FileSettings.py} (100%) rename packit/src/{SettingsActivity/SubSettings/hotkeys.py => settingsactivity/subsettings/Hotkeys.py} (100%) rename packit/src/{SettingsActivity/SubSettings/inline.py => settingsactivity/subsettings/Inline.py} (96%) rename packit/src/{SettingsActivity/SubSettings/interface.py => settingsactivity/subsettings/Interface.py} (100%) rename packit/src/{SettingsActivity/SubSettings/misc.py => settingsactivity/subsettings/Misc.py} (100%) rename packit/src/{SettingsActivity/SubSettings => settingsactivity/subsettings}/PluginCardEditor.py (99%) rename packit/src/{SettingsActivity/SubSettings/pluginProfile.py => settingsactivity/subsettings/PluginProfile.py} (100%) rename packit/src/{SettingsActivity/SubSettings/sfx.py => settingsactivity/subsettings/Sfx.py} (95%) rename packit/src/{SettingsActivity/SubSettings/updplugins.py => settingsactivity/subsettings/Updplugins.py} (100%) rename packit/src/{SettingsActivity/service => settingsactivity/subsettings}/__init__.py (100%) rename packit/src/{standaloneHooks/addIconsFab.py => standalonehooks/AddIconsFab.py} (99%) rename packit/src/{standaloneHooks/addPluginFab.py => standalonehooks/AddPluginFab.py} (99%) rename packit/src/{standaloneHooks => standalonehooks}/InstallDismissHook.py (95%) rename packit/src/{standaloneHooks/settingsActivityHook.py => standalonehooks/SettingsActivityHook.py} (100%) rename packit/src/{standaloneHooks/universalFragmentFix.py => standalonehooks/UniversalFragmentFix.py} (100%) rename packit/src/{standaloneHooks => standalonehooks}/__init__.py (100%) rename packit/src/ui/{contextMenu.py => ContextMenu.py} (100%) rename packit/src/ui/{md3Slider.py => Md3Slider.py} (100%) rename packit/src/ui/{reportDialog.py => ReportDialog.py} (99%) rename packit/src/ui/{restartDialog.py => RestartDialog.py} (98%) rename packit/src/ui/{viewUtils.py => ViewUtils.py} (100%) rename packit/src/ui/{AchievementsActivity/fragment.py => achievementsactivity/Fragment.py} (99%) rename packit/src/ui/{AchievementsActivity => achievementsactivity}/__init__.py (100%) rename packit/src/ui/{AchievementsActivity => achievementsactivity}/service/AchivementsEngine.py (98%) rename packit/src/ui/{AchievementsActivity => achievementsactivity}/service/__init__.py (100%) rename packit/src/ui/contributors/{fragment.py => Fragment.py} (99%) rename packit/src/ui/{FilesActivity/fragment.py => filesactivity/Fragment.py} (99%) rename packit/src/ui/{FilesActivity/infoDialog.py => filesactivity/InfoDialog.py} (98%) rename packit/src/ui/{FilesActivity/openFileFragment.py => filesactivity/OpenFileFragment.py} (98%) rename packit/src/ui/{FilesActivity/packlight.py => filesactivity/Packlight.py} (98%) rename packit/src/ui/{FilesActivity => filesactivity}/__init__.py (100%) rename packit/src/ui/{IconsListActivity/fragment.py => iconslistactivity/Fragment.py} (98%) rename packit/src/ui/{IconsListActivity => iconslistactivity}/RepoBottomSheet.py (97%) rename packit/src/ui/{IconsListActivity => iconslistactivity}/SortBottomSheet.py (97%) rename packit/src/ui/{IconsListActivity => iconslistactivity}/__init__.py (100%) rename packit/src/ui/{PluginActivity/fragment.py => pluginactivity/Fragment.py} (99%) rename packit/src/ui/{PluginActivity/versionPicker.py => pluginactivity/VersionPicker.py} (98%) rename packit/src/ui/{PluginActivity => pluginactivity}/__init__.py (100%) rename packit/src/ui/{PluginListActivity/card.py => pluginlistactivity/Card.py} (95%) rename packit/src/ui/{PluginListActivity/fragment.py => pluginlistactivity/Fragment.py} (95%) rename packit/src/ui/{PluginListActivity/listView.py => pluginlistactivity/ListView.py} (99%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/__init__.py (100%) rename packit/src/ui/{PluginListActivity/filter/filterDrawer.py => pluginlistactivity/filter/FilterDrawer.py} (99%) rename packit/src/ui/{PluginListActivity/filter/filterEngine.py => pluginlistactivity/filter/FilterEngine.py} (100%) rename packit/src/ui/{PluginListActivity/filter/tagLayoutListener.py => pluginlistactivity/filter/TagLayoutListener.py} (100%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/filter/__init__.py (100%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/helpers/PluginActions.py (94%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/helpers/ReportService.py (91%) rename packit/src/ui/{PluginListActivity/helpers/uiHelpers.py => pluginlistactivity/helpers/UiHelpers.py} (100%) rename packit/src/ui/{PluginListActivity/helpers/utils.py => pluginlistactivity/helpers/Utils.py} (98%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/helpers/__init__.py (100%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/sheets/AISearchSheet.py (98%) rename packit/src/ui/{PluginListActivity/sheets/depsSheet.py => pluginlistactivity/sheets/DepsSheet.py} (99%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/sheets/RepoBottomSheet.py (97%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/sheets/SortBottomSheet.py (97%) rename packit/src/ui/{PluginListActivity/sheets/tgChannelSheet.py => pluginlistactivity/sheets/TgChannelSheet.py} (96%) rename packit/src/ui/{PluginListActivity => pluginlistactivity}/sheets/__init__.py (100%) rename packit/src/ui/{pluginsUpdates/clearIgnoreListDialog.py => pluginsupdates/ClearIgnoreListDialog.py} (99%) rename packit/src/ui/{pluginsUpdates/fragment.py => pluginsupdates/Fragment.py} (98%) rename packit/src/ui/{pluginsUpdates/hideAllDialog.py => pluginsupdates/HideAllDialog.py} (99%) rename packit/src/ui/{pluginsUpdates/hideDialog.py => pluginsupdates/HideDialog.py} (99%) rename packit/src/ui/{pluginsUpdates/startupSheet.py => pluginsupdates/StartupSheet.py} (98%) rename packit/src/ui/{pluginsUpdates => pluginsupdates}/__init__.py (100%) rename packit/src/ui/{ReposActivity/actions.py => reposactivity/Actions.py} (98%) rename packit/src/ui/{ReposActivity/addSheet.py => reposactivity/AddSheet.py} (99%) rename packit/src/ui/{ReposActivity/card.py => reposactivity/Card.py} (97%) rename packit/src/ui/{ReposActivity/fragment.py => reposactivity/Fragment.py} (95%) rename packit/src/ui/{ReposActivity/repoIcon.py => reposactivity/RepoIcon.py} (97%) rename packit/src/ui/{ReposActivity/repoSheet.py => reposactivity/RepoSheet.py} (97%) rename packit/src/ui/{ReposActivity => reposactivity}/__init__.py (95%) rename packit/src/ui/suggest/{fragment.py => Fragment.py} (99%) rename packit/src/utils/{app_version.py => AppVersion.py} (100%) rename packit/src/utils/{buildInfo.py => BuildInfo.py} (100%) rename packit/src/utils/{bulletins.py => Bulletins.py} (100%) rename packit/src/utils/{cachedRepos.py => CachedRepos.py} (98%) rename packit/src/utils/{copy.py => Copy.py} (89%) rename packit/src/utils/{drawable.py => Drawable.py} (100%) rename packit/src/utils/{globalState.py => GlobalState.py} (100%) rename packit/src/utils/{hashUtil.py => HashUtil.py} (99%) rename packit/src/utils/{imagePool.py => ImagePool.py} (100%) rename packit/src/utils/{importFailed.py => ImportFailed.py} (100%) rename packit/src/utils/{installIndex.py => InstallIndex.py} (97%) rename packit/src/utils/{jsonx.py => Jsonx.py} (100%) rename packit/src/utils/{localConfig.py => LocalConfig.py} (96%) rename packit/src/utils/{markdown.py => Markdown.py} (100%) rename packit/src/utils/{media.py => Media.py} (94%) rename packit/src/utils/{netQueue.py => NetQueue.py} (100%) rename packit/src/utils/{paths.py => Paths.py} (99%) rename packit/src/utils/{repoStats.py => RepoStats.py} (96%) rename packit/src/utils/{ripple.py => Ripple.py} (100%) rename packit/src/utils/{search.py => Search.py} (99%) rename packit/src/utils/{share.py => Share.py} (98%) rename packit/src/utils/{stickers.py => Stickers.py} (100%) rename packit/src/utils/{translation.py => Translation.py} (97%) diff --git a/packit/meta.yml b/packit/meta.yml index acdb814..f610466 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.1.2-dev.31" +version: "0.1.2-dev.32" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/BasePlugin.py b/packit/src/BasePlugin.py index d603b55..0028f32 100644 --- a/packit/src/BasePlugin.py +++ b/packit/src/BasePlugin.py @@ -3,13 +3,13 @@ from typing import List, Any from base_plugin import BasePlugin -from . import main +from . import Main as main import time _launch_start = time.time() -# the launch logic has been delegated to main.py +# the launch logic has been delegated to Main.py # there shouldn't be anything extra in this file (if it is not required) class Main(BasePlugin): def __init__(self): diff --git a/packit/src/ChatActivity/SecurityBottomSheets/__init__.py b/packit/src/ChatActivity/SecurityBottomSheets/__init__.py deleted file mode 100644 index 094fb53..0000000 --- a/packit/src/ChatActivity/SecurityBottomSheets/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -# pyright: reportMissingImports=false -# SPDX-License-Identifier: GPL-3.0-or-later - -from .signaturesBottomSheet import setup_policy_button_hook -from .hashBottomSheet import setup_hash_button_hook \ No newline at end of file diff --git a/packit/src/core.py b/packit/src/Core.py similarity index 96% rename from packit/src/core.py rename to packit/src/Core.py index df0cade..5c0ba05 100644 --- a/packit/src/core.py +++ b/packit/src/Core.py @@ -19,17 +19,17 @@ from org.telegram.messenger import ApplicationLoader, AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins import PluginsController except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins import PluginsController failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import NotificationCenter except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import NotificationCenter failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() import time import signal @@ -119,7 +119,7 @@ def _is_elyx_plugin(plugin_info: dict) -> bool: def install_plugin(plugin_info: dict, icon_view=None, button=None, original_icon_id=None, loading_view=None, on_finish=None, install_ui=None, all_plugins: list = None, rm_rid: str = "", succ_download=None): deps = plugin_info.get("deps") or [] if deps: - from .ui.PluginListActivity.sheets.depsSheet import show_deps_sheet + from .ui.pluginlistactivity.sheets.DepsSheet import show_deps_sheet def on_confirmed(): _do_install(plugin_info, icon_view, button, original_icon_id, loading_view, on_finish, install_ui, rm_rid=rm_rid, succ_download=succ_download) show_deps_sheet(install_ui, plugin_info, on_confirmed, all_plugins=all_plugins, on_cancel=on_finish) @@ -155,10 +155,10 @@ def _commit_index(): return try: if _is_elyx_plugin(plugin_info): - from .utils.installIndex import commit_elyx_pending + from .utils.InstallIndex import commit_elyx_pending commit_elyx_pending(plugin_info, rm_rid, original_path=temp_path) else: - from .utils.installIndex import commit_pending + from .utils.InstallIndex import commit_pending commit_pending() except Exception as e: logx(f"core: index commit error: {e}", False) @@ -191,7 +191,7 @@ def _on_plugins_updated(): # to elyx only to avoid double counting. if _is_elyx_plugin(plugin_info): try: - from .ui.AchievementsActivity.service.AchivementsEngine import increment_category + from .ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Installing plugins") except Exception as e: logx(f"core: elyx achievement increment error: {e}", False) @@ -207,7 +207,7 @@ def _on_plugins_updated(): logx(f"core: check restart={restart}", True) if restart in ("required", "optional"): logx("core: calling show_restart_dialog", True) - from .ui.restartDialog import show_restart_dialog + from .ui.RestartDialog import show_restart_dialog show_restart_dialog(restart, fragment) except Exception as e: logx(f"core: restart dialog error: {e}", False) @@ -261,7 +261,7 @@ def didReceivedNotification(self, id, account, *args): ElyxEngine.instance.showInstallDialog(fragment, install_params) else: if write_index: - from .utils.installIndex import set_pending + from .utils.InstallIndex import set_pending set_pending(plugin_info, rm_rid) PluginsController.getInstance().showInstallDialog(fragment, temp_path, True) @@ -275,14 +275,14 @@ def didReceivedNotification(self, id, account, *args): pass -from .utils.hashUtil import hashFile, getHashMethod, METHOD_SHA256, METHOD_BITHASH, matchesStoredHash +from .utils.HashUtil import hashFile, getHashMethod, METHOD_SHA256, METHOD_BITHASH, matchesStoredHash def _get_plugin_cache_path(pkg: str, filename: str) -> str: # cache is isolated per hash method method = getHashMethod() subdir = "BitHash" if method == METHOD_BITHASH else "sha256" - from .utils.paths import getPluginCacheDir + from .utils.Paths import getPluginCacheDir cache_dir = getPluginCacheDir(subdir) os.makedirs(cache_dir, exist_ok=True) return os.path.join(cache_dir, filename) @@ -290,7 +290,7 @@ def _get_plugin_cache_path(pkg: str, filename: str) -> str: -# keep old name as alias so fragment.py import stays valid +# keep old name as alias so Fragment.py import stays valid def _sha256_file(path: str) -> str: return hashFile(path) @@ -346,7 +346,7 @@ def _do_install(plugin_info: dict, icon_view=None, button=None, original_icon_id def task(): try: - from .utils.paths import getPluginsDir + from .utils.Paths import getPluginsDir plugins_dir = getPluginsDir() try: os.makedirs(plugins_dir, exist_ok=True) @@ -520,7 +520,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(_s("core_iconpack_http_error", code=r.status_code))) return - from .utils.paths import getIconPackTmpPath + from .utils.Paths import getIconPackTmpPath tmp_path = getIconPackTmpPath(pack_id) content_length = r.headers.get("content-length") @@ -596,7 +596,7 @@ def install_plugin_silent(file_path: str, plugin_data: dict, repo_id: str, on_co from elyxcore._plugin_engine import ElyxEngine except ImportError: from elyxcore import ElyxPlugin, ElyxEngine # older SDKs - from .utils.installIndex import commit_elyx_pending + from .utils.InstallIndex import commit_elyx_pending elyx_plugin = ElyxPlugin(plzip=ZipFile(file_path, "r"), raise_errors=False) @@ -633,7 +633,7 @@ def _elyx_error(error): try: from elyxcore import gen from org.telegram.messenger import Utilities - from .utils.installIndex import set_pending, commit_pending + from .utils.InstallIndex import set_pending, commit_pending Callback = gen(Utilities.Callback, "run") python_engine = PluginsController.getEngines().get("python") diff --git a/packit/src/dexLoader.py b/packit/src/DexLoader.py similarity index 99% rename from packit/src/dexLoader.py rename to packit/src/DexLoader.py index 2c8f33c..d6f0200 100644 --- a/packit/src/dexLoader.py +++ b/packit/src/DexLoader.py @@ -19,7 +19,7 @@ def _dexPath(name: str) -> str: - from .utils.paths import _filesDir + from .utils.Paths import _filesDir return _filesDir() + _DEX_BASE + "/" + name + ".dex" diff --git a/packit/src/main.py b/packit/src/Main.py similarity index 87% rename from packit/src/main.py rename to packit/src/Main.py index 4598771..8fe1507 100644 --- a/packit/src/main.py +++ b/packit/src/Main.py @@ -2,11 +2,11 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from .utils.bulletins import factory as _pbf +from .utils.Bulletins import factory as _pbf import time from typing import Any -from .other import text as _text -from .ChatActivity.inline.enterView import ( +from .other import Text as _text +from .chatactivity.inline.EnterView import ( _packit_get_class, _packit_hook_enter_view_constructor, _packit_attach_text_watcher, @@ -26,7 +26,7 @@ def _clearLatestLog(): try: - from .utils.paths import getCacheRoot, getPluginsDir + from .utils.Paths import getCacheRoot, getPluginsDir import os, json settingsPath = getPluginsDir() + "/plugin_settings.json" if os.path.exists(settingsPath): @@ -44,14 +44,14 @@ def _clearLatestLog(): def startInit(plugin, launchStart): _clearLatestLog() - from .nativeLoader import detectArch + from .NativeLoader import detectArch detectArch() from .RepositoryManager import RepositoryManager - from .core import PackItCore + from .Core import PackItCore from .MainActivity import SettingsBuilder - from .DialogsActivity.button import ChatButton - from .other.badges import BadgeManager + from .dialogsactivity.Button import ChatButton + from .other.Badges import BadgeManager plugin._launch_start = launchStart plugin.repoManager = RepositoryManager() @@ -79,7 +79,7 @@ def startInit(plugin, launchStart): def _migrate_packitcache(): import os try: - from .utils.paths import _filesDir + from .utils.Paths import _filesDir base = _filesDir() old = base + "/packitCache" new = base + "/packit" @@ -92,7 +92,7 @@ def _migrate_packitcache(): def _check_paths(): try: - from .utils.paths import ( + from .utils.Paths import ( getCacheRoot, getConfigsDir, getReposCacheDir, getTempDir, getPluginsDir, getElyxArchivesDir, getBitHashSoPath, ) @@ -120,31 +120,31 @@ def loadPlugin(plugin): logx(f"PackIt: import settings failed: {e}", False) return - from .ui.PluginActivity.fragment import process_start + from .ui.pluginactivity.Fragment import process_start process_start() if RENAME_PACKITCACHE: _migrate_packitcache() if CHECK_PATHS: _check_paths() - from .nativeLoader import CHECK_SO_PATHS, checkSoPaths + from .NativeLoader import CHECK_SO_PATHS, checkSoPaths if CHECK_SO_PATHS: checkSoPaths() - from .utils.localConfig import LocalConfig + from .utils.LocalConfig import LocalConfig LocalConfig.init() - from .DialogsActivity.buildNotCorrect import setup_build_not_correct_check + from .dialogsactivity.BuildNotCorrect import setup_build_not_correct_check setup_build_not_correct_check() try: - from .utils.installIndex import purge_missing + from .utils.InstallIndex import purge_missing purge_missing() except Exception as e: logx(f"PackIt: installIndex purge error: {e}", False) - from .other import isBeta - from .other import everyone as _everyone - isBeta.init() + from .other import IsBeta + from .other import Everyone as _everyone + IsBeta.init() _everyone.init() try: - from .ui.AchievementsActivity.service.AchivementsEngine import sync_accounts, sync_completed, _load_account, _save_account + from .ui.achievementsactivity.service.AchivementsEngine import sync_accounts, sync_completed, _load_account, _save_account sync_accounts() loaded, load_ok = _load_account() data, _ = sync_completed(loaded) @@ -165,36 +165,36 @@ def loadPlugin(plugin): plugin.deeplink_hook_ref = setup_deeplink_hook(plugin) plugin.chatUI.initialize_chat_menu() plugin.badgeManager.setup_hooks() - from .ChatActivity.SecurityBottomSheets import setup_policy_button_hook, setup_hash_button_hook + from .chatactivity.securitybottomsheets import setup_policy_button_hook, setup_hash_button_hook plugin.policy_button_hook_ref = setup_policy_button_hook(plugin) plugin.hash_button_hook_ref = setup_hash_button_hook(plugin, plugin.repoManager) - from .ChatActivity.LinksIcons import setup_links_buttons_hook + from .chatactivity.linksicons import setup_links_buttons_hook plugin.links_button_hook_ref = setup_links_buttons_hook(plugin) - from .standaloneHooks.InstallDismissHook import setup_install_dismiss_hook + from .standalonehooks.InstallDismissHook import setup_install_dismiss_hook plugin.install_dismiss_hook_ref = setup_install_dismiss_hook(plugin) - from .standaloneHooks.universalFragmentFix import setup_universal_fragment_fix + from .standalonehooks.UniversalFragmentFix import setup_universal_fragment_fix plugin.universal_fragment_fix_ref = setup_universal_fragment_fix(plugin) - from .ChatActivity.export.DecryptorBottomSheet import setup_packit_file_hook + from .chatactivity.export.DecryptorBottomSheet import setup_packit_file_hook setup_packit_file_hook(plugin) - from .ChatActivity.afpFile import setup_afp_file_hook + from .chatactivity.AfpFile import setup_afp_file_hook setup_afp_file_hook(plugin) - from .standaloneHooks.addPluginFab import setup_plugins_activity_fab + from .standalonehooks.AddPluginFab import setup_plugins_activity_fab plugin.plugins_activity_fab_ref = setup_plugins_activity_fab(plugin) - from .standaloneHooks.addIconsFab import setup_icon_packs_activity_fab + from .standalonehooks.AddIconsFab import setup_icon_packs_activity_fab plugin.icon_packs_activity_fab_ref = setup_icon_packs_activity_fab(plugin) - from .standaloneHooks.settingsActivityHook import setup_settings_activity_hook + from .standalonehooks.SettingsActivityHook import setup_settings_activity_hook plugin.settings_activity_hook_refs = setup_settings_activity_hook(plugin) - from .SettingsActivity.service.fastExpandableHook import setup_fast_expandable_hook + from .settingsactivity.service.FastExpandableHook import setup_fast_expandable_hook plugin.fast_expandable_hook_ref = setup_fast_expandable_hook(plugin, plugin.settingsBuilder.otherSettings) - from .DialogsActivity.pillWidget import setup_pill_widget + from .dialogsactivity.PillWidget import setup_pill_widget setup_pill_widget(plugin) - from .DialogsActivity.updatesWidget import setup_updates_widget + from .dialogsactivity.UpdatesWidget import setup_updates_widget setup_updates_widget(plugin) plugin.dialogs_menu_hook_ref = plugin.chatUI.setup_dialogs_menu_hook() plugin.everyone_hook_refs = _everyone.setup_hook(plugin) - from .ChatActivity.inline.enterView import setup_packit_autocomplete + from .chatactivity.inline.EnterView import setup_packit_autocomplete plugin.packit_hook_constructor_ref = setup_packit_autocomplete(plugin) - from .ChatActivity.inline.inlineBtns import setup_inline_translate_button + from .chatactivity.inline.InlineBtns import setup_inline_translate_button setup_inline_translate_button(plugin) plugin._init_official_repository() plugin._check_for_update() @@ -221,7 +221,7 @@ def _show_startup_bulletin(plugin): def _check_for_update(plugin): try: - from .DialogsActivity.PackitUpdateSheet import check_and_show + from .dialogsactivity.PackitUpdateSheet import check_and_show check_and_show() except Exception as e: logx(f"PackIt: update check error: {e}", False) @@ -229,7 +229,7 @@ def _check_for_update(plugin): def _check_startup_updates(plugin): try: - from .ui.pluginsUpdates.startupSheet import check_and_show_startup_updates + from .ui.pluginsupdates.StartupSheet import check_and_show_startup_updates check_and_show_startup_updates(plugin=plugin) except Exception as e: logx(f"PackIt: startup updates check error: {e}", False) @@ -240,7 +240,7 @@ def _check_update_notifications_bulletin(plugin): def task(): try: - from .ui.pluginsUpdates.fragment import _check_updates, _filter_ignored + from .ui.pluginsupdates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) if not updates: return @@ -283,7 +283,7 @@ def show(): return try: from elyx import assets - from .utils.media import playSound + from .utils.Media import playSound _snd = assets.sounds.available_updates.path_str playSound(_snd, "sfx_available_updates") except Exception as _e: @@ -291,7 +291,7 @@ def show(): if single_update is not None: def _install(): try: - from .ui.pluginsUpdates.fragment import _get_repos, _get_repo_plugins_url + from .ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url import requests as _req pid = str(single_update.get("id") or "") repo_id = str(single_update.get("repo_id") or "") @@ -326,7 +326,7 @@ def _install(): if not plugin_item: logx(f"PackIt: update bulletin install: plugin '{pid}' not found in repo", True) return - from .core import install_plugin + from .Core import install_plugin run_on_ui_thread(lambda: install_plugin(plugin_item, all_plugins=all_plugins, rm_rid=repo_id)) except Exception as _e: logx(f"PackIt: update bulletin install error: {_e}", True) @@ -335,7 +335,7 @@ def _install(): else: def _action(): try: - from .ui.pluginsUpdates.fragment import show_updates_fragment + from .ui.pluginsupdates.Fragment import show_updates_fragment show_updates_fragment() except Exception as _e: logx(f"PackIt: update bulletin open error: {_e}", True) @@ -362,7 +362,7 @@ def _check_identity_achievement(plugin): return first_name = str(user.first_name) if user.first_name else "" if first_name.lower() in ("shareui", "fuchs"): - from .ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from .ui.achievementsactivity.service.AchivementsEngine import unlock_secret unlock_secret("identity") @@ -385,7 +385,7 @@ def on_send_message_hook(plugin, account: int, params: Any): if params.message.startswith(".deleteachievements"): try: import os - from .ui.AchievementsActivity.service.AchivementsEngine import _get_db_path, _get_snap_path + from .ui.achievementsactivity.service.AchivementsEngine import _get_db_path, _get_snap_path for path in (_get_db_path(), _get_snap_path()): if os.path.exists(path): os.remove(path) diff --git a/packit/src/MainActivity.py b/packit/src/MainActivity.py index 456221a..d76a8c9 100644 --- a/packit/src/MainActivity.py +++ b/packit/src/MainActivity.py @@ -7,13 +7,13 @@ from elyx import strings, metainfo except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, metainfo failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from .SettingsActivity.deeplinks import DeeplinksSettings -from .SettingsActivity.settings import OtherSettings -from .SettingsActivity.docs import DocumentationSettings -from .ui.contributors.fragment import show_contributors_fragment -from .SettingsActivity.profile import ProfileSettings -from .SettingsActivity.utilities import UtilitiesSettings + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from .settingsactivity.Deeplinks import DeeplinksSettings +from .settingsactivity.Settings import OtherSettings +from .settingsactivity.Docs import DocumentationSettings +from .ui.contributors.Fragment import show_contributors_fragment +from .settingsactivity.Profile import ProfileSettings +from .settingsactivity.Utilities import UtilitiesSettings from ui.bulletin import BulletinHelper from base_plugin import BasePlugin, MethodHook from android_utils import run_on_ui_thread @@ -22,17 +22,17 @@ from org.telegram.ui.ActionBar import Theme, BottomSheet except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme, BottomSheet failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper, UItem, BackupImageView, EffectsTextView, BulletinFactory except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper, UItem, BackupImageView, EffectsTextView, BulletinFactory failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.models import HeaderSetting except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.models import HeaderSetting failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.widget import FrameLayout, TextView, LinearLayout, ScrollView from android.graphics.drawable import GradientDrawable from android.view import Gravity @@ -41,15 +41,15 @@ from org.telegram.messenger import AndroidUtilities, ImageLocation, MediaDataController, R except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, ImageLocation, MediaDataController, R failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from .ui.PluginListActivity.fragment import InstallUI -from .ui.IconsListActivity.fragment import InstallIconsUI + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from .ui.pluginlistactivity.Fragment import InstallUI +from .ui.iconslistactivity.Fragment import InstallIconsUI from client_utils import get_last_fragment try: from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.net import Uri from java import dynamic_proxy as dyp from android_utils import OnClickListener, OnLongClickListener @@ -147,12 +147,12 @@ def _create_settings_header(self, context): imageView = BackupImageView(context) imageView.setRoundRadius(AndroidUtilities.dp(45)) - from .utils.stickers import load_sticker + from .utils.Stickers import load_sticker load_sticker(imageView, __icon__, 130) def _on_sticker_long_click(): try: - from .SettingsActivity.debugItems import show_debug_menu + from .settingsactivity.DebugItems import show_debug_menu show_debug_menu() except Exception as _e: logx(f"MainActivity: sticker long click error: {_e}", True) @@ -193,7 +193,7 @@ def _open_install_plugin(self, view): def _check_updates(self, view): try: - from .ui.pluginsUpdates.fragment import show_updates_fragment + from .ui.pluginsupdates.Fragment import show_updates_fragment show_updates_fragment(self.plugin) except Exception as e: @@ -201,7 +201,7 @@ def _check_updates(self, view): def _open_repositories(self, view): try: - from .ui.ReposActivity import show_repos_fragment + from .ui.reposactivity import show_repos_fragment show_repos_fragment(self.repoManager) except Exception as e: logx(f"MainActivity: _open_repositories error: {e}", False) @@ -358,7 +358,7 @@ def buildMainSettings(self): ] def _build_client_label(self): - from .utils.buildInfo import getBuildClientName, getBuildStaticVersion + from .utils.BuildInfo import getBuildClientName, getBuildStaticVersion client_str = getBuildClientName() static_ver = getBuildStaticVersion() diff --git a/packit/src/nativeLoader.py b/packit/src/NativeLoader.py similarity index 99% rename from packit/src/nativeLoader.py rename to packit/src/NativeLoader.py index 52f2a08..40fee54 100644 --- a/packit/src/nativeLoader.py +++ b/packit/src/NativeLoader.py @@ -38,7 +38,7 @@ def detectArch() -> str: def _soPath(libName: str) -> str: - from .utils.paths import _filesDir + from .utils.Paths import _filesDir arch = detectArch() return _filesDir() + _BASE + "/" + arch + "/" + libName + ".so" @@ -140,7 +140,7 @@ def _retry(): # index 49 of the error pack; the shared loader binds it now if # cached, else on diceStickersDidLoad (no polling) - from .utils.stickers import load_sticker + from .utils.Stickers import load_sticker load_sticker(iv, "wtffffffffffDD/49", 100) linear.addView(iv, LayoutHelper.createLinear( diff --git a/packit/src/RepositoryManager.py b/packit/src/RepositoryManager.py index 4cebd7b..d32c2e4 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/RepositoryManager.py @@ -2,22 +2,22 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from .utils.netQueue import run_serial_io +from .utils.NetQueue import run_serial_io from .network import Storage -from .utils import cachedRepos +from .utils import CachedRepos import json from client_utils import get_last_fragment, run_on_queue try: from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from .utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() OFFICIAL_REPO_URL = "https://raw.githubusercontent.com/shareui/packit/refs/heads/main/configs/repomap.json" @@ -77,7 +77,7 @@ def setRepositories(self, repos): # the sources screen is a plain fragment with no adapter, so # rebuildAllItems never reaches it — it listens here instead try: - from .ui.ReposActivity import notify_repos_changed + from .ui.reposactivity import notify_repos_changed notify_repos_changed() except Exception: pass @@ -89,7 +89,7 @@ def _fetch_and_save_repomap(self, url: str) -> dict | None: logx(f"repom: cannot fetch repomap from '{url}': {error}", True) return None repometa = data.get("repometa") - if not cachedRepos.write(repometa.get("rm_rid"), data): + if not CachedRepos.write(repometa.get("rm_rid"), data): return None return repometa @@ -113,7 +113,7 @@ def addRepositoryWithUrl(self, url: str): if not rm_name: return None, "missing rm_name" - if not cachedRepos.write(rm_rid, data): + if not CachedRepos.write(rm_rid, data): return None, "cache write failed" repos = self.getRepositories() @@ -181,7 +181,7 @@ def removeRepository(self, idx): logx(f"repom.removeRepository: removing idx={idx}, id={repr(repo_id)}, name={repr(repo.get('name'))}", True) if repo_id: - dropped = cachedRepos.forget(repo_id) + dropped = CachedRepos.forget(repo_id) logx(f"repom.removeRepository: cache for '{repo_id}' " f"{'deleted' if dropped else 'was not there'}", True) @@ -323,7 +323,7 @@ def task(): changed = True logx(f"updateAllCaches: restored name '{rm_name}' for '{rm_rid}'", True) - cachedRepos.write(rm_rid, data) + CachedRepos.write(rm_rid, data) logx(f"updateAllCaches: updated cache for '{rm_rid}'", True) except Exception as e: logx(f"updateAllCaches: error for {url}: {e}", False) diff --git a/packit/src/ChatActivity/afpFile.py b/packit/src/chatactivity/AfpFile.py similarity index 98% rename from packit/src/ChatActivity/afpFile.py rename to packit/src/chatactivity/AfpFile.py index 5c0632d..f0b2794 100644 --- a/packit/src/ChatActivity/afpFile.py +++ b/packit/src/chatactivity/AfpFile.py @@ -29,9 +29,9 @@ def before_hooked_method(self, param): logx(f"afpFile: before_hooked_method error: {e}", False) def _read(self, file_path: str, filename: str): - from ..scl.scl import parse - from ..scl.opts import ParseOpts - from ..utils.paths import getTempDir + from ..scl.Scl import parse + from ..scl.Opts import ParseOpts + from ..utils.Paths import getTempDir import os import shutil import time diff --git a/packit/src/ChatActivity/ConfirmImportBottomSheet.py b/packit/src/chatactivity/ConfirmImportBottomSheet.py similarity index 99% rename from packit/src/ChatActivity/ConfirmImportBottomSheet.py rename to packit/src/chatactivity/ConfirmImportBottomSheet.py index 974fbcd..f748081 100644 --- a/packit/src/ChatActivity/ConfirmImportBottomSheet.py +++ b/packit/src/chatactivity/ConfirmImportBottomSheet.py @@ -314,8 +314,8 @@ def __init__(self): super().__init__() def onClick(self, v): import threading import zipfile - from ..core import onlyLocalInstallNoUi - from ..utils.paths import getTempDir + from ..Core import onlyLocalInstallNoUi + from ..utils.Paths import getTempDir from ui.bulletin import BulletinHelper from org.telegram.messenger import R as R_tg @@ -601,7 +601,7 @@ def _show(): def _run_installs(): try: import re as _re - from ..utils.app_version import _parse_version, _get_app_version + from ..utils.AppVersion import _parse_version, _get_app_version tmp_dir = getTempDir() os.makedirs(tmp_dir, exist_ok=True) with zipfile.ZipFile(file_path, "r") as zf: diff --git a/packit/src/ChatActivity/ImportBottomSheet.py b/packit/src/chatactivity/ImportBottomSheet.py similarity index 99% rename from packit/src/ChatActivity/ImportBottomSheet.py rename to packit/src/chatactivity/ImportBottomSheet.py index 6c073a5..8801ffe 100644 --- a/packit/src/ChatActivity/ImportBottomSheet.py +++ b/packit/src/chatactivity/ImportBottomSheet.py @@ -26,7 +26,7 @@ def _make_icon_view(activity, icon_str: str, size_dp: int): iv.getImageReceiver().setCrossfadeWithOldImage(True) except Exception: pass - from ..utils.stickers import load_sticker + from ..utils.Stickers import load_sticker load_sticker(iv, icon_str, size_dp) return iv except Exception as e: diff --git a/packit/src/ChatActivity/__init__.py b/packit/src/chatactivity/__init__.py similarity index 100% rename from packit/src/ChatActivity/__init__.py rename to packit/src/chatactivity/__init__.py diff --git a/packit/src/ChatActivity/export/DecryptorBottomSheet.py b/packit/src/chatactivity/export/DecryptorBottomSheet.py similarity index 93% rename from packit/src/ChatActivity/export/DecryptorBottomSheet.py rename to packit/src/chatactivity/export/DecryptorBottomSheet.py index f764f31..8c36b69 100644 --- a/packit/src/ChatActivity/export/DecryptorBottomSheet.py +++ b/packit/src/chatactivity/export/DecryptorBottomSheet.py @@ -37,8 +37,8 @@ def before_hooked_method(self, param): def _prepare_and_show(self, file_path: str): try: - from ...ChatActivity.export.bin.writer import _get_user_id, _get_install_ts - from ...ChatActivity.export.bin.reader import read_file + from ...chatactivity.export.bin.Writer import _get_user_id, _get_install_ts + from ...chatactivity.export.bin.Reader import read_file from elyx import strings current_user_id = _get_user_id() @@ -54,14 +54,14 @@ def _prepare_and_show(self, file_path: str): import_xp = None if "achievements" in blocks: try: - from ...ui.AchievementsActivity.service.AchivementsEngine import get_level_info + from ...ui.achievementsactivity.service.AchivementsEngine import get_level_info achievements_data = json.loads(blocks["achievements"]) def _is_hashed_id(k: str) -> bool: return len(k) == 16 and all(c in "0123456789abcdef" for c in k) if isinstance(achievements_data, dict) and achievements_data and all(_is_hashed_id(k) for k in achievements_data): - from ...ui.AchievementsActivity.service.AchivementsEngine import _hash_account_id + from ...ui.achievementsactivity.service.AchivementsEngine import _hash_account_id account_data = achievements_data.get(_hash_account_id(export_user_id), {}) elif isinstance(achievements_data, dict): account_data = achievements_data @@ -76,7 +76,7 @@ def _is_hashed_id(k: str) -> bool: from .ImportBottomSheet import show_import_bottom_sheet def on_confirm(): - from ...ui.AchievementsActivity.service.AchivementsEngine import _hash_account_id + from ...ui.achievementsactivity.service.AchivementsEngine import _hash_account_id account_id = _hash_account_id(export_user_id) threading.Thread(target=self._restore, args=(blocks, account_id), daemon=True).start() @@ -92,7 +92,7 @@ def show(): def _restore(self, blocks: dict, account_id: str): try: - from ...ChatActivity.export.bin.reader import _write_blocks + from ...chatactivity.export.bin.Reader import _write_blocks from client_utils import get_last_fragment from ui.bulletin import BulletinHelper from org.telegram.messenger import R diff --git a/packit/src/ChatActivity/export/ImportBottomSheet.py b/packit/src/chatactivity/export/ImportBottomSheet.py similarity index 98% rename from packit/src/ChatActivity/export/ImportBottomSheet.py rename to packit/src/chatactivity/export/ImportBottomSheet.py index c169847..10b035a 100644 --- a/packit/src/ChatActivity/export/ImportBottomSheet.py +++ b/packit/src/chatactivity/export/ImportBottomSheet.py @@ -41,7 +41,7 @@ def show_import_bottom_sheet(fragment, num_blocks: int, on_confirm, import_level level = import_level xp_into = import_xp else: - from ...ui.AchievementsActivity.service.AchivementsEngine import get_level_info, _load_account + from ...ui.achievementsactivity.service.AchivementsEngine import get_level_info, _load_account data, _ = _load_account() level, xp_into, _ = get_level_info(data) diff --git a/packit/src/ChatActivity/export/__init__.py b/packit/src/chatactivity/export/__init__.py similarity index 100% rename from packit/src/ChatActivity/export/__init__.py rename to packit/src/chatactivity/export/__init__.py diff --git a/packit/src/ChatActivity/export/bin/reader.py b/packit/src/chatactivity/export/bin/Reader.py similarity index 96% rename from packit/src/ChatActivity/export/bin/reader.py rename to packit/src/chatactivity/export/bin/Reader.py index e10979c..e27e184 100644 --- a/packit/src/ChatActivity/export/bin/reader.py +++ b/packit/src/chatactivity/export/bin/Reader.py @@ -6,7 +6,7 @@ import os import ctypes -from .writer import _get_configs_dir, _get_lib, _FILE_NAMES +from .Writer import _get_configs_dir, _get_lib, _FILE_NAMES def _write_blocks(blocks: dict, account_id: str): @@ -52,7 +52,7 @@ def _is_hashed_id(k: str) -> bool: account_data = account_data["d"] depth += 1 - from ....ui.AchievementsActivity.service.AchivementsEngine import load_account_data_for_import + from ....ui.achievementsactivity.service.AchivementsEngine import load_account_data_for_import load_account_data_for_import(account_id, account_data) logx(f"exportBin: merged achievements for account {account_id}", True) diff --git a/packit/src/ChatActivity/export/bin/writer.py b/packit/src/chatactivity/export/bin/Writer.py similarity index 96% rename from packit/src/ChatActivity/export/bin/writer.py rename to packit/src/chatactivity/export/bin/Writer.py index 8a7ca6b..7a143e8 100644 --- a/packit/src/ChatActivity/export/bin/writer.py +++ b/packit/src/chatactivity/export/bin/Writer.py @@ -25,7 +25,7 @@ def _get_configs_dir() -> str: - from ....utils.paths import getConfigsDir + from ....utils.Paths import getConfigsDir return getConfigsDir() @@ -55,13 +55,13 @@ def _get_install_ts() -> int: def _get_lib(): - from ....nativeLoader import loadExport + from ....NativeLoader import loadExport return loadExport() def _read_achievements_block() -> str: try: - from ....ui.AchievementsActivity.service.AchivementsEngine import ( + from ....ui.achievementsactivity.service.AchivementsEngine import ( _load_account, _get_current_account_id ) account_id = _get_current_account_id() @@ -76,7 +76,7 @@ def _read_achievements_block() -> str: def _read_saved_plugins_block() -> str: try: - from ....ui.PluginActivity.fragment import _read_saved_plugins + from ....ui.pluginactivity.Fragment import _read_saved_plugins data = _read_saved_plugins() content = json.dumps(data, ensure_ascii=False) logx(f"exportBin: saved_plugins block read ({len(data)} items)", True) diff --git a/packit/src/ChatActivity/export/bin/__init__.py b/packit/src/chatactivity/export/bin/__init__.py similarity index 100% rename from packit/src/ChatActivity/export/bin/__init__.py rename to packit/src/chatactivity/export/bin/__init__.py diff --git a/packit/src/ChatActivity/inline/enterView.py b/packit/src/chatactivity/inline/EnterView.py similarity index 98% rename from packit/src/ChatActivity/inline/enterView.py rename to packit/src/chatactivity/inline/EnterView.py index 2a1418c..3d615f9 100644 --- a/packit/src/ChatActivity/inline/enterView.py +++ b/packit/src/chatactivity/inline/EnterView.py @@ -107,7 +107,7 @@ def _flag_match(plugin, flags): # app_version: each expression must pass check_app_version if "app_version" in flags: - from ...utils.app_version import check_app_version + from ...utils.AppVersion import check_app_version for expr in flags["app_version"]: if not check_app_version(expr): return False @@ -126,8 +126,8 @@ def plugin(self): def after_hooked_method(self, param): try: - from . import inlineState - if not inlineState.get_state(): + from . import InlineState + if not InlineState.get_state(): return plugin = self.plugin if not plugin: @@ -184,8 +184,8 @@ def onTextChanged(self, s, start, before, count): pass def afterTextChanged(self, editable): try: - from . import inlineState - if not inlineState.get_state(): + from . import InlineState + if not InlineState.get_state(): return except Exception: pass @@ -242,14 +242,14 @@ def do_search(): def _packit_load_plugins_from_cache(self): from ...network import Storage - from ...utils import cachedRepos + from ...utils import CachedRepos plugins_list = [] try: for repo in self.repoManager.getRepositories(): repo_id = repo.get("id") if not repo_id: continue - plugins_url = cachedRepos.plugins_url(repo) + plugins_url = CachedRepos.plugins_url(repo) if not plugins_url: continue entries, error = Storage.fetch_plugins(plugins_url) @@ -296,7 +296,7 @@ def _packit_search_in_background(self, search_key, token): run_on_ui_thread(lambda: self._packit_show_plugins_popup(result)) return - from ...utils.search import build_index, score as search_score + from ...utils.Search import build_index, score as search_score index = build_index(candidates) @@ -563,8 +563,8 @@ def onItemClick(self, view, position): def open_profile(): try: - from ...ui.PluginListActivity.fragment import InstallUI - from ...ui.PluginActivity.fragment import show_plugin_profile + from ...ui.pluginlistactivity.Fragment import InstallUI + from ...ui.pluginactivity.Fragment import show_plugin_profile class _FakePlugin: def __init__(self, rm): @@ -639,8 +639,8 @@ def _packit_hook_container_dismiss(self, bot_container): class DismissHook(MethodHook): def before_hooked_method(self_hook, param): try: - from . import inlineState - if not inlineState.get_state(): + from . import InlineState + if not InlineState.get_state(): return except Exception: pass @@ -685,7 +685,7 @@ def _u16len(text) -> int: def _strip_markdown(text) -> str: # plain text for places that cannot render entities (the autocomplete popup # binds its rows to java Strings), so markers don't show up raw - from ...utils.markdown import to_plain + from ...utils.Markdown import to_plain return to_plain(text) @@ -789,7 +789,7 @@ def _packit_send_plugin_info(self, plugin_data): output_type = getattr(self, "_packit_output_type", None) - from .messageBuilder import build_plugin_message + from .MessageBuilder import build_plugin_message message_text, entities = build_plugin_message( name, version, author, plugin_id, repo_id, description, output_type=output_type, diff --git a/packit/src/ChatActivity/inline/inlineBtns.py b/packit/src/chatactivity/inline/InlineBtns.py similarity index 98% rename from packit/src/ChatActivity/inline/inlineBtns.py rename to packit/src/chatactivity/inline/InlineBtns.py index baf685c..0f1c541 100644 --- a/packit/src/ChatActivity/inline/inlineBtns.py +++ b/packit/src/chatactivity/inline/InlineBtns.py @@ -162,7 +162,7 @@ def _p(key): v = params.get(key) return str(v) if v is not None else "" - from .messageBuilder import build_plugin_message + from .MessageBuilder import build_plugin_message return build_plugin_message( _p("packit_name"), _p("packit_version"), _p("packit_author"), _p("packit_plugin_id"), _p("packit_repo_id"), translated_desc, @@ -178,7 +178,7 @@ def _do_translate_inline(message_object): # runs on background thread: translates only the description, rebuilds message with formatting try: from client_utils import edit_message - from ...utils.translation import _translate_text + from ...utils.Translation import _translate_text from java.util import Locale owner = message_object.messageOwner @@ -219,7 +219,7 @@ def set_pending(): def set_translated(): try: - from .messageBuilder import edit_message_with_entities + from .MessageBuilder import edit_message_with_entities if not edit_message_with_entities(message_object, rebuilt_text, rebuilt_entities): # last resort: at least put the translated text in place edit_message(message_object, text=rebuilt_text) @@ -340,7 +340,7 @@ def _do_send_file_inline(message_object, plugin_ref): # resolve plugins url from repo cache link = None try: - from ...ui.pluginsUpdates.fragment import _get_repos, _get_repo_plugins_url, _fetch_repo_plugins + from ...ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url, _fetch_repo_plugins repos = _get_repos() repo_url = None for r in repos: @@ -460,8 +460,8 @@ def __init__(self, plugin): def after_hooked_method(self, param): try: - from . import inlineState - if not inlineState.get_state(): + from . import InlineState + if not InlineState.get_state(): return message_object = param.thisObject if not _is_packit_inline_message(message_object): @@ -544,8 +544,8 @@ def hasSeparator(self, row_idx): class _DidPressCustomBotButtonHook(MethodHook): def before_hooked_method(self, param): try: - from . import inlineState - if not inlineState.get_state(): + from . import InlineState + if not InlineState.get_state(): return logx(f"inlineBtns: didPressCustomBotButton fired, args={len(param.args)}", True) diff --git a/packit/src/ChatActivity/inline/inlineState.py b/packit/src/chatactivity/inline/InlineState.py similarity index 100% rename from packit/src/ChatActivity/inline/inlineState.py rename to packit/src/chatactivity/inline/InlineState.py diff --git a/packit/src/ChatActivity/inline/messageBuilder.py b/packit/src/chatactivity/inline/MessageBuilder.py similarity index 99% rename from packit/src/ChatActivity/inline/messageBuilder.py rename to packit/src/chatactivity/inline/MessageBuilder.py index 5b8537a..61b87c6 100644 --- a/packit/src/ChatActivity/inline/messageBuilder.py +++ b/packit/src/chatactivity/inline/MessageBuilder.py @@ -86,7 +86,7 @@ def span(entity, start, text, **attrs): quote_start = offset if show_description and description: - from ...utils.markdown import parse as md_parse + from ...utils.Markdown import parse as md_parse parsed = md_parse(description) if parsed is not None: desc_text = parsed.text diff --git a/packit/src/ChatActivity/inline/__init__.py b/packit/src/chatactivity/inline/__init__.py similarity index 100% rename from packit/src/ChatActivity/inline/__init__.py rename to packit/src/chatactivity/inline/__init__.py diff --git a/packit/src/ChatActivity/LinksIcons/linksBottomSheet.py b/packit/src/chatactivity/linksicons/LinksBottomSheet.py similarity index 100% rename from packit/src/ChatActivity/LinksIcons/linksBottomSheet.py rename to packit/src/chatactivity/linksicons/LinksBottomSheet.py diff --git a/packit/src/ChatActivity/LinksIcons/__init__.py b/packit/src/chatactivity/linksicons/__init__.py similarity index 60% rename from packit/src/ChatActivity/LinksIcons/__init__.py rename to packit/src/chatactivity/linksicons/__init__.py index 159dda4..dfee257 100644 --- a/packit/src/ChatActivity/LinksIcons/__init__.py +++ b/packit/src/chatactivity/linksicons/__init__.py @@ -1,4 +1,4 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -from .linksBottomSheet import setup_links_buttons_hook \ No newline at end of file +from .LinksBottomSheet import setup_links_buttons_hook \ No newline at end of file diff --git a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py b/packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py similarity index 99% rename from packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py rename to packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py index f49e45e..4e423b9 100644 --- a/packit/src/ChatActivity/SecurityBottomSheets/hashBottomSheet.py +++ b/packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py @@ -26,7 +26,7 @@ logx(f"hashBottomSheet: import LayoutHelper error: {e}", False) -from ...utils.hashUtil import hashFile as _computeSha256 +from ...utils.HashUtil import hashFile as _computeSha256 def _extractPluginVersion(filePath: str) -> str | None: @@ -63,10 +63,10 @@ def _extractPluginId(filePath: str) -> str | None: def _loadCachedRepos() -> list: # [(name, pluginsUrl, repoId), …] for every repository with a usable cache - from ...utils import cachedRepos + from ...utils import CachedRepos result = [] - for rm_rid, cached in cachedRepos.all_cached(): - pluginsUrl = cachedRepos.plugins_url(rm_rid) + for rm_rid, cached in CachedRepos.all_cached(): + pluginsUrl = CachedRepos.plugins_url(rm_rid) if not pluginsUrl: continue meta = cached.get("repometa") or {} @@ -136,7 +136,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(strings["sec_plugin_no_link"])) return - from ...utils.paths import getPluginsDir + from ...utils.Paths import getPluginsDir pluginsDir = getPluginsDir() os.makedirs(pluginsDir, exist_ok=True) tempPath = os.path.join(pluginsDir, f".temp_{pluginId}.plugin") diff --git a/packit/src/ChatActivity/SecurityBottomSheets/signaturesBottomSheet.py b/packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py similarity index 99% rename from packit/src/ChatActivity/SecurityBottomSheets/signaturesBottomSheet.py rename to packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py index c554353..1b0c74e 100644 --- a/packit/src/ChatActivity/SecurityBottomSheets/signaturesBottomSheet.py +++ b/packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py @@ -800,7 +800,7 @@ def _showResults(results: dict, act): def onLearnMore(v): try: - from ...utils.localConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig LocalConfig.set("signatures", True) except Exception as ex: if DEBUG_LOGS: @@ -853,7 +853,7 @@ def onLearnMore(v): wrapper.addView(warningView, lp_warn) try: - from ...utils.localConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig showLearnMore = not LocalConfig.get("signatures", False) except Exception: showLearnMore = True diff --git a/packit/src/chatactivity/securitybottomsheets/__init__.py b/packit/src/chatactivity/securitybottomsheets/__init__.py new file mode 100644 index 0000000..b88323f --- /dev/null +++ b/packit/src/chatactivity/securitybottomsheets/__init__.py @@ -0,0 +1,5 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +from .SignaturesBottomSheet import setup_policy_button_hook +from .HashBottomSheet import setup_hash_button_hook \ No newline at end of file diff --git a/packit/src/deeplinks/contributors.py b/packit/src/deeplinks/Contributors.py similarity index 100% rename from packit/src/deeplinks/contributors.py rename to packit/src/deeplinks/Contributors.py diff --git a/packit/src/deeplinks/deepHandler.py b/packit/src/deeplinks/DeepHandler.py similarity index 75% rename from packit/src/deeplinks/deepHandler.py rename to packit/src/deeplinks/DeepHandler.py index a331cb9..8cfccd0 100644 --- a/packit/src/deeplinks/deepHandler.py +++ b/packit/src/deeplinks/DeepHandler.py @@ -14,28 +14,28 @@ except Exception: _dh_strings = None -from . import mainMenu -from . import settings -from . import deeplinkMenu -from . import other -from . import contributors -from . import docs -from . import forum -from . import repo -from . import install -from . import update -from . import problems -from . import pkill -from . import plugin -from .secret import premium -from .secret import terraria -from .secret import aytist -from . import suggestion +from . import MainMenu +from . import Settings +from . import DeeplinkMenu +from . import Other +from . import Contributors +from . import Docs +from . import Forum +from . import Repo +from . import Install +from . import Update +from . import Problems +from . import Pkill +from . import Plugin +from .secret import Premium +from .secret import Terraria +from .secret import Aytist +from . import Suggestion class PackItDeeplinkHook(MethodHook): def __init__(self, plugin): - self.plugin = plugin + self.plugin = Plugin self.pending_intent = None self.pending_param = None self.is_processing = False @@ -67,23 +67,23 @@ def before_hooked_method(self, param): def show_packit_notification(self, url): try: - mainMenu.handle(url) - settings.handle(url, self.plugin) - deeplinkMenu.handle(url) - other.handle(url) - contributors.handle(url) - docs.handle(url) - forum.handle(url) - repo.handle(url, self.plugin.repoManager) - install.handle(url, self.plugin.repoManager) - update.handle(url, self.plugin.repoManager) - problems.handle(url) - pkill.handle(url) - plugin.handle(url, self.plugin.repoManager) - premium.handle(url) - terraria.handle(url) - aytist.handle(url) - suggestion.handle(url, self.plugin) + MainMenu.handle(url) + Settings.handle(url, self.plugin) + DeeplinkMenu.handle(url) + Other.handle(url) + Contributors.handle(url) + Docs.handle(url) + Forum.handle(url) + Repo.handle(url, self.plugin.repoManager) + Install.handle(url, self.plugin.repoManager) + Update.handle(url, self.plugin.repoManager) + Problems.handle(url) + Pkill.handle(url) + Plugin.handle(url, self.plugin.repoManager) + Premium.handle(url) + Terraria.handle(url) + Aytist.handle(url) + Suggestion.handle(url, self.plugin) except Exception as e: logx(f"[PackIt] Error showing notification: {e}", False) try: @@ -126,7 +126,7 @@ def setup_deeplink_hook(plugin): find_class("java.lang.Boolean").TYPE ) method.setAccessible(True) - return plugin.hook_method(method, PackItDeeplinkHook(plugin)) + return Plugin.hook_method(method, PackItDeeplinkHook(Plugin)) except Exception as e: logx(f"[PackIt] Error setting up deeplink hook: {e}", False) return None \ No newline at end of file diff --git a/packit/src/deeplinks/deeplinkMenu.py b/packit/src/deeplinks/DeeplinkMenu.py similarity index 100% rename from packit/src/deeplinks/deeplinkMenu.py rename to packit/src/deeplinks/DeeplinkMenu.py diff --git a/packit/src/deeplinks/docs.py b/packit/src/deeplinks/Docs.py similarity index 100% rename from packit/src/deeplinks/docs.py rename to packit/src/deeplinks/Docs.py diff --git a/packit/src/deeplinks/forum.py b/packit/src/deeplinks/Forum.py similarity index 92% rename from packit/src/deeplinks/forum.py rename to packit/src/deeplinks/Forum.py index 60675a7..c0b9331 100644 --- a/packit/src/deeplinks/forum.py +++ b/packit/src/deeplinks/Forum.py @@ -7,7 +7,7 @@ from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url): diff --git a/packit/src/deeplinks/install.py b/packit/src/deeplinks/Install.py similarity index 96% rename from packit/src/deeplinks/install.py rename to packit/src/deeplinks/Install.py index ecb113b..69d9ddc 100644 --- a/packit/src/deeplinks/install.py +++ b/packit/src/deeplinks/Install.py @@ -2,19 +2,19 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils import cachedRepos -from ..utils.bulletins import factory as _pbf +from ..utils import CachedRepos +from ..utils.Bulletins import factory as _pbf from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread from urllib.parse import urlparse, parse_qs -from ..core import install_plugin, install_icon_pack -from ..ui.PluginListActivity.fragment import InstallUI +from ..Core import install_plugin, install_icon_pack +from ..ui.pluginlistactivity.Fragment import InstallUI try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() import requests import json import os @@ -23,7 +23,7 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() # install&repo=: required: repo — optional: plugin, icon, version _INSTALL_REQUIRED = {"repo"} @@ -42,7 +42,7 @@ def _findRepo(repoManager, repoId: str) -> dict | None: def _resolvePluginsUrl(repo: dict) -> str: - return cachedRepos.plugins_url(repo) + return CachedRepos.plugins_url(repo) def handle(url, repoManager): @@ -108,7 +108,7 @@ def _is_version_ok(app_ver_expr: str) -> bool: if not app_ver_expr: return True try: - from ..utils.app_version import check_app_version + from ..utils.AppVersion import check_app_version return check_app_version(app_ver_expr) except Exception: return True @@ -117,7 +117,7 @@ def _is_version_ok(app_ver_expr: str) -> bool: def _find_best_compatible(plugin: dict) -> dict | None: # returns a plugin dict with link/app_version set to best available compatible version # checks root version first (newest), then versions dict descending - from ..ui.PluginActivity.versionPicker import _build_version_entries + from ..ui.pluginactivity.VersionPicker import _build_version_entries entries = _build_version_entries(plugin) for e in entries: if _is_version_ok(e["app_version"]): @@ -140,7 +140,7 @@ def _show_incompatible_sheet(requested_version: str, compatible_plugin: dict, al from org.telegram.ui.ActionBar import BottomSheet, Theme from org.telegram.ui.Components import LayoutHelper from org.telegram.messenger import AndroidUtilities - from ..core import install_plugin + from ..Core import install_plugin fragment = get_last_fragment() if not fragment: @@ -360,7 +360,7 @@ def _show_loading_bulletin(): def _resolveIconsUrl(repo: dict) -> str: - return cachedRepos.icons_url(repo) + return CachedRepos.icons_url(repo) def _handleInstallIconPack(repo: dict, iconId: str): diff --git a/packit/src/deeplinks/mainMenu.py b/packit/src/deeplinks/MainMenu.py similarity index 89% rename from packit/src/deeplinks/mainMenu.py rename to packit/src/deeplinks/MainMenu.py index 17c5cc4..2f637da 100644 --- a/packit/src/deeplinks/mainMenu.py +++ b/packit/src/deeplinks/MainMenu.py @@ -7,7 +7,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url): diff --git a/packit/src/deeplinks/other.py b/packit/src/deeplinks/Other.py similarity index 100% rename from packit/src/deeplinks/other.py rename to packit/src/deeplinks/Other.py diff --git a/packit/src/deeplinks/pkill.py b/packit/src/deeplinks/Pkill.py similarity index 94% rename from packit/src/deeplinks/pkill.py rename to packit/src/deeplinks/Pkill.py index 9b21a26..f1f253c 100644 --- a/packit/src/deeplinks/pkill.py +++ b/packit/src/deeplinks/Pkill.py @@ -13,7 +13,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def kill_process(): time.sleep(1) diff --git a/packit/src/deeplinks/plugin.py b/packit/src/deeplinks/Plugin.py similarity index 95% rename from packit/src/deeplinks/plugin.py rename to packit/src/deeplinks/Plugin.py index 878a6fe..f5fec4c 100644 --- a/packit/src/deeplinks/plugin.py +++ b/packit/src/deeplinks/Plugin.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils import cachedRepos +from ..utils import CachedRepos from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -26,7 +26,7 @@ def _resolvePluginsUrl(repo: dict) -> str: - return cachedRepos.plugins_url(repo) + return CachedRepos.plugins_url(repo) def _findRepo(repoManager, repoId: str) -> dict | None: @@ -103,7 +103,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(str(strings("dl_plugin_not_found", plugin_id=pluginId)))) return - from ..ui.PluginListActivity.fragment import InstallUI + from ..ui.pluginlistactivity.Fragment import InstallUI class _FakePlugin: def __init__(self, rm): @@ -112,7 +112,7 @@ def __init__(self, rm): installUI = InstallUI(_FakePlugin(repoManager)) def _show(_p=plugin, _all=all_plugins, _rid=repoId): - from ..ui.PluginActivity.fragment import show_plugin_profile + from ..ui.pluginactivity.Fragment import show_plugin_profile show_plugin_profile(_p, installUI, _all, repo_id=_rid) run_on_ui_thread(_show) diff --git a/packit/src/deeplinks/problems.py b/packit/src/deeplinks/Problems.py similarity index 92% rename from packit/src/deeplinks/problems.py rename to packit/src/deeplinks/Problems.py index 51ef74b..56911ab 100644 --- a/packit/src/deeplinks/problems.py +++ b/packit/src/deeplinks/Problems.py @@ -7,7 +7,7 @@ from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url): diff --git a/packit/src/deeplinks/repo.py b/packit/src/deeplinks/Repo.py similarity index 95% rename from packit/src/deeplinks/repo.py rename to packit/src/deeplinks/Repo.py index 15092d6..0a66daa 100644 --- a/packit/src/deeplinks/repo.py +++ b/packit/src/deeplinks/Repo.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread, OnClickListener @@ -16,7 +16,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from hook_utils import find_class try: from org.telegram.messenger import R as R_tg, ApplicationLoader @@ -26,17 +26,17 @@ from org.telegram.ui.Stories.recorder import ButtonWithCounterView except Exception as e: import android_utils as _au; _au.log(f"repo deeplink: import tg classes failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.utils.text import LocaleUtils except Exception as e: import android_utils as _au; _au.log(f"repo deeplink: import LocaleUtils failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from urllib.parse import urlparse, parse_qs import requests import json from ..network import Storage -from ..utils import cachedRepos +from ..utils import CachedRepos BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") @@ -55,13 +55,13 @@ def _sheet_chip(act, text: str): # the same pill the source cards use, so the sheet that adds a source and # the card it becomes are recognisably the same thing - from ..ui.ReposActivity.card import _chip - from ..ui.ReposActivity.repoIcon import accent_for + from ..ui.reposactivity.Card import _chip + from ..ui.reposactivity.RepoIcon import accent_for return _chip(act, text, accent_for({})) def _sheet_chip_lp(margin_dp=3): - from ..ui.ReposActivity.card import _ROW_H + from ..ui.reposactivity.Card import _ROW_H lp = LinearLayout.LayoutParams(-2, AndroidUtilities.dp(_ROW_H)) lp.leftMargin = AndroidUtilities.dp(margin_dp) lp.rightMargin = AndroidUtilities.dp(margin_dp) @@ -131,9 +131,9 @@ def fetch_task(): repometa = data.get("repometa") # cached now, so the sheet's avatar and everything the # source screen shows are there the moment it is added - cachedRepos.write(repometa.get("rm_rid"), data) + CachedRepos.write(repometa.get("rm_rid"), data) - plugins_url = cachedRepos.plugins_url(repometa.get("rm_rid"), link) + plugins_url = CachedRepos.plugins_url(repometa.get("rm_rid"), link) entries, list_error = Storage.fetch_plugins(plugins_url) if list_error: logx(f"repo deeplink: plugin count unavailable: {list_error}", True) @@ -185,7 +185,7 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): # picture for every repository in existence, which told the reader # nothing about the one they were about to add. try: - from ..ui.ReposActivity.repoIcon import build_icon_view + from ..ui.reposactivity.RepoIcon import build_icon_view icon_view = build_icon_view( act, {"id": rm_rid, "name": rm_name, "url": link}, 76, 22, rm_icon) linear.addView(icon_view, LayoutHelper.createLinear( @@ -299,7 +299,7 @@ def onClick(self, v): repoManager.setRepositories(currentRepos) BulletinHelper.show_success(strings.repo_add_success) try: - from ..ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ..ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Repositories") except Exception as e: logx(f"repo deeplink: achievements increment error: {e}", False) diff --git a/packit/src/deeplinks/settings.py b/packit/src/deeplinks/Settings.py similarity index 90% rename from packit/src/deeplinks/settings.py rename to packit/src/deeplinks/Settings.py index 27fe079..d49906c 100644 --- a/packit/src/deeplinks/settings.py +++ b/packit/src/deeplinks/Settings.py @@ -7,12 +7,12 @@ from com.exteragram.messenger.plugins import PluginsController except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins import PluginsController failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.ui import PluginSettingsActivity except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.ui import PluginSettingsActivity failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url, plugin): diff --git a/packit/src/deeplinks/suggestion.py b/packit/src/deeplinks/Suggestion.py similarity index 92% rename from packit/src/deeplinks/suggestion.py rename to packit/src/deeplinks/Suggestion.py index 0d25dfc..e56f045 100644 --- a/packit/src/deeplinks/suggestion.py +++ b/packit/src/deeplinks/Suggestion.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils import cachedRepos +from ..utils import CachedRepos from urllib.parse import urlparse, parse_qs from android_utils import run_on_ui_thread from client_utils import get_last_fragment @@ -16,7 +16,7 @@ def _load_repomap(rm_rid: str): - return cachedRepos.read(rm_rid) + return CachedRepos.read(rm_rid) def _has_required_fields(data: dict) -> bool: @@ -57,7 +57,7 @@ def handle(url: str, plugin=None): def _open_fragment(data: dict, plugin=None): try: - from ..ui.suggest.fragment import show_suggest_fragment + from ..ui.suggest.Fragment import show_suggest_fragment show_suggest_fragment(data, plugin) except Exception as e: logx(f"suggestion deeplink: _open_fragment error: {e}", False) \ No newline at end of file diff --git a/packit/src/deeplinks/update.py b/packit/src/deeplinks/Update.py similarity index 95% rename from packit/src/deeplinks/update.py rename to packit/src/deeplinks/Update.py index b986002..3453a4f 100644 --- a/packit/src/deeplinks/update.py +++ b/packit/src/deeplinks/Update.py @@ -3,7 +3,7 @@ from packutil import logx from ..network import Storage -from ..utils import cachedRepos +from ..utils import CachedRepos from ui.bulletin import BulletinHelper from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread @@ -11,12 +11,12 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from urllib.parse import urlparse, parse_qs import requests import json @@ -60,7 +60,7 @@ def task(): changed = True logx(f"update deeplink: set id='{rmRid}' for repo '{repo.get('name')}'", True) - cachedRepos.write(rmRid, data) + CachedRepos.write(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) except Exception as e: logx(f"update deeplink: error for {url}: {e}", False) @@ -105,7 +105,7 @@ def task(): return repometa = data.get("repometa") rmRid = repometa.get("rm_rid") - cachedRepos.write(rmRid, data) + CachedRepos.write(rmRid, data) logx(f"update deeplink: updated cache for '{rmRid}'", True) idx = next((i for i, rp in enumerate(repos) if rp.get("id") == repoId), None) diff --git a/packit/src/deeplinks/__init__.py b/packit/src/deeplinks/__init__.py index 434a6c1..a96dbae 100644 --- a/packit/src/deeplinks/__init__.py +++ b/packit/src/deeplinks/__init__.py @@ -1,6 +1,6 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -from .deepHandler import setup_deeplink_hook +from .DeepHandler import setup_deeplink_hook __all__ = ['setup_deeplink_hook'] \ No newline at end of file diff --git a/packit/src/deeplinks/secret/aytist.py b/packit/src/deeplinks/secret/Aytist.py similarity index 98% rename from packit/src/deeplinks/secret/aytist.py rename to packit/src/deeplinks/secret/Aytist.py index 3b4c521..23afd3c 100644 --- a/packit/src/deeplinks/secret/aytist.py +++ b/packit/src/deeplinks/secret/Aytist.py @@ -22,7 +22,7 @@ def handle(url): if url != "tg://packit?aytist": return try: - from ...ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret unlock_secret("aytist") run_on_ui_thread(_startSpawnChain) except Exception as e: diff --git a/packit/src/deeplinks/secret/premium.py b/packit/src/deeplinks/secret/Premium.py similarity index 93% rename from packit/src/deeplinks/secret/premium.py rename to packit/src/deeplinks/secret/Premium.py index a62337b..1ff14db 100644 --- a/packit/src/deeplinks/secret/premium.py +++ b/packit/src/deeplinks/secret/Premium.py @@ -9,7 +9,7 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url): @@ -17,7 +17,7 @@ def handle(url): return try: _playMaxVolume() - from ...ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret unlock_secret("premium") except Exception as e: logx(f"deeplinks.premium: error: {e}", False) diff --git a/packit/src/deeplinks/secret/terraria.py b/packit/src/deeplinks/secret/Terraria.py similarity index 94% rename from packit/src/deeplinks/secret/terraria.py rename to packit/src/deeplinks/secret/Terraria.py index d0180f4..fe43444 100644 --- a/packit/src/deeplinks/secret/terraria.py +++ b/packit/src/deeplinks/secret/Terraria.py @@ -9,7 +9,7 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def handle(url): @@ -18,7 +18,7 @@ def handle(url): try: _playMaxVolume() logx(f"deeplinks.terraria: calling unlock_secret", True) - from ...ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret unlock_secret("terraria") logx(f"deeplinks.terraria: unlock_secret returned", True) except Exception as e: diff --git a/packit/src/DialogsActivity/btnCAB.py b/packit/src/dialogsactivity/BtnCAB.py similarity index 98% rename from packit/src/DialogsActivity/btnCAB.py rename to packit/src/dialogsactivity/BtnCAB.py index f30aeae..bf7b402 100644 --- a/packit/src/DialogsActivity/btnCAB.py +++ b/packit/src/dialogsactivity/BtnCAB.py @@ -9,13 +9,13 @@ from org.telegram.ui import ChatActivity except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui import ChatActivity failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from hook_utils import find_class try: from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() class BtnCAB: diff --git a/packit/src/DialogsActivity/btnPluginsMenu.py b/packit/src/dialogsactivity/BtnPluginsMenu.py similarity index 97% rename from packit/src/DialogsActivity/btnPluginsMenu.py rename to packit/src/dialogsactivity/BtnPluginsMenu.py index 258caff..d0f9322 100644 --- a/packit/src/DialogsActivity/btnPluginsMenu.py +++ b/packit/src/dialogsactivity/BtnPluginsMenu.py @@ -7,7 +7,7 @@ from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() _SETTINGS_LINK = "https://t.me/exteraSettings?s=mainMenuSettings" diff --git a/packit/src/DialogsActivity/buildNotCorrect.py b/packit/src/dialogsactivity/BuildNotCorrect.py similarity index 98% rename from packit/src/DialogsActivity/buildNotCorrect.py rename to packit/src/dialogsactivity/BuildNotCorrect.py index a46f25a..81e0279 100644 --- a/packit/src/DialogsActivity/buildNotCorrect.py +++ b/packit/src/dialogsactivity/BuildNotCorrect.py @@ -22,7 +22,7 @@ def _getDismissedHash() -> str: try: - from ..utils.localConfig import LocalConfig + from ..utils.LocalConfig import LocalConfig return LocalConfig.get(_HASH_CONFIG_KEY, "") except Exception as e: logx(f"buildNotCorrect: _getDismissedHash error: {e}", False) @@ -31,7 +31,7 @@ def _getDismissedHash() -> str: def _saveDismissedHash(hashVal: str): try: - from ..utils.localConfig import LocalConfig + from ..utils.LocalConfig import LocalConfig LocalConfig.set(_HASH_CONFIG_KEY, hashVal) except Exception as e: logx(f"buildNotCorrect: _saveDismissedHash error: {e}", False) @@ -224,7 +224,7 @@ def onClick(self, v): def _checkAndShow(): try: - from ..utils.buildInfo import ( + from ..utils.BuildInfo import ( getBuildClientPkg, getBuildClientName, getBuildStaticVersion, getCurrClientPkg, getCurrClientName, diff --git a/packit/src/DialogsActivity/button.py b/packit/src/dialogsactivity/Button.py similarity index 90% rename from packit/src/DialogsActivity/button.py rename to packit/src/dialogsactivity/Button.py index 02f3ba6..edfa0a7 100644 --- a/packit/src/DialogsActivity/button.py +++ b/packit/src/dialogsactivity/Button.py @@ -8,21 +8,21 @@ from com.exteragram.messenger.plugins import PluginsController except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins import PluginsController failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.ui import PluginSettingsActivity except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.ui import PluginSettingsActivity failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from .btnCAB import BtnCAB -from .btnPluginsMenu import BtnPluginsMenu -from .chatDialogButton import ChatDialogButton +from .BtnCAB import BtnCAB +from .BtnPluginsMenu import BtnPluginsMenu +from .ChatDialogButton import ChatDialogButton class ChatButton(BtnCAB, BtnPluginsMenu, ChatDialogButton): diff --git a/packit/src/DialogsActivity/chatDialogButton.py b/packit/src/dialogsactivity/ChatDialogButton.py similarity index 99% rename from packit/src/DialogsActivity/chatDialogButton.py rename to packit/src/dialogsactivity/ChatDialogButton.py index 74448dd..41ee1a4 100644 --- a/packit/src/DialogsActivity/chatDialogButton.py +++ b/packit/src/dialogsactivity/ChatDialogButton.py @@ -161,10 +161,10 @@ def _open(): logx(f"ChatDialogButton: open settings error: {e}", False) run_on_ui_thread(_open) elif m == 2: - from ..ui.IconsListActivity.fragment import InstallIconsUI + from ..ui.iconslistactivity.Fragment import InstallIconsUI run_on_ui_thread(lambda: InstallIconsUI(plugin).open()) else: - from ..ui.PluginListActivity.fragment import InstallUI + from ..ui.pluginlistactivity.Fragment import InstallUI run_on_ui_thread(lambda: InstallUI(plugin).open()) except Exception as e: logx(f"ChatDialogButton: onClick error: {e}", False) diff --git a/packit/src/DialogsActivity/PackitUpdateSheet.py b/packit/src/dialogsactivity/PackitUpdateSheet.py similarity index 98% rename from packit/src/DialogsActivity/PackitUpdateSheet.py rename to packit/src/dialogsactivity/PackitUpdateSheet.py index 15eca14..dfd4206 100644 --- a/packit/src/DialogsActivity/PackitUpdateSheet.py +++ b/packit/src/dialogsactivity/PackitUpdateSheet.py @@ -69,7 +69,7 @@ def _get_current_version() -> str: def _get_dismissed_ver() -> str: try: - from ..utils.localConfig import LocalConfig + from ..utils.LocalConfig import LocalConfig v = LocalConfig.get("update_dismissed_ver", "") logx(f"updateSheet: dismissed_ver='{v}'", True) return v @@ -80,7 +80,7 @@ def _get_dismissed_ver() -> str: def _save_dismissed_ver(ver: str): try: - from ..utils.localConfig import LocalConfig + from ..utils.LocalConfig import LocalConfig LocalConfig.set("update_dismissed_ver", ver) logx(f"updateSheet: saved dismissed_ver='{ver}'", True) except Exception as e: @@ -127,7 +127,7 @@ def _show_update_sheet(new_ver: str, changelog: str, sticker: str, download_url: iv.getImageReceiver().setCrossfadeWithOldImage(True) except Exception as e: logx(f"updateSheet: setCrossfadeWithOldImage error: {e}", False) - from ..utils.stickers import load_sticker + from ..utils.Stickers import load_sticker load_sticker(iv, sticker, sticker_size_dp) linear.addView(iv, LayoutHelper.createLinear( sticker_size_dp, sticker_size_dp, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0 diff --git a/packit/src/DialogsActivity/pillWidget.py b/packit/src/dialogsactivity/PillWidget.py similarity index 99% rename from packit/src/DialogsActivity/pillWidget.py rename to packit/src/dialogsactivity/PillWidget.py index c12162a..a1c9cf1 100644 --- a/packit/src/DialogsActivity/pillWidget.py +++ b/packit/src/dialogsactivity/PillWidget.py @@ -429,7 +429,7 @@ def _open_settings(plugin): def _open_install(plugin): try: - from ..ui.PluginListActivity.fragment import InstallUI + from ..ui.pluginlistactivity.Fragment import InstallUI InstallUI(plugin).open() except Exception as e: logx(f"PillWidget: _open_install error: {e}", False) @@ -437,7 +437,7 @@ def _open_install(plugin): def _open_icons(plugin): try: - from ..ui.IconsListActivity.fragment import InstallIconsUI + from ..ui.iconslistactivity.Fragment import InstallIconsUI InstallIconsUI(plugin).open() except Exception as e: logx(f"PillWidget: _open_icons error: {e}", False) diff --git a/packit/src/DialogsActivity/updatesWidget.py b/packit/src/dialogsactivity/UpdatesWidget.py similarity index 98% rename from packit/src/DialogsActivity/updatesWidget.py rename to packit/src/dialogsactivity/UpdatesWidget.py index e358258..3161c25 100644 --- a/packit/src/DialogsActivity/updatesWidget.py +++ b/packit/src/dialogsactivity/UpdatesWidget.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.netQueue import run_io +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 @@ -206,7 +206,7 @@ def _prefetch_and_register(plugin): def task(): try: - from ..ui.pluginsUpdates.fragment import _check_updates, _filter_ignored + from ..ui.pluginsupdates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) _updates_count[0] = len(updates) _updates_list[0] = updates @@ -382,7 +382,7 @@ def _on_click(plugin, pill): def _install_single(plugin, item): # installs the single available update directly, then re-checks on success from client_utils import run_on_queue - from ..ui.pluginsUpdates.fragment import _get_repos, _get_repo_plugins_url + from ..ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url import requests as _req pid = str(item.get("id") or "") @@ -392,7 +392,7 @@ def _on_installed(installed_pid): if installed_pid != pid: return try: - from ..core import remove_install_listener + from ..Core import remove_install_listener remove_install_listener(_on_installed) except Exception as e: logx(f"UpdatesWidget: remove_install_listener error: {e}", False) @@ -431,7 +431,7 @@ def task(): if not plugin_data: logx(f"UpdatesWidget: _install_single plugin '{pid}' not found in repo", True) return - from ..core import install_plugin, add_install_listener + from ..Core import install_plugin, add_install_listener add_install_listener(_on_installed) from android_utils import run_on_ui_thread run_on_ui_thread(lambda: install_plugin(plugin_data, all_plugins=all_plugins, rm_rid=repo_id)) @@ -443,7 +443,7 @@ def task(): def _open_updates(plugin): try: - from ..ui.pluginsUpdates.fragment import show_updates_fragment + from ..ui.pluginsupdates.Fragment import show_updates_fragment show_updates_fragment(plugin) except Exception as e: logx(f"UpdatesWidget: _open_updates error: {e}", False) @@ -477,7 +477,7 @@ def finish_loading(count, updates): def task(): try: - from ..ui.pluginsUpdates.fragment import _check_updates, _filter_ignored + from ..ui.pluginsupdates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) count = len(updates) run_on_ui_thread(lambda: finish_loading(count, updates)) diff --git a/packit/src/DialogsActivity/__init__.py b/packit/src/dialogsactivity/__init__.py similarity index 100% rename from packit/src/DialogsActivity/__init__.py rename to packit/src/dialogsactivity/__init__.py diff --git a/packit/src/network/Storage.py b/packit/src/network/Storage.py index 4c2746f..9783896 100644 --- a/packit/src/network/Storage.py +++ b/packit/src/network/Storage.py @@ -13,7 +13,7 @@ # from here. # # Where a fetched repomap is kept, and everything read back out of it, is -# utils/cachedRepos — this module does not touch that file. +# utils/CachedRepos — this module does not touch that file. from packutil import logx import json @@ -21,8 +21,8 @@ import requests -from ..utils import jsonx as _jsonx -from ..utils.paths import getRepoIconCachePath, getRepoIconCacheDir +from ..utils import Jsonx as _jsonx +from ..utils.Paths import getRepoIconCachePath, getRepoIconCacheDir # repositories are served from github raw and the like; some of them log this HEADERS = {"User-Agent": "PackIt/1.0 (Android; github.com/shareui/packit)"} @@ -176,7 +176,7 @@ def peek_icon(url: str, px: int): def load_icon(url: str, px: int): """memory -> disk -> network, decoded to a px-sized bitmap. Off the ui thread.""" - from ..utils import imagePool + from ..utils import ImagePool bmp = peek_icon(url, px) if bmp is not None: @@ -192,7 +192,7 @@ def load_icon(url: str, px: int): data = None if not data: - data = imagePool.fetch(url) + data = ImagePool.fetch(url) if not data: return None try: @@ -202,7 +202,7 @@ def load_icon(url: str, px: int): except Exception as e: logx(f"Storage: icon cache write failed: {e}", True) - bmp = imagePool.decode(data, px, imagePool.looks_like_svg(url, data)) + bmp = ImagePool.decode(data, px, ImagePool.looks_like_svg(url, data)) if bmp is None: # a corrupted cache entry would keep failing forever try: diff --git a/packit/src/other/badges.py b/packit/src/other/Badges.py similarity index 97% rename from packit/src/other/badges.py rename to packit/src/other/Badges.py index 20c6e29..2da973e 100644 --- a/packit/src/other/badges.py +++ b/packit/src/other/Badges.py @@ -128,7 +128,7 @@ def setup_hooks(self): # primary path: precompiled Kotlin dex (config fetch + cache + hooks # all live in packit/dex//badges.dex, source in /kotlin/) try: - from ..dexLoader import loadBadges + from ..DexLoader import loadBadges if loadBadges(self.context, enabled): self._dex_loaded = True logx("[Packit Badges] using precompiled dex", True) @@ -182,21 +182,21 @@ def _install_hooks(self): logx(f"[Packit Badges] hook install error: {e}", False) try: - from .chatBadge import setup_chat_badge_hook + from .ChatBadge import setup_chat_badge_hook chat_refs = setup_chat_badge_hook(self.plugin, _lookup) self._hook_refs.extend(chat_refs) except Exception as e: logx(f"[Packit Badges] chat hook error: {e}", False) try: - from .chatTitleIcon import setup_title_icon_hook + from .ChatTitleIcon import setup_title_icon_hook title_refs = setup_title_icon_hook(self.plugin, _lookup) self._hook_refs.extend(title_refs) except Exception as e: logx(f"[Packit Badges] title icon hook error: {e}", False) try: - from .profileTitleIcon import setup_profile_title_icon_hook + from .ProfileTitleIcon import setup_profile_title_icon_hook profile_refs = setup_profile_title_icon_hook(self.plugin, _lookup) self._hook_refs.extend(profile_refs) except Exception as e: @@ -206,7 +206,7 @@ def cleanup(self): try: if self._dex_loaded: try: - from ..dexLoader import unloadBadges + from ..DexLoader import unloadBadges unloadBadges() except Exception as e: logx(f"[Packit Badges] dex unload error: {e}", False) diff --git a/packit/src/other/chatBadge.py b/packit/src/other/ChatBadge.py similarity index 100% rename from packit/src/other/chatBadge.py rename to packit/src/other/ChatBadge.py diff --git a/packit/src/other/chatTitleIcon.py b/packit/src/other/ChatTitleIcon.py similarity index 100% rename from packit/src/other/chatTitleIcon.py rename to packit/src/other/ChatTitleIcon.py diff --git a/packit/src/other/everyone.py b/packit/src/other/Everyone.py similarity index 98% rename from packit/src/other/everyone.py rename to packit/src/other/Everyone.py index cb1e0d6..91b2b11 100644 --- a/packit/src/other/everyone.py +++ b/packit/src/other/Everyone.py @@ -23,7 +23,7 @@ def _get_cache_path() -> str: - from ..utils.paths import getCacheRoot + from ..utils.Paths import getCacheRoot cache_dir = getCacheRoot() os.makedirs(cache_dir, exist_ok=True) return os.path.join(cache_dir, _CACHE_FILENAME) diff --git a/packit/src/other/isBeta.py b/packit/src/other/IsBeta.py similarity index 98% rename from packit/src/other/isBeta.py rename to packit/src/other/IsBeta.py index c35bb89..8945ea7 100644 --- a/packit/src/other/isBeta.py +++ b/packit/src/other/IsBeta.py @@ -18,8 +18,8 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from ..utils.localConfig import LocalConfig + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from ..utils.LocalConfig import LocalConfig BETA = False _COUNTDOWN_SEC = 5 diff --git a/packit/src/other/profileTitleIcon.py b/packit/src/other/ProfileTitleIcon.py similarity index 100% rename from packit/src/other/profileTitleIcon.py rename to packit/src/other/ProfileTitleIcon.py diff --git a/packit/src/other/text.py b/packit/src/other/Text.py similarity index 94% rename from packit/src/other/text.py rename to packit/src/other/Text.py index d691f6f..0782525 100644 --- a/packit/src/other/text.py +++ b/packit/src/other/Text.py @@ -16,7 +16,7 @@ def check_message(text: str): return lower = text.lower().strip() try: - from ..ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ..ui.achievementsactivity.service.AchivementsEngine import unlock_secret if lower == _TRIGGER_TALKING: unlock_secret("talking_about_you") elif lower == _TRIGGER_UTILS: diff --git a/packit/src/scl/doc.py b/packit/src/scl/Doc.py similarity index 94% rename from packit/src/scl/doc.py rename to packit/src/scl/Doc.py index c3c85d3..3d5e2b3 100644 --- a/packit/src/scl/doc.py +++ b/packit/src/scl/Doc.py @@ -1,6 +1,6 @@ -from . import native as _native -from .errors import TomlError -from .value import Value, ListBuilder, StructBuilder, _makeVal +from . import Native as _native +from .Errors import TomlError +from .Value import Value, ListBuilder, StructBuilder, _makeVal class Doc: # precondition: ptr is a valid doc pointer from the native layer, or None for empty diff --git a/packit/src/scl/errors.py b/packit/src/scl/Errors.py similarity index 100% rename from packit/src/scl/errors.py rename to packit/src/scl/Errors.py diff --git a/packit/src/scl/native.py b/packit/src/scl/Native.py similarity index 99% rename from packit/src/scl/native.py rename to packit/src/scl/Native.py index 9266c0c..137c1bd 100644 --- a/packit/src/scl/native.py +++ b/packit/src/scl/Native.py @@ -2,8 +2,8 @@ import platform def _soPath() -> str: - from ..utils.paths import _filesDir - from ..nativeLoader import detectArch + from ..utils.Paths import _filesDir + from ..NativeLoader import detectArch arch = detectArch() return _filesDir() + f"/plugins/ElyxPlugins/shareui_packit/packit/native/{arch}/libscl.so" diff --git a/packit/src/scl/opts.py b/packit/src/scl/Opts.py similarity index 93% rename from packit/src/scl/opts.py rename to packit/src/scl/Opts.py index 6821314..781cfdf 100644 --- a/packit/src/scl/opts.py +++ b/packit/src/scl/Opts.py @@ -1,7 +1,7 @@ import ctypes from dataclasses import dataclass, field -from . import native as _native +from . import Native as _native @dataclass class ParseOpts: diff --git a/packit/src/scl/scl.py b/packit/src/scl/Scl.py similarity index 92% rename from packit/src/scl/scl.py rename to packit/src/scl/Scl.py index b823a62..5f8b6b1 100644 --- a/packit/src/scl/scl.py +++ b/packit/src/scl/Scl.py @@ -1,8 +1,8 @@ -from . import native as _native -from .errors import ParseError, TomlError -from .opts import ParseOpts -from .doc import Doc -from .value import Value +from . import Native as _native +from .Errors import ParseError, TomlError +from .Opts import ParseOpts +from .Doc import Doc +from .Value import Value NULL = _native.NULL STRING = _native.STRING diff --git a/packit/src/scl/value.py b/packit/src/scl/Value.py similarity index 99% rename from packit/src/scl/value.py rename to packit/src/scl/Value.py index 2303b2d..e9d4f5e 100644 --- a/packit/src/scl/value.py +++ b/packit/src/scl/Value.py @@ -1,4 +1,4 @@ -from . import native as _native +from . import Native as _native class Value: def __init__(self, ptr, doc): diff --git a/packit/src/scl/__init__.py b/packit/src/scl/__init__.py index 50eca64..c23df1e 100644 --- a/packit/src/scl/__init__.py +++ b/packit/src/scl/__init__.py @@ -1,4 +1,4 @@ -from .scl import ( +from .Scl import ( parse, parseFile, version, @@ -17,7 +17,7 @@ STRUCT, UNION, ) -from .errors import ParseError, TomlError -from .opts import ParseOpts -from .doc import Doc -from .value import Value +from .Errors import ParseError, TomlError +from .Opts import ParseOpts +from .Doc import Doc +from .Value import Value diff --git a/packit/src/SettingsActivity/debugItems.py b/packit/src/settingsactivity/DebugItems.py similarity index 97% rename from packit/src/SettingsActivity/debugItems.py rename to packit/src/settingsactivity/DebugItems.py index 2b57aa7..712ea2e 100644 --- a/packit/src/SettingsActivity/debugItems.py +++ b/packit/src/settingsactivity/DebugItems.py @@ -22,7 +22,7 @@ def _test_native_error(): _ = 123 / 0 except Exception as e: logx(f"debugItems: test native error triggered: {e}", False) - from ..nativeLoader import showNativeErrorSheet + from ..NativeLoader import showNativeErrorSheet showNativeErrorSheet("libpackitdb.so", str(e)) @@ -31,7 +31,7 @@ def _migrate_achievements(): import os import ctypes import zlib - from ..ui.AchievementsActivity.service.AchivementsEngine import ( + from ..ui.achievementsactivity.service.AchivementsEngine import ( _get_current_account_id, _get_configs_dir, _save_account, _db_to_dict, _lib, _BUF_SIZE ) @@ -263,7 +263,7 @@ def _dump_class_info(class_name: str, methods: bool = True, fields: bool = True) def _check_build_info(): try: - from ..utils.buildInfo import ( + from ..utils.BuildInfo import ( getBuildClientName, getBuildClientPkg, getCurrClientName, getCurrClientPkg, getBuildStaticVersion, getClientVersion @@ -281,7 +281,7 @@ def _check_build_info(): def _migrate_installdate_to_b64(): try: import os, json, base64 - from ..utils.localConfig import _get_install_date_path + from ..utils.LocalConfig import _get_install_date_path path = _get_install_date_path() if not os.path.exists(path): _show_bulletin("InstallDate not found") @@ -327,7 +327,7 @@ def show_debug_menu(): return def _trigger_startup_sheet(): - from ..ui.pluginsUpdates.startupSheet import check_and_show_startup_updates + from ..ui.pluginsupdates.StartupSheet import check_and_show_startup_updates check_and_show_startup_updates() def _update_repos_cache(): diff --git a/packit/src/SettingsActivity/deeplinks.py b/packit/src/settingsactivity/Deeplinks.py similarity index 97% rename from packit/src/SettingsActivity/deeplinks.py rename to packit/src/settingsactivity/Deeplinks.py index 2c0336e..8d5f5d7 100644 --- a/packit/src/SettingsActivity/deeplinks.py +++ b/packit/src/settingsactivity/Deeplinks.py @@ -7,7 +7,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from client_utils import get_last_fragment from android.content import Intent from android.net import Uri @@ -15,7 +15,7 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from ..ui.DeeplinkBottomSheets import show_deeplink_sheet diff --git a/packit/src/SettingsActivity/docs.py b/packit/src/settingsactivity/Docs.py similarity index 93% rename from packit/src/SettingsActivity/docs.py rename to packit/src/settingsactivity/Docs.py index 9455127..9641469 100644 --- a/packit/src/SettingsActivity/docs.py +++ b/packit/src/settingsactivity/Docs.py @@ -11,52 +11,52 @@ from android.widget import LinearLayout, TextView, ImageView, FrameLayout except Exception as e: import android_utils as _au; _au.log(f"import android.widget failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.graphics.drawable import GradientDrawable except Exception as e: import android_utils as _au; _au.log(f"import android.graphics.drawable import GradientDrawable failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.view import Gravity except Exception as e: import android_utils as _au; _au.log(f"import android.view import Gravity failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.util import TypedValue except Exception as e: import android_utils as _au; _au.log(f"import android.util import TypedValue failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from androidx.core.content import ContextCompat except Exception as e: import android_utils as _au; _au.log(f"import androidx.core.content import ContextCompat failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _makeBanner(context, icon_name, title_text, subtitle_text): @@ -163,7 +163,7 @@ def _openEnlightenment(self, view, *_): BulletinHelper.show_info(strings.enlighten_11, fragment) settings.set_setting("enlighten_clicks", 0) try: - from ..ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ..ui.achievementsactivity.service.AchivementsEngine import unlock_secret logx(f"docs._openEnlightenment: calling unlock_secret enlightened", True) unlock_secret("enlightened") logx(f"docs._openEnlightenment: unlock_secret done", True) diff --git a/packit/src/SettingsActivity/profile.py b/packit/src/settingsactivity/Profile.py similarity index 98% rename from packit/src/SettingsActivity/profile.py rename to packit/src/settingsactivity/Profile.py index 8f9e48d..ed2eeb9 100644 --- a/packit/src/SettingsActivity/profile.py +++ b/packit/src/settingsactivity/Profile.py @@ -2,20 +2,20 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf from ui.settings import Header, Text, Divider, Custom from ui.bulletin import BulletinHelper from client_utils import get_last_fragment -from ..ui.AchievementsActivity.fragment import show_achievements +from ..ui.achievementsactivity.Fragment import show_achievements import threading import time try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() -from ..ui.AchievementsActivity.service.AchivementsEngine import get_all_with_progress, get_stats + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from ..ui.achievementsactivity.service.AchivementsEngine import get_all_with_progress, get_stats def _get_greeting(first_name: str) -> str: @@ -180,7 +180,7 @@ def _show_achievements(self, view): def _do_export(self, include_local_config: bool, include_achievements: bool, include_saved_plugins: bool): try: - from ..ChatActivity.export.bin.writer import build_binary, _rand_suffix + from ..chatactivity.export.bin.Writer import build_binary, _rand_suffix from android_utils import run_on_ui_thread from java import jclass, dynamic_proxy from java.io import File, FileOutputStream @@ -625,7 +625,7 @@ def onShare(): pass try: - from ..ui.viewUtils import applyFontToTree + from ..ui.ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass @@ -657,7 +657,7 @@ def _make_stats_card(self, context): level, xp_a, xp_b = s["level_info"] try: - from ..utils.localConfig import days_since_install + from ..utils.LocalConfig import days_since_install days = days_since_install() except Exception: days = 0 diff --git a/packit/src/SettingsActivity/settings.py b/packit/src/settingsactivity/Settings.py similarity index 98% rename from packit/src/SettingsActivity/settings.py rename to packit/src/settingsactivity/Settings.py index c4c8093..61b9e93 100644 --- a/packit/src/SettingsActivity/settings.py +++ b/packit/src/settingsactivity/Settings.py @@ -10,22 +10,22 @@ from org.telegram.messenger import ApplicationLoader, AndroidUtilities, R except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader, AndroidUtilities failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme, BottomSheet except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.widget import LinearLayout, TextView, FrameLayout from android.view import Gravity from android.net import Uri @@ -1561,13 +1561,13 @@ def _open_pill_stack_settings(self, view): def _open_files_browser(self): try: - from ..ui.FilesActivity.fragment import show_files_browser + from ..ui.filesactivity.Fragment import show_files_browser show_files_browser(plugin=self.plugin) except Exception as e: logx(f"OtherSettings: _open_files_browser error: {e}", False) def _getCacheDir(self) -> str: - from ..utils.paths import getCacheRoot + from ..utils.Paths import getCacheRoot return getCacheRoot() def _killProcess(self, *_): @@ -1646,7 +1646,7 @@ def _onClearPluginCacheClick(self, view, update_callback=None): def onConfirm(b, w): b.dismiss() try: - from ..utils.paths import getCacheRoot + from ..utils.Paths import getCacheRoot plugin_cache_dir = getCacheRoot() + "/.cache/plugins" if os.path.exists(plugin_cache_dir): shutil.rmtree(plugin_cache_dir) @@ -1701,7 +1701,7 @@ def onRestart(b, w): def _open_card_editor(self): try: - from .SubSettings.PluginCardEditor import build_card_editor_page + from .subsettings.PluginCardEditor import build_card_editor_page return build_card_editor_page() except Exception as e: logx(f"OtherSettings: _open_card_editor error: {e}", False) @@ -1709,7 +1709,7 @@ def _open_card_editor(self): def _open_interface_page(self): try: - from .SubSettings.interface import build_interface_page + from .subsettings.Interface import build_interface_page return build_interface_page(self, self._getContext()) except Exception as e: logx(f"OtherSettings: _open_interface_page error: {e}", False) @@ -1717,7 +1717,7 @@ def _open_interface_page(self): def _open_sfx_page(self): try: - from .SubSettings.sfx import build_sfx_page + from .subsettings.Sfx import build_sfx_page return build_sfx_page(self, self._getContext()) except Exception as e: logx(f"OtherSettings: _open_sfx_page error: {e}", False) @@ -1725,7 +1725,7 @@ def _open_sfx_page(self): def _open_comps_page(self): try: - from .SubSettings.comps import build_comps_page + from .subsettings.Comps import build_comps_page return build_comps_page(self, self._getContext()) except Exception as e: logx(f"OtherSettings: _open_comps_page error: {e}", False) @@ -1733,7 +1733,7 @@ def _open_comps_page(self): def _open_hotkeys_page(self): try: - from .SubSettings.hotkeys import build_hotkeys_page + from .subsettings.Hotkeys import build_hotkeys_page return build_hotkeys_page(self, self._getContext()) except Exception as e: logx(f"OtherSettings: _open_hotkeys_page error: {e}", False) @@ -1741,7 +1741,7 @@ def _open_hotkeys_page(self): def _open_plugin_profile_page(self): try: - from .SubSettings.pluginProfile import build_plugin_profile_page + from .subsettings.PluginProfile import build_plugin_profile_page return build_plugin_profile_page() except Exception as e: logx(f"OtherSettings: _open_plugin_profile_page error: {e}", False) @@ -1749,7 +1749,7 @@ def _open_plugin_profile_page(self): def _open_inline_page(self): try: - from .SubSettings.inline import build_inline_page + from .subsettings.Inline import build_inline_page return build_inline_page(self, _fmt_inline_str, _reload_plugin_settings, _open_url) except Exception as e: logx(f"OtherSettings: _open_inline_page error: {e}", False) @@ -1757,7 +1757,7 @@ def _open_inline_page(self): def _open_file_settings_page(self): try: - from .SubSettings.fileSettings import build_file_settings_page + from .subsettings.FileSettings import build_file_settings_page return build_file_settings_page(self) except Exception as e: logx(f"OtherSettings: _open_file_settings_page error: {e}", False) @@ -1765,7 +1765,7 @@ def _open_file_settings_page(self): def _open_misc_page(self): try: - from .SubSettings.misc import build_misc_page + from .subsettings.Misc import build_misc_page return build_misc_page(self) except Exception as e: logx(f"OtherSettings: _open_misc_page error: {e}", False) @@ -1773,7 +1773,7 @@ def _open_misc_page(self): def _open_apikeys_page(self): try: - from .SubSettings.apikeys import build_apikeys_page + from .subsettings.Apikeys import build_apikeys_page return build_apikeys_page() except Exception as e: logx(f"OtherSettings: _open_apikeys_page error: {e}", False) @@ -1781,7 +1781,7 @@ def _open_apikeys_page(self): def _open_updplugins_page(self): try: - from .SubSettings.updplugins import build_updplugins_page + from .subsettings.Updplugins import build_updplugins_page return build_updplugins_page(self) except Exception as e: logx(f"OtherSettings: _open_updplugins_page error: {e}", False) @@ -1789,7 +1789,7 @@ def _open_updplugins_page(self): def _open_debug_page(self): try: - from .SubSettings.debug import build_debug_page + from .subsettings.Debug import build_debug_page return build_debug_page() except Exception as e: logx(f"OtherSettings: _open_debug_page error: {e}", False) @@ -1797,7 +1797,7 @@ def _open_debug_page(self): def _onClearIgnoreListClick(self, view): try: - from ..ui.pluginsUpdates.clearIgnoreListDialog import show_clear_ignore_list_dialog + from ..ui.pluginsupdates.ClearIgnoreListDialog import show_clear_ignore_list_dialog frag = get_last_fragment() act = frag.getParentActivity() if frag else None if not act: @@ -1943,7 +1943,7 @@ def build(self): red=True )) - from ..utils.paths import getCacheRoot + from ..utils.Paths import getCacheRoot pluginCacheDir = getCacheRoot() + "/.cache/plugins" pluginCacheCard, pluginCacheUpdateFunc = _buildCacheCard(ctx, pluginCacheDir, lambda v: self._onClearPluginCacheClick(v, pluginCacheUpdateFunc), title=strings.clear_plugin_cache) if pluginCacheCard is not None: diff --git a/packit/src/SettingsActivity/utilities.py b/packit/src/settingsactivity/Utilities.py similarity index 97% rename from packit/src/SettingsActivity/utilities.py rename to packit/src/settingsactivity/Utilities.py index b4c0eba..48adafc 100644 --- a/packit/src/SettingsActivity/utilities.py +++ b/packit/src/settingsactivity/Utilities.py @@ -14,7 +14,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _calcPluginsDirSize(): @@ -79,7 +79,7 @@ def _reloadSettings(self): logx(f"utilities._reloadSettings: {e}", False) def _on_export(self, selected_files, export_settings, export_locally): - from .service.pluginsExport import buildArchive + from .service.PluginsExport import buildArchive archive_name = self._archive_name.strip() or "plugins" buildArchive(selected_files, export_settings, export_locally, archive_name) diff --git a/packit/src/SettingsActivity/SubSettings/__init__.py b/packit/src/settingsactivity/__init__.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/__init__.py rename to packit/src/settingsactivity/__init__.py diff --git a/packit/src/SettingsActivity/service/AddKeyDialog.py b/packit/src/settingsactivity/service/AddKeyDialog.py similarity index 100% rename from packit/src/SettingsActivity/service/AddKeyDialog.py rename to packit/src/settingsactivity/service/AddKeyDialog.py diff --git a/packit/src/SettingsActivity/service/fastExpandableHook.py b/packit/src/settingsactivity/service/FastExpandableHook.py similarity index 100% rename from packit/src/SettingsActivity/service/fastExpandableHook.py rename to packit/src/settingsactivity/service/FastExpandableHook.py diff --git a/packit/src/SettingsActivity/service/pluginsExport.py b/packit/src/settingsactivity/service/PluginsExport.py similarity index 98% rename from packit/src/SettingsActivity/service/pluginsExport.py rename to packit/src/settingsactivity/service/PluginsExport.py index 8012944..521e59b 100644 --- a/packit/src/SettingsActivity/service/pluginsExport.py +++ b/packit/src/settingsactivity/service/PluginsExport.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf import os import json import zipfile @@ -36,7 +36,7 @@ def _resolvePluginsDir() -> str | None: def _resolveLocalConfigPath() -> str | None: try: - from ...utils.paths import getConfigsDir + from ...utils.Paths import getConfigsDir return os.path.join(getConfigsDir(), "localConfig.json") except Exception as e: logx(f"pluginsExport._resolveLocalConfigPath: {e}", False) @@ -79,7 +79,7 @@ def _readPluginMeta(filepath: str) -> dict: def _buildConfigScl(selected_files: list, export_settings: bool, export_locally: bool, plugins_dir: str, local_cfg_path: str | None) -> str: - from ...scl.scl import Doc + from ...scl.Scl import Doc doc = Doc.new() doc.set("type", "local" if export_locally else "external") doc.set("settings", export_settings) @@ -89,7 +89,7 @@ def _buildConfigScl(selected_files: list, export_settings: bool, export_locally: def _buildLocalScl(selected_files: list, plugins_dir: str) -> str: # list of plugin descriptors: id, name, path (relative to archive root), version - from ...scl.scl import Doc + from ...scl.Scl import Doc doc = Doc.new() listBuilder = doc.newList() diff --git a/packit/src/SettingsActivity/__init__.py b/packit/src/settingsactivity/service/__init__.py similarity index 100% rename from packit/src/SettingsActivity/__init__.py rename to packit/src/settingsactivity/service/__init__.py diff --git a/packit/src/SettingsActivity/SubSettings/apikeys.py b/packit/src/settingsactivity/subsettings/Apikeys.py similarity index 96% rename from packit/src/SettingsActivity/SubSettings/apikeys.py rename to packit/src/settingsactivity/subsettings/Apikeys.py index 53aeb91..ed0fc06 100644 --- a/packit/src/SettingsActivity/SubSettings/apikeys.py +++ b/packit/src/settingsactivity/subsettings/Apikeys.py @@ -41,8 +41,8 @@ def _get_gemini_key_preview() -> "str | None": # returns "AB..xyz" preview or None if key not set try: import ctypes - from ...nativeLoader import loadPackitKey - from ...utils.paths import getKeysDir + from ...NativeLoader import loadPackitKey + from ...utils.Paths import getKeysDir lib = loadPackitKey() if not lib: @@ -83,8 +83,8 @@ def _get_gemini_key_preview() -> "str | None": def _save_gemini_key(keyValue: str): try: import ctypes - from ...nativeLoader import loadPackitKey - from ...utils.paths import getKeysDir + from ...NativeLoader import loadPackitKey + from ...utils.Paths import getKeysDir import os keysDir = getKeysDir() @@ -117,8 +117,8 @@ def _save_gemini_key(keyValue: str): def _delete_gemini_key(): try: - from ...nativeLoader import loadPackitKey - from ...utils.paths import getKeysDir + from ...NativeLoader import loadPackitKey + from ...utils.Paths import getKeysDir lib = loadPackitKey() if not lib: @@ -142,7 +142,7 @@ def _delete_gemini_key(): def _has_gemini_cache() -> bool: try: import json, os - from ...utils.paths import getGeminiCachePath + from ...utils.Paths import getGeminiCachePath path = getGeminiCachePath() if not os.path.exists(path): return False @@ -166,7 +166,7 @@ def _on_confirm(b, w): b.dismiss() try: import os - from ...utils.paths import getGeminiCachePath + from ...utils.Paths import getGeminiCachePath path = getGeminiCachePath() if os.path.exists(path): os.remove(path) diff --git a/packit/src/SettingsActivity/SubSettings/comps.py b/packit/src/settingsactivity/subsettings/Comps.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/comps.py rename to packit/src/settingsactivity/subsettings/Comps.py diff --git a/packit/src/SettingsActivity/SubSettings/debug.py b/packit/src/settingsactivity/subsettings/Debug.py similarity index 99% rename from packit/src/SettingsActivity/SubSettings/debug.py rename to packit/src/settingsactivity/subsettings/Debug.py index 2f4ad81..895e0ef 100644 --- a/packit/src/SettingsActivity/SubSettings/debug.py +++ b/packit/src/settingsactivity/subsettings/Debug.py @@ -214,7 +214,7 @@ def pair_row(left, right): ) try: - from ...ui.viewUtils import applyFontToTree + from ...ui.ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass @@ -271,7 +271,7 @@ def _onWriteLogsChange(enabled): def _sendLatestLog(view): try: - from ...utils.paths import getCacheRoot, getLogShareCachePath + from ...utils.Paths import getCacheRoot, getLogShareCachePath log_path = getCacheRoot() + "/latestlog.txt" logx(f"sendLatestLog: log_path={log_path}", True) if not os.path.exists(log_path): @@ -353,7 +353,7 @@ def _copyLatestLogPath(view): def _getLatestLogPath(): try: - from ...utils.paths import getCacheRoot + from ...utils.Paths import getCacheRoot return getCacheRoot() + "/latestlog.txt" except Exception: return None diff --git a/packit/src/SettingsActivity/SubSettings/fileSettings.py b/packit/src/settingsactivity/subsettings/FileSettings.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/fileSettings.py rename to packit/src/settingsactivity/subsettings/FileSettings.py diff --git a/packit/src/SettingsActivity/SubSettings/hotkeys.py b/packit/src/settingsactivity/subsettings/Hotkeys.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/hotkeys.py rename to packit/src/settingsactivity/subsettings/Hotkeys.py diff --git a/packit/src/SettingsActivity/SubSettings/inline.py b/packit/src/settingsactivity/subsettings/Inline.py similarity index 96% rename from packit/src/SettingsActivity/SubSettings/inline.py rename to packit/src/settingsactivity/subsettings/Inline.py index cac9996..ff35585 100644 --- a/packit/src/SettingsActivity/SubSettings/inline.py +++ b/packit/src/settingsactivity/subsettings/Inline.py @@ -3,7 +3,7 @@ from ui.settings import Header, Switch, Divider, Input, Text from elyx import strings -from ...ChatActivity.inline import inlineState +from ...chatactivity.inline import InlineState def build_inline_page(other_settings, fmt_inline_str, reload_plugin_settings, open_url): @@ -16,7 +16,7 @@ def build_inline_page(other_settings, fmt_inline_str, reload_plugin_settings, op default=True, icon="msg_search", link_alias="inline_search_enabled", - on_change=lambda v: inlineState.update_state(v) + on_change=lambda v: InlineState.update_state(v) ), Input( key="inline_search_command", diff --git a/packit/src/SettingsActivity/SubSettings/interface.py b/packit/src/settingsactivity/subsettings/Interface.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/interface.py rename to packit/src/settingsactivity/subsettings/Interface.py diff --git a/packit/src/SettingsActivity/SubSettings/misc.py b/packit/src/settingsactivity/subsettings/Misc.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/misc.py rename to packit/src/settingsactivity/subsettings/Misc.py diff --git a/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py b/packit/src/settingsactivity/subsettings/PluginCardEditor.py similarity index 99% rename from packit/src/SettingsActivity/SubSettings/PluginCardEditor.py rename to packit/src/settingsactivity/subsettings/PluginCardEditor.py index b5df227..4ac7f95 100644 --- a/packit/src/SettingsActivity/SubSettings/PluginCardEditor.py +++ b/packit/src/settingsactivity/subsettings/PluginCardEditor.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf import ctypes from android.view import View, Gravity from android.widget import LinearLayout, TextView, FrameLayout, ScrollView, ImageView, SeekBar @@ -1532,7 +1532,7 @@ def on_change(v): md3 = None try: - from ...ui.md3Slider import createMd3Slider + from ...ui.Md3Slider import createMd3Slider md3 = createMd3Slider(ctx, min_val, max_val, val, on_change) except Exception as e: logx(f"PCE: md3 slider create error: {e}", False) diff --git a/packit/src/SettingsActivity/SubSettings/pluginProfile.py b/packit/src/settingsactivity/subsettings/PluginProfile.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/pluginProfile.py rename to packit/src/settingsactivity/subsettings/PluginProfile.py diff --git a/packit/src/SettingsActivity/SubSettings/sfx.py b/packit/src/settingsactivity/subsettings/Sfx.py similarity index 95% rename from packit/src/SettingsActivity/SubSettings/sfx.py rename to packit/src/settingsactivity/subsettings/Sfx.py index aca2881..a51f384 100644 --- a/packit/src/SettingsActivity/SubSettings/sfx.py +++ b/packit/src/settingsactivity/subsettings/Sfx.py @@ -26,7 +26,7 @@ def _reload(): def _make_expandable(other_settings, ctx): try: from android_utils import OnClickListener - from ...dexLoader import sfxExpandableCreate + from ...DexLoader import sfxExpandableCreate checked_count = sum( 1 for key, _, default in _SFX_CHILDREN if settings.get(key, default) @@ -72,7 +72,7 @@ def switch_click(view): def _make_child(ctx, key, text, default): try: - from ...dexLoader import sfxChildCreate + from ...DexLoader import sfxChildCreate item = sfxChildCreate( ctx, @@ -100,7 +100,7 @@ def _make_volume_slider(ctx): try: from java import dynamic_proxy from java.lang.reflect import InvocationHandler - from ...dexLoader import sfxVolumeSliderCreate + from ...DexLoader import sfxVolumeSliderCreate class _VolumeChange(dynamic_proxy(InvocationHandler)): def invoke(self, proxy, method, args): @@ -120,7 +120,7 @@ def invoke(self, proxy, method, args): _VolumeChange(), ) if view is None: - from ...ui.md3Slider import createMd3Slider + from ...ui.Md3Slider import createMd3Slider def on_change(value): settings.set("sfx_volume", int(value), reload_settings=False) diff --git a/packit/src/SettingsActivity/SubSettings/updplugins.py b/packit/src/settingsactivity/subsettings/Updplugins.py similarity index 100% rename from packit/src/SettingsActivity/SubSettings/updplugins.py rename to packit/src/settingsactivity/subsettings/Updplugins.py diff --git a/packit/src/SettingsActivity/service/__init__.py b/packit/src/settingsactivity/subsettings/__init__.py similarity index 100% rename from packit/src/SettingsActivity/service/__init__.py rename to packit/src/settingsactivity/subsettings/__init__.py diff --git a/packit/src/standaloneHooks/addIconsFab.py b/packit/src/standalonehooks/AddIconsFab.py similarity index 99% rename from packit/src/standaloneHooks/addIconsFab.py rename to packit/src/standalonehooks/AddIconsFab.py index ca17be3..793e322 100644 --- a/packit/src/standaloneHooks/addIconsFab.py +++ b/packit/src/standalonehooks/AddIconsFab.py @@ -142,7 +142,7 @@ def _inject_fab(plugin, frag_view, fragment=None): def on_fab_click(v): try: - from ..ui.IconsListActivity.fragment import InstallIconsUI + from ..ui.iconslistactivity.Fragment import InstallIconsUI InstallIconsUI(plugin).open() except Exception as e: logx(f"addIconsFab: on_fab_click error: {e}", False) diff --git a/packit/src/standaloneHooks/addPluginFab.py b/packit/src/standalonehooks/AddPluginFab.py similarity index 99% rename from packit/src/standaloneHooks/addPluginFab.py rename to packit/src/standalonehooks/AddPluginFab.py index 4e01d34..70008de 100644 --- a/packit/src/standaloneHooks/addPluginFab.py +++ b/packit/src/standalonehooks/AddPluginFab.py @@ -108,7 +108,7 @@ def _inject_fab(plugin, frag_view): def on_fab_click(v): try: - from ..ui.PluginListActivity.fragment import InstallUI + from ..ui.pluginlistactivity.Fragment import InstallUI InstallUI(plugin).open() except Exception as e: logx(f"addPluginFab: on_fab_click error: {e}", False) diff --git a/packit/src/standaloneHooks/InstallDismissHook.py b/packit/src/standalonehooks/InstallDismissHook.py similarity index 95% rename from packit/src/standaloneHooks/InstallDismissHook.py rename to packit/src/standalonehooks/InstallDismissHook.py index 3efcb2c..23a820d 100644 --- a/packit/src/standaloneHooks/InstallDismissHook.py +++ b/packit/src/standalonehooks/InstallDismissHook.py @@ -20,14 +20,14 @@ def after_hooked_method(self, param): if error_str is not None: return try: - from ..ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ..ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Installing plugins") except Exception as e: logx(f"installSuccessHook: achievements increment error: {e}", False) # backstop index write; core's pluginsUpdated observer is the # primary path. commit_pending consumes _pending -> idempotent. try: - from ..utils.installIndex import commit_pending + from ..utils.InstallIndex import commit_pending commit_pending() except Exception as e: logx(f"installSuccessHook: commit_pending error: {e}", False) diff --git a/packit/src/standaloneHooks/settingsActivityHook.py b/packit/src/standalonehooks/SettingsActivityHook.py similarity index 100% rename from packit/src/standaloneHooks/settingsActivityHook.py rename to packit/src/standalonehooks/SettingsActivityHook.py diff --git a/packit/src/standaloneHooks/universalFragmentFix.py b/packit/src/standalonehooks/UniversalFragmentFix.py similarity index 100% rename from packit/src/standaloneHooks/universalFragmentFix.py rename to packit/src/standalonehooks/UniversalFragmentFix.py diff --git a/packit/src/standaloneHooks/__init__.py b/packit/src/standalonehooks/__init__.py similarity index 100% rename from packit/src/standaloneHooks/__init__.py rename to packit/src/standalonehooks/__init__.py diff --git a/packit/src/ui/contextMenu.py b/packit/src/ui/ContextMenu.py similarity index 100% rename from packit/src/ui/contextMenu.py rename to packit/src/ui/ContextMenu.py diff --git a/packit/src/ui/DeeplinkBottomSheets.py b/packit/src/ui/DeeplinkBottomSheets.py index 1aed7ee..b954d7c 100644 --- a/packit/src/ui/DeeplinkBottomSheets.py +++ b/packit/src/ui/DeeplinkBottomSheets.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf from android.view import View, MotionEvent from android.widget import LinearLayout, TextView, FrameLayout, ScrollView, ImageView from android.view import Gravity @@ -17,22 +17,22 @@ from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() DEEPLINKS_DATA = { @@ -474,7 +474,7 @@ def copy_link(v): root.addView(close_btn, LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) sheet.setCustomView(root) try: - from .viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/ExportBottomSheet.py b/packit/src/ui/ExportBottomSheet.py index 35f59b3..3bda9d8 100644 --- a/packit/src/ui/ExportBottomSheet.py +++ b/packit/src/ui/ExportBottomSheet.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.ripple import safe_ripple as _safe_ripple +from ..utils.Ripple import safe_ripple as _safe_ripple import os import ast import re @@ -288,7 +288,7 @@ def _createCheckRow(act, label, version_str, icon_str, checked, on_change): except Exception: pass row.addView(icon_view, LayoutHelper.createLinear(icon_size_dp, icon_size_dp, Gravity.CENTER_VERTICAL, 0, 0, 10, 0)) - from ..utils.stickers import load_sticker + from ..utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"ExportBottomSheet._createCheckRow: icon error: {e}\n{traceback.format_exc()}", False) @@ -788,7 +788,7 @@ def _onExport(): sheet.setCustomView(outer) try: - from .viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass diff --git a/packit/src/ui/FontPickerBottomSheet.py b/packit/src/ui/FontPickerBottomSheet.py index db35fb3..0eb0483 100644 --- a/packit/src/ui/FontPickerBottomSheet.py +++ b/packit/src/ui/FontPickerBottomSheet.py @@ -412,7 +412,7 @@ def _showStyleSheet(act, family, styles, selected_filename, parent_sheet, on_sel root.addView(_createCloseButton(act, sheet.dismiss), LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) try: - from .viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass @@ -486,7 +486,7 @@ def showFontPicker(act, on_select=None): root.addView(_createCloseButton(act, sheet.dismiss), LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) try: - from .viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/md3Slider.py b/packit/src/ui/Md3Slider.py similarity index 100% rename from packit/src/ui/md3Slider.py rename to packit/src/ui/Md3Slider.py diff --git a/packit/src/ui/NoInternetBanner.py b/packit/src/ui/NoInternetBanner.py index 6e21a24..35f7f98 100644 --- a/packit/src/ui/NoInternetBanner.py +++ b/packit/src/ui/NoInternetBanner.py @@ -333,7 +333,7 @@ def _create_banner(self): except Exception: pass try: - from ..viewUtils import applyFont + from .ViewUtils import applyFont applyFont(tv) except Exception: pass diff --git a/packit/src/ui/reportDialog.py b/packit/src/ui/ReportDialog.py similarity index 99% rename from packit/src/ui/reportDialog.py rename to packit/src/ui/ReportDialog.py index 108f4c8..147be71 100644 --- a/packit/src/ui/reportDialog.py +++ b/packit/src/ui/ReportDialog.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf import ctypes import json import os @@ -122,14 +122,14 @@ def onAnimationRepeat(self, a, *args): pass def _load_reasons(repo_id: str) -> list: - from ..utils import cachedRepos - return cachedRepos.reasons(repo_id) + from ..utils import CachedRepos + return CachedRepos.reasons(repo_id) def _load_report_settings(repo_id: str): # (forum_username, topic_msg_id), or (None, None) - from ..utils import cachedRepos - return cachedRepos.report_settings(repo_id) + from ..utils import CachedRepos + return CachedRepos.report_settings(repo_id) def _submit_report(forum_username: str, topic_msg_id: int, plugin_name: str, plugin_id: str, repo_id: str, reason: str, description: str, act, on_done): @@ -1031,7 +1031,7 @@ def _on_submit(v): card.setScaleY(0.92) try: - from .viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/restartDialog.py b/packit/src/ui/RestartDialog.py similarity index 98% rename from packit/src/ui/restartDialog.py rename to packit/src/ui/RestartDialog.py index 0c45ff6..ab67ea2 100644 --- a/packit/src/ui/restartDialog.py +++ b/packit/src/ui/RestartDialog.py @@ -326,8 +326,8 @@ def _dismiss(on_end=None): def _do_restart(): try: - from ..deeplinks import pkill - pkill.handle("tg://packit?pkill") + from ..deeplinks import Pkill + Pkill.handle("tg://packit?pkill") except Exception as e: logx(f"restartDialog: pkill error: {e}", False) @@ -347,7 +347,7 @@ def _do_restart(): card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from .ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/viewUtils.py b/packit/src/ui/ViewUtils.py similarity index 100% rename from packit/src/ui/viewUtils.py rename to packit/src/ui/ViewUtils.py diff --git a/packit/src/ui/AchievementsActivity/fragment.py b/packit/src/ui/achievementsactivity/Fragment.py similarity index 99% rename from packit/src/ui/AchievementsActivity/fragment.py rename to packit/src/ui/achievementsactivity/Fragment.py index 7821c94..3ae4972 100644 --- a/packit/src/ui/AchievementsActivity/fragment.py +++ b/packit/src/ui/achievementsactivity/Fragment.py @@ -462,7 +462,7 @@ def afterCreateView(self, view): _add_actionbar_glow(view) _add_bottom_glow(view) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(view) except Exception: pass @@ -630,7 +630,7 @@ def afterCreateView(self, view): _add_actionbar_glow(view) _add_bottom_glow(view) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(view) except Exception: pass @@ -761,7 +761,7 @@ def show_hint_sheet(achievement: dict): sheet.setCustomView(root) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/AchievementsActivity/__init__.py b/packit/src/ui/achievementsactivity/__init__.py similarity index 100% rename from packit/src/ui/AchievementsActivity/__init__.py rename to packit/src/ui/achievementsactivity/__init__.py diff --git a/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py b/packit/src/ui/achievementsactivity/service/AchivementsEngine.py similarity index 98% rename from packit/src/ui/AchievementsActivity/service/AchivementsEngine.py rename to packit/src/ui/achievementsactivity/service/AchivementsEngine.py index f04d217..69d0b31 100644 --- a/packit/src/ui/AchievementsActivity/service/AchivementsEngine.py +++ b/packit/src/ui/achievementsactivity/service/AchivementsEngine.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ....utils.bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf import os import json import zlib @@ -12,7 +12,7 @@ from org.telegram.messenger import ApplicationLoader, UserConfig except Exception as e: import android_utils as _au; _au.log(f"achievements: import failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() _achievement_pending = False _bulletin_container = None @@ -46,7 +46,7 @@ def _load_achievements() -> list: def _get_configs_dir() -> str: - from ....utils.paths import getConfigsDir + from ....utils.Paths import getConfigsDir return getConfigsDir() @@ -78,7 +78,7 @@ def _get_current_account_id() -> str: return "0" def _load_lib(): - from ....nativeLoader import loadPackitDb + from ....NativeLoader import loadPackitDb lib = loadPackitDb() if lib is not None: logx("packitdb: libpackitdb loaded ok", True) @@ -302,7 +302,7 @@ def sync_completed(data: dict) -> tuple: data[aid] = current_level try: - from ....utils.localConfig import days_since_install + from ....utils.LocalConfig import days_since_install days = days_since_install() except Exception: days = 0 @@ -389,7 +389,7 @@ def show(): return def _open(): - from ....ui.AchievementsActivity.fragment import show_hint_sheet + from ....ui.achievementsactivity.Fragment import show_hint_sheet show_hint_sheet(achievement) ctx = fragment.getContext() @@ -425,7 +425,7 @@ def _open(): def _play_achievement_sound(): try: - from ....utils.media import playSound + from ....utils.Media import playSound from elyx import assets sound_path = assets.sounds.received_achievement.path_str playSound(sound_path, "sfx_achievement", check_pending=False, default=True) diff --git a/packit/src/ui/AchievementsActivity/service/__init__.py b/packit/src/ui/achievementsactivity/service/__init__.py similarity index 100% rename from packit/src/ui/AchievementsActivity/service/__init__.py rename to packit/src/ui/achievementsactivity/service/__init__.py diff --git a/packit/src/ui/contributors/fragment.py b/packit/src/ui/contributors/Fragment.py similarity index 99% rename from packit/src/ui/contributors/fragment.py rename to packit/src/ui/contributors/Fragment.py index 2e9b4f0..ceff4d5 100644 --- a/packit/src/ui/contributors/fragment.py +++ b/packit/src/ui/contributors/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf from java import dynamic_proxy from android_utils import run_on_ui_thread, OnClickListener from client_utils import get_last_fragment, run_on_queue diff --git a/packit/src/ui/FilesActivity/fragment.py b/packit/src/ui/filesactivity/Fragment.py similarity index 99% rename from packit/src/ui/FilesActivity/fragment.py rename to packit/src/ui/filesactivity/Fragment.py index c10251c..f32a2db 100644 --- a/packit/src/ui/FilesActivity/fragment.py +++ b/packit/src/ui/filesactivity/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.ripple import safe_ripple as _safe_ripple +from ...utils.Ripple import safe_ripple as _safe_ripple import os from android.view import View, Gravity, MotionEvent from android.widget import LinearLayout, TextView, FrameLayout, ScrollView, ImageView, HorizontalScrollView @@ -90,7 +90,7 @@ def _restore_icon(icon_id): def _task(): try: - from .openFileFragment import _is_binary, open_file + from .OpenFileFragment import _is_binary, open_file if _is_binary(path): logx("filesActivity: binary file, showing sheet", True) if icon_view is not None: @@ -154,7 +154,7 @@ def _format_size(size): def _get_cache_root(): try: - from ...utils.paths import getCacheRoot + from ...utils.Paths import getCacheRoot return getCacheRoot() except Exception as e: logx(f"filesActivity: _get_cache_root error: {e}", False) @@ -977,7 +977,7 @@ def _get_file_info(path): def _show_file_info(act, path): try: - from .infoDialog import show_info_dialog + from .InfoDialog import show_info_dialog info = _get_file_info(path) name = os.path.basename(path) show_info_dialog(act, name, info) diff --git a/packit/src/ui/FilesActivity/infoDialog.py b/packit/src/ui/filesactivity/InfoDialog.py similarity index 98% rename from packit/src/ui/FilesActivity/infoDialog.py rename to packit/src/ui/filesactivity/InfoDialog.py index a8eb854..58a8da6 100644 --- a/packit/src/ui/FilesActivity/infoDialog.py +++ b/packit/src/ui/filesactivity/InfoDialog.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.ripple import safe_ripple as _safe_ripple -from ...utils.bulletins import factory as _pbf +from ...utils.Ripple import safe_ripple as _safe_ripple +from ...utils.Bulletins import factory as _pbf import ctypes from android_utils import run_on_ui_thread, OnClickListener @@ -366,7 +366,7 @@ def _dismiss(): card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/FilesActivity/openFileFragment.py b/packit/src/ui/filesactivity/OpenFileFragment.py similarity index 98% rename from packit/src/ui/FilesActivity/openFileFragment.py rename to packit/src/ui/filesactivity/OpenFileFragment.py index 0cc2f44..d22f16d 100644 --- a/packit/src/ui/FilesActivity/openFileFragment.py +++ b/packit/src/ui/filesactivity/OpenFileFragment.py @@ -173,7 +173,7 @@ def onTouch(self, v, event): sheet.setCustomView(container) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(container) except Exception: pass @@ -220,7 +220,7 @@ def onFragmentDestroy(self, *_): self._load_cancelled = True self._highlight_cancelled = True try: - from ...dexLoader import openFileCancel + from ...DexLoader import openFileCancel if self._viewer_view is not None: openFileCancel(self._viewer_view) except Exception as e: @@ -411,7 +411,7 @@ def _tokenize(self): if ext not in (".json", ".py", ".plugin", ".java", ".kt"): return tt, ts, te, ck, cv try: - from .packlight import tokenizeJson, tokenizePython, tokenizeJava, tokenizeKotlin, _resolveColors + from .Packlight import tokenizeJson, tokenizePython, tokenizeJava, tokenizeKotlin, _resolveColors text = self._text if ext == ".json": result = tokenizeJson(text) @@ -446,7 +446,7 @@ def _attach_viewer(self, tt, ts, te, ck, cv): if self._load_cancelled or self._viewer_container is None: return try: - from ...dexLoader import openFileCreate, openFileCancel + from ...DexLoader import openFileCreate, openFileCancel # drop a previous viewer (rebuild after save) if self._viewer_view is not None: try: @@ -544,7 +544,7 @@ def _startHighlight(self): def _highlightBg(self, ext: str): try: - from .packlight import tokenizeJson, tokenizePython, tokenizeJava, tokenizeKotlin, _resolveColors + from .Packlight import tokenizeJson, tokenizePython, tokenizeJava, tokenizeKotlin, _resolveColors if self._highlight_cancelled: return text = self._text @@ -574,7 +574,7 @@ def _applyHighlightChunked(self, text: str, tokBuf, ranges, cnt: int, colors: di return try: from android.text import SpannableString - from .packlight import _applySpans + from .Packlight import _applySpans if offset == 0: self._spannable = SpannableString(text) diff --git a/packit/src/ui/FilesActivity/packlight.py b/packit/src/ui/filesactivity/Packlight.py similarity index 98% rename from packit/src/ui/FilesActivity/packlight.py rename to packit/src/ui/filesactivity/Packlight.py index e1e100c..5343dba 100644 --- a/packit/src/ui/FilesActivity/packlight.py +++ b/packit/src/ui/filesactivity/Packlight.py @@ -72,7 +72,7 @@ def _loadLib(): global _lib if _lib is not None: return _lib - from ...nativeLoader import loadPackLight + from ...NativeLoader import loadPackLight _lib = loadPackLight() if _lib is not None: _setupArgtypes(_lib) diff --git a/packit/src/ui/FilesActivity/__init__.py b/packit/src/ui/filesactivity/__init__.py similarity index 100% rename from packit/src/ui/FilesActivity/__init__.py rename to packit/src/ui/filesactivity/__init__.py diff --git a/packit/src/ui/IconsListActivity/fragment.py b/packit/src/ui/iconslistactivity/Fragment.py similarity index 98% rename from packit/src/ui/IconsListActivity/fragment.py rename to packit/src/ui/iconslistactivity/Fragment.py index 9040d8f..62a7d94 100644 --- a/packit/src/ui/IconsListActivity/fragment.py +++ b/packit/src/ui/iconslistactivity/Fragment.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf -from ...utils.netQueue import run_io +from ...utils.Bulletins import factory as _pbf +from ...utils.NetQueue import run_io import json import threading import re @@ -25,32 +25,32 @@ from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper, EditTextBoldCursor except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper, EditTextBoldCursor failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, R as R_tg failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android_utils import OnClickListener, OnLongClickListener try: from com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from .RepoBottomSheet import show_icon_repo_sheet from .SortBottomSheet import show_icon_sort_menu -from ...utils import search as search_mod +from ...utils import Search as search_mod def _count_active_repos(repoManager) -> int: @@ -271,7 +271,7 @@ def onTextChanged(self, s, start, before, count): def _icons_on_clear_click(self, act): try: from elyx import assets - from ...utils.media import playSound + from ...utils.Media import playSound playSound(assets.sounds.clear_search.path_str, "sfx_clear_search") except Exception: pass @@ -288,7 +288,7 @@ def _icons_on_clear_click(self, act): def _icons_on_search_btn_click(self, act): try: from elyx import assets - from ...utils.media import playSound + from ...utils.Media import playSound playSound(assets.sounds.search_btn.path_str, "sfx_search") except Exception: pass @@ -355,7 +355,7 @@ 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 + from ...DexLoader import catalogIconsChromeCreate live_search = bool(settings.get("live_search", True)) try: accent = Theme.getColor(Theme.key_featuredStickers_addButton) @@ -686,8 +686,8 @@ def load_task(): logx(f"IconList._open_all_repos_icons: loading repo '{repo.get('name')}' id='{repo_id}' url='{repo_url}'", True) try: from ...network import Storage - from ...utils import cachedRepos - icons_url = cachedRepos.icons_url(repo, repo_url) + from ...utils import CachedRepos + icons_url = CachedRepos.icons_url(repo, repo_url) logx(f"IconList._open_all_repos_icons: fetching icons from '{icons_url}'", True) entries, error = Storage.fetch_icons(icons_url) if error: @@ -741,8 +741,8 @@ def _open_repo_icons(self, repo): def load_task(): try: from ...network import Storage - from ...utils import cachedRepos - icons_url = cachedRepos.icons_url(repo_id, repo_url) + from ...utils import CachedRepos + icons_url = CachedRepos.icons_url(repo_id, repo_url) logx(f"IconList._open_repo_icons: fetching '{icons_url}'", True) icons, error = Storage.fetch_icons(icons_url) if error: @@ -751,8 +751,8 @@ def load_task(): # left behind for the sources screen: repomap points at this # file by url and carries no count of its own try: - from ...utils import repoStats - repoStats.remember(repo_id, icons=len(icons)) + from ...utils import RepoStats + RepoStats.remember(repo_id, icons=len(icons)) except Exception as e: logx(f"IconList._open_repo_icons: stats write failed: {e}", True) # index build is heavy — run it here on the queue thread @@ -1320,7 +1320,7 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) self.search.addTextChangedListener(_IconsSearchTextWatcher(self, clear_btn, act)) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass @@ -1718,7 +1718,7 @@ def make_item(self, icon): try: _q = getattr(self, "last_search_query", None) if _q: - from ..viewUtils import highlightQuery + from ..ViewUtils import highlightQuery _hl = highlightQuery( _display_name, str(_q), Theme.getColor(Theme.key_featuredStickers_addButton), @@ -1872,7 +1872,7 @@ def fetch_first(urls=all_urls, attempt=0): card.setFocusable(True) def _on_click(v, _icon=icon): try: - from ...core import install_icon_pack + from ...Core import install_icon_pack install_icon_pack(_icon) except Exception as ex: logx(f"icons: card click error: {ex}", True) @@ -1903,7 +1903,7 @@ def _on_long_click(v, _icon=icon, _repo_id=self.repo_id): except Exception as _be: logx(f"icons: copy bulletin error: {_be}", True) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception: pass diff --git a/packit/src/ui/IconsListActivity/RepoBottomSheet.py b/packit/src/ui/iconslistactivity/RepoBottomSheet.py similarity index 97% rename from packit/src/ui/IconsListActivity/RepoBottomSheet.py rename to packit/src/ui/iconslistactivity/RepoBottomSheet.py index 46b73dc..b403c82 100644 --- a/packit/src/ui/IconsListActivity/RepoBottomSheet.py +++ b/packit/src/ui/iconslistactivity/RepoBottomSheet.py @@ -15,22 +15,22 @@ from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def show_icon_repo_sheet(install_ui, repos, on_select=None): @@ -239,7 +239,7 @@ def on_all_click(v): sheet.setCustomView(root) try: - from ...ui.viewUtils import applyFontToTree + from ...ui.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/IconsListActivity/SortBottomSheet.py b/packit/src/ui/iconslistactivity/SortBottomSheet.py similarity index 97% rename from packit/src/ui/IconsListActivity/SortBottomSheet.py rename to packit/src/ui/iconslistactivity/SortBottomSheet.py index 8914645..7fa28af 100644 --- a/packit/src/ui/IconsListActivity/SortBottomSheet.py +++ b/packit/src/ui/iconslistactivity/SortBottomSheet.py @@ -15,22 +15,22 @@ from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() _SORT_ICONS = { "alpha_az": "msg_archive", diff --git a/packit/src/ui/IconsListActivity/__init__.py b/packit/src/ui/iconslistactivity/__init__.py similarity index 100% rename from packit/src/ui/IconsListActivity/__init__.py rename to packit/src/ui/iconslistactivity/__init__.py diff --git a/packit/src/ui/PluginActivity/fragment.py b/packit/src/ui/pluginactivity/Fragment.py similarity index 99% rename from packit/src/ui/PluginActivity/fragment.py rename to packit/src/ui/pluginactivity/Fragment.py index 676b64f..a9a8d6d 100644 --- a/packit/src/ui/PluginActivity/fragment.py +++ b/packit/src/ui/pluginactivity/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf import ctypes import threading from android.view import Gravity, View @@ -36,7 +36,7 @@ def _get_saved_plugins_path() -> str: - from ...utils.paths import getConfigsDir + from ...utils.Paths import getConfigsDir import os d = getConfigsDir() os.makedirs(d, exist_ok=True) @@ -464,13 +464,13 @@ def _on_click(v): logx(f"pluginProfile: _build_team_card error: {_e}", True) -from .versionPicker import _show_version_picker +from .VersionPicker import _show_version_picker def _show_plugin_menu(act, p, anchor_view, repo_id: str = ""): try: - from ..PluginListActivity.helpers.PluginActions import share_plugin_file, view_plugin_code, download_plugin_file - from ..PluginListActivity.helpers.ReportService import report_plugin + from ..pluginlistactivity.helpers.PluginActions import share_plugin_file, view_plugin_code, download_plugin_file + from ..pluginlistactivity.helpers.ReportService import report_plugin from org.telegram.ui.Components import ItemOptions from org.telegram.ui.ActionBar import ActionBarMenuSubItem from org.telegram.messenger import R as R_tg, AndroidUtilities @@ -504,7 +504,7 @@ def _copy_and_dismiss(link): AndroidUtilities.addToClipboard(link) options.dismiss() try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as _ae: logx(f"pluginProfile: achievement increment error: {_ae}", True) @@ -584,7 +584,7 @@ def onFragmentCreate(self, *_): def onFragmentDestroy(self, *_): logx(f"pluginProfile: onFragmentDestroy plugin={self.plugin.get('id')}", True) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import unregister_bulletin_container + from ...ui.achievementsactivity.service.AchivementsEngine import unregister_bulletin_container unregister_bulletin_container(self.content_view) except Exception as e: logx(f"pluginProfile: unregister_bulletin_container error: {e}", False) @@ -688,7 +688,7 @@ def beforeCreateView(self): self.content_view.setBackgroundColor(bg_color) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import register_bulletin_container + from ...ui.achievementsactivity.service.AchivementsEngine import register_bulletin_container register_bulletin_container(self.content_view) except Exception as e: logx(f"pluginProfile: register_bulletin_container error: {e}", False) @@ -760,7 +760,7 @@ def overScrollBy(self, deltaX, deltaY, scrollX, scrollY, ) iv_lp.rightMargin = AndroidUtilities.dp(14) top_row.addView(iv, iv_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(iv, icon_str, sticker_size) info_col = LinearLayout(act) @@ -928,7 +928,7 @@ def _format_date(raw, prefix): deps = p.get("deps") or [] if has_link: - from ...utils.app_version import check_app_version as _check_app_version + from ...utils.AppVersion import check_app_version as _check_app_version plugin_app_ver = p.get("app_version") is_available = (not plugin_app_ver) or _check_app_version(plugin_app_ver) @@ -1050,7 +1050,7 @@ def _set_loading(_btn, _label, _btn_text_color, _act, isLoading): def _do_install(_p, _install_ui, _all, _btn, _label, _btn_text_color, _act, on_finish_override=None, succ_download=None): - from ...core import install_plugin + from ...Core import install_plugin if not on_finish_override: _set_loading(_btn, _label, _btn_text_color, _act, True) @@ -1058,7 +1058,7 @@ def _finish(ok): if ok: try: from elyx import assets - from ...utils.media import playSound + from ...utils.Media import playSound _snd = assets.sounds.install.path_str playSound(_snd, "sfx_install") except Exception: @@ -1203,8 +1203,8 @@ def onInstallClickFab(v, _p=p, _install_ui=_install_ui_ref, _all=_all_plugins_re if not versions: _do_install(_p, _install_ui, _all, _btn, _label, _btn_text_color, _act) return - from ...utils.app_version import check_app_version as _check_app_version2 - from .versionPicker import _build_version_entries + from ...utils.AppVersion import check_app_version as _check_app_version2 + from .VersionPicker import _build_version_entries all_entries = _build_version_entries(_p) hide_unavail = False try: @@ -1239,7 +1239,7 @@ def onInstallClickFab(v, _p=p, _install_ui=_install_ui_ref, _all=_all_plugins_re # rebind _do_install so its internal loader uses fab dimensions def _do_install(_p, _install_ui, _all, _btn, _label, _btn_text_color, _act, on_finish_override=None, succ_download=None): - from ...core import install_plugin + from ...Core import install_plugin if not on_finish_override: _set_loading_fab(_btn, _label, _btn_text_color, _act, True) @@ -1247,7 +1247,7 @@ def _finish(ok): if ok: try: from elyx import assets - from ...utils.media import playSound + from ...utils.Media import playSound _snd = assets.sounds.install.path_str playSound(_snd, "sfx_install") except Exception: @@ -1979,7 +1979,7 @@ def _make_text_btn_ext(icon_name, text): translate_btn_ext = _make_text_btn_ext("msg_replace", str(strings["translate"])) def onTranslateClickExt(v, _p=p, _readme=fetched_readme): - from ...utils.translation import translate_plugin + from ...utils.Translation import translate_plugin text = _readme[0] if _readme[0] else None translate_plugin(_p, text_override=text) translate_btn_ext.setOnClickListener(OnClickListener(onTranslateClickExt)) @@ -2103,7 +2103,7 @@ def _make_text_btn(icon_name, text): translate_btn = _make_icon_only_btn("msg_replace") def onTranslateClick(v, _p=p): - from ...utils.translation import translate_plugin + from ...utils.Translation import translate_plugin translate_plugin(_p) translate_btn.setOnClickListener(OnClickListener(onTranslateClick)) @@ -2568,7 +2568,7 @@ def onTranslateBarClick(v, _p=p): return import threading import requests as _req - from ...utils.translation import _show_translate_sheet + from ...utils.Translation import _show_translate_sheet from java.util import Locale def _set_loading(loading): @@ -3053,7 +3053,7 @@ def _get_client_color(client): ) iv_lp.rightMargin = AndroidUtilities.dp(10) dep_row.addView(dep_iv, iv_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(dep_iv, dep_icon_str, icon_size_dp) # status icon: msg_select green / msg_cancel red @@ -3262,7 +3262,7 @@ def onTouch(self, v, event): logx(f"pluginProfile: beforeCreateView done, content_view={self.content_view}", True) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass @@ -3330,7 +3330,7 @@ def _make_scroll_item(self, act, plugin, item_w, text_color): except Exception: pass col.addView(iv, icon_container_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(iv, icon_str, size_dp) else: # same placeholder as ImportBottomSheet: circle with plugins_filled icon @@ -3397,7 +3397,7 @@ def _make_mini_card(self, act, plugin, text_color, gray_color): iv_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(size_dp), AndroidUtilities.dp(size_dp)) iv_lp.rightMargin = AndroidUtilities.dp(12) row.addView(iv, iv_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(iv, icon_str, size_dp) info = LinearLayout(act) @@ -3596,7 +3596,7 @@ def on_delete(v): import shutil import os import signal - from ...utils.paths import getPackItPluginDir + from ...utils.Paths import getPackItPluginDir try: shutil.rmtree(getPackItPluginDir(), ignore_errors=True) except Exception as e: diff --git a/packit/src/ui/PluginActivity/versionPicker.py b/packit/src/ui/pluginactivity/VersionPicker.py similarity index 98% rename from packit/src/ui/PluginActivity/versionPicker.py rename to packit/src/ui/pluginactivity/VersionPicker.py index e306cf3..4286fa6 100644 --- a/packit/src/ui/PluginActivity/versionPicker.py +++ b/packit/src/ui/pluginactivity/VersionPicker.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.ripple import safe_ripple as _safe_ripple -from ...utils.bulletins import factory as _pbf +from ...utils.Ripple import safe_ripple as _safe_ripple +from ...utils.Bulletins import factory as _pbf import threading from android_utils import run_on_ui_thread, OnClickListener, OnLongClickListener from client_utils import get_last_fragment @@ -51,7 +51,7 @@ def _build_version_entries(plugin): # returns list sorted newest first def _ver_key(v): try: - from ..PluginListActivity.helpers.utils import _parse_version + from ..pluginlistactivity.helpers.Utils import _parse_version return _parse_version(v) except Exception: return [] @@ -89,7 +89,7 @@ def _show_version_picker(act, plugin, install_ui, all_plugins, btn, label, btn_t from android.util import TypedValue from android.graphics.drawable import GradientDrawable from android_utils import OnClickListener - from ...utils.app_version import check_app_version as _check_app_version + from ...utils.AppVersion import check_app_version as _check_app_version import ctypes from java import dynamic_proxy @@ -102,7 +102,7 @@ def _show_version_picker(act, plugin, install_ui, all_plugins, btn, label, btn_t try: from elyx import settings as _s if _s.get("hide_unavailable_plugins", False): - from ...utils.app_version import check_app_version as _check_app_version + from ...utils.AppVersion import check_app_version as _check_app_version entries = [e for e in entries if not e["app_version"] or _check_app_version(e["app_version"])] except Exception: pass @@ -842,7 +842,7 @@ def _unwrap(): card.setScaleX(0.92) card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/PluginActivity/__init__.py b/packit/src/ui/pluginactivity/__init__.py similarity index 100% rename from packit/src/ui/PluginActivity/__init__.py rename to packit/src/ui/pluginactivity/__init__.py diff --git a/packit/src/ui/PluginListActivity/card.py b/packit/src/ui/pluginlistactivity/Card.py similarity index 95% rename from packit/src/ui/PluginListActivity/card.py rename to packit/src/ui/pluginlistactivity/Card.py index 1bb1c93..671fc24 100644 --- a/packit/src/ui/PluginListActivity/card.py +++ b/packit/src/ui/pluginlistactivity/Card.py @@ -42,9 +42,9 @@ Browser = None from .helpers.PluginActions import copy_plugin_link, share_plugin_file, view_plugin_code, report_plugin, download_plugin_file, translate_plugin -from .filter.tagLayoutListener import _TagsOverflowListener -from .helpers.utils import _check_app_version -from ..viewUtils import highlightQuery as _highlight_query +from .filter.TagLayoutListener import _TagsOverflowListener +from .helpers.Utils import _check_app_version +from ..ViewUtils import highlightQuery as _highlight_query def make_plugin_card(self, p): @@ -135,7 +135,7 @@ def create_icon_pill(icon_name, handler): def onIconClick(v, plugin=p): try: - from ..PluginActivity.fragment import show_plugin_profile + from ..pluginactivity.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -150,7 +150,7 @@ def onIconClick(v, plugin=p): # make_plugin_card runs off the UI thread (_load_initial_batch worker), # MediaDataController and BackupImageView.setImage must run on the UI thread - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker run_on_ui_thread(lambda: load_sticker(icon_view, icon_str, icon_size_dp)) except Exception as e: pass @@ -317,7 +317,7 @@ def onTagClick(v, url=tag_url): def onPlusClick(v, plugin=p): try: - from ..PluginActivity.fragment import show_plugin_profile + from ..pluginactivity.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or ""), scroll_to_tags=True) @@ -479,7 +479,7 @@ def _position_and_show(): pass try: - from ..PluginActivity.fragment import show_plugin_profile + from ..pluginactivity.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -487,7 +487,7 @@ def _position_and_show(): def onCardClick(v, plugin=p, row_ref=row, hint_ref=current_hint_ref, available=is_available): if not self._s_show_view_button: try: - from ..PluginActivity.fragment import show_plugin_profile + from ..pluginactivity.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -556,7 +556,7 @@ def do_install(): pass return try: - from ...core import install_plugin + from ...Core import install_plugin install_plugin( p, install_ui=self.install_ui, @@ -569,7 +569,7 @@ def do_install(): def do_download_relocated(): download_plugin_file(p) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception as e: pass @@ -577,7 +577,7 @@ def do_download_relocated(): def do_copy_relocated(): copy_plugin_link(p, self.repo_id or self.title, copyLinkSoundPath) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as e: pass @@ -585,7 +585,7 @@ def do_copy_relocated(): def do_share_relocated(): share_plugin_file(p, str(display_name), act_for_share) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception as e: pass @@ -593,7 +593,7 @@ def do_share_relocated(): def do_code_relocated(): view_plugin_code(p, act) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception as e: pass @@ -604,7 +604,7 @@ def do_translate_relocated(): def do_report_relocated(): report_plugin(p, act, repo_id=self.repo_id or str(p.get("_repo_id") or "")) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Reporting") except Exception as e: pass @@ -628,10 +628,10 @@ def do_report_relocated(): def show_plugin_actions_menu(anchor_view): try: - from ..contextMenu import show_plugin_context_menu + from ..ContextMenu import show_plugin_context_menu def do_install(): - from ...core import install_plugin + from ...Core import install_plugin install_plugin( p, install_ui=self.install_ui, @@ -642,7 +642,7 @@ def do_install(): def do_download(): download_plugin_file(p) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception: pass @@ -650,7 +650,7 @@ def do_download(): def do_copy(): copy_plugin_link(p, self.repo_id or self.title, copyLinkSoundPath) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception: pass @@ -658,7 +658,7 @@ def do_copy(): def do_share(): share_plugin_file(p, str(display_name), act_for_share) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception: pass @@ -666,7 +666,7 @@ def do_share(): def do_code(): view_plugin_code(p, act) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception: pass @@ -677,7 +677,7 @@ def do_translate(): def do_report(): report_plugin(p, act, repo_id=self.repo_id or str(p.get("_repo_id") or "")) try: - from ...ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ...ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Reporting") except Exception: pass diff --git a/packit/src/ui/PluginListActivity/fragment.py b/packit/src/ui/pluginlistactivity/Fragment.py similarity index 95% rename from packit/src/ui/PluginListActivity/fragment.py rename to packit/src/ui/pluginlistactivity/Fragment.py index 179b571..e738363 100644 --- a/packit/src/ui/PluginListActivity/fragment.py +++ b/packit/src/ui/pluginlistactivity/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.netQueue import run_io +from ...utils.NetQueue import run_io import re import json import threading @@ -25,28 +25,28 @@ from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper, BackupImageView, EditTextBoldCursor except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper, BackupImageView, EditTextBoldCursor failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities, MediaDataController, ImageLocation, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, MediaDataController, ImageLocation, R as R_tg failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android_utils import OnClickListener try: from com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.utils.text import LocaleUtils except Exception as e: @@ -55,12 +55,12 @@ from org.telegram.ui.ActionBar import ActionBarPopupWindow except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import ActionBarPopupWindow failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.net import Uri except Exception as e: import android_utils as _au; _au.log(f"import android.net import Uri failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger.browser import Browser except Exception: @@ -69,47 +69,47 @@ from androidx.core.content import ContextCompat except Exception as e: import android_utils as _au; _au.log(f"import androidx.core.content import ContextCompat failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.graphics.drawable import GradientDrawable, RippleDrawable except Exception as e: import android_utils as _au; _au.log(f"import android.graphics.drawable import GradientDrawable, RippleDrawable failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.graphics import Color as AColor, PorterDuff except Exception as e: import android_utils as _au; _au.log(f"import android.graphics import Color, PorterDuff failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.content.res import ColorStateList as AColorStateList except Exception as e: import android_utils as _au; _au.log(f"import android.content.res import ColorStateList failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.view import View as AView, Gravity as AGravity except Exception as e: import android_utils as _au; _au.log(f"import android.view import View, Gravity failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.widget import FrameLayout as AFrame, LinearLayout as ALinear, TextView as AText, ImageView as AImage except Exception as e: import android_utils as _au; _au.log(f"import android.widget import FrameLayout, LinearLayout, TextView, ImageView failed: {e}") - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from .helpers import uiHelpers +from .helpers import UiHelpers from .sheets.RepoBottomSheet import show_repo_sheet from .sheets.SortBottomSheet import show_sort_menu from .sheets.AISearchSheet import show_ai_search_sheet -from .filter.filterDrawer import show_tag_drawer -from ...utils import search as search_mod -from .filter import filterEngine as tag_mod +from .filter.FilterDrawer import show_tag_drawer +from ...utils import Search as search_mod +from .filter import FilterEngine as tag_mod from .helpers.PluginActions import copy_plugin_link, share_plugin_file, view_plugin_code, report_plugin, download_plugin_file, translate_plugin -from ...utils.media import playSound -from ...core import install_plugin -from . import card as _card +from ...utils.Media import playSound +from ...Core import install_plugin +from . import Card as _card -from .helpers.utils import ( +from .helpers.Utils import ( _count_active_repos, _plural_form, _format_plural, _build_stats_label, _build_plugin_count_label, _parse_version, _check_app_version, _filter_unavailable, @@ -158,34 +158,34 @@ def _parse_github_url(self, url): return None, None def _apply_press_scale(self, view): - uiHelpers.apply_press_scale(view) + UiHelpers.apply_press_scale(view) def _apply_press_scale_on_target(self, view, target): - uiHelpers.apply_press_scale_on_target(view, target) + UiHelpers.apply_press_scale_on_target(view, target) def _create_close_button(self, act, text=None): - return uiHelpers.create_close_button(act, text) + return UiHelpers.create_close_button(act, text) def _setup_bottom_sheet(self, sheet): - uiHelpers.setup_bottom_sheet(sheet) + UiHelpers.setup_bottom_sheet(sheet) def _create_rounded_bg(self, color): - return uiHelpers.create_rounded_bg(color) + return UiHelpers.create_rounded_bg(color) def _format_file_size(self, bytes_val): - return uiHelpers.format_file_size(bytes_val) + return UiHelpers.format_file_size(bytes_val) def _make_info_chip(self, act, text, color_key, size_sp=11): - return uiHelpers.make_info_chip(act, text, color_key, size_sp) + return UiHelpers.make_info_chip(act, text, color_key, size_sp) def _create_pill(self, act, background, pressed, padding_h=14, padding_v=8): - return uiHelpers.create_pill(act, background, pressed, padding_h, padding_v) + return UiHelpers.create_pill(act, background, pressed, padding_h, padding_v) def _resolve_icon(self, name): - return uiHelpers.resolve_icon(name) + return UiHelpers.resolve_icon(name) def _get_theme_colors(self): - return uiHelpers.get_theme_colors() + return UiHelpers.get_theme_colors() def open(self): fragment = get_last_fragment() @@ -220,10 +220,10 @@ def open(self): show_repo_sheet(self, repos) def _create_circular_loading(self, act, size_dp=20): - return uiHelpers.create_circular_loading(act, size_dp) + return UiHelpers.create_circular_loading(act, size_dp) def _create_center_loading_animation(self, parent_layout): - return uiHelpers.create_center_loading_animation(parent_layout) + return UiHelpers.create_center_loading_animation(parent_layout) def _reload_current_plugins(self, repo_id=None): delegate = getattr(self, '_active_delegate', None) @@ -246,9 +246,9 @@ def load_task(): if not repo_url: continue try: from ...network import Storage - from ...utils import cachedRepos + from ...utils import CachedRepos entries, error = Storage.fetch_plugins( - cachedRepos.plugins_url(repo, repo_url)) + CachedRepos.plugins_url(repo, repo_url)) if error: continue repo_name = repo.get("name", "Unknown") @@ -263,15 +263,15 @@ def load_task(): repo = next((r for r in repos if r.get("id") == repo_id), None) if not repo: return from ...network import Storage - from ...utils import cachedRepos - plugins, error = Storage.fetch_plugins(cachedRepos.plugins_url(repo)) + from ...utils import CachedRepos + plugins, error = Storage.fetch_plugins(CachedRepos.plugins_url(repo)) if error: raise Exception(error) # the sources screen has no other way to know how big a # source is: repomap only points at this file by url try: - from ...utils import repoStats - repoStats.remember(repo_id, plugins=len(plugins)) + from ...utils import RepoStats + RepoStats.remember(repo_id, plugins=len(plugins)) except Exception as e: logx(f"InstallUI: repo stats write failed: {e}", True) run_on_ui_thread(lambda: self._update_current_fragment_plugins(plugins)) @@ -460,7 +460,7 @@ def onFragmentDestroy(self, *_): logx(f"InstallUI: NoInternetBanner unregister error: {e}", False) try: if hasattr(self, 'content_view') and self.content_view is not None: - from ...ui.AchievementsActivity.service.AchivementsEngine import unregister_bulletin_container + from ...ui.achievementsactivity.service.AchivementsEngine import unregister_bulletin_container unregister_bulletin_container(self.content_view) parent = self.content_view.getParent() if parent is not None: @@ -474,7 +474,7 @@ def onFragmentDestroy(self, *_): except Exception: pass try: - from ...utils.localConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig showTgc = LocalConfig.get("showTgc", False) if not showTgc: count = LocalConfig.get("installUiOpenCount", 0) + 1 @@ -484,7 +484,7 @@ def onFragmentDestroy(self, *_): def _show(): try: - from .sheets.tgChannelSheet import show_tg_channel_sheet + from .sheets.TgChannelSheet import show_tg_channel_sheet frag = get_last_fragment() if not frag: return @@ -516,7 +516,7 @@ def beforeCreateView(self): # 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 + from . import ListView as _lv from android_utils import run_on_ui_thread act = get_last_fragment().getContext() try: @@ -691,7 +691,7 @@ def build_list_with_sort(self, sort_type: str, q=None): filtered = [] elif getattr(self, "selected_saved", None) is not None and self.selected_saved != {"saved", "unsaved"}: try: - from ..PluginActivity.fragment import _read_saved_plugins + from ..pluginactivity.Fragment import _read_saved_plugins saved_ids = set(_read_saved_plugins()) show_saved = "saved" in self.selected_saved show_unsaved = "unsaved" in self.selected_saved diff --git a/packit/src/ui/PluginListActivity/listView.py b/packit/src/ui/pluginlistactivity/ListView.py similarity index 99% rename from packit/src/ui/PluginListActivity/listView.py rename to packit/src/ui/pluginlistactivity/ListView.py index 1c95d5b..3229491 100644 --- a/packit/src/ui/PluginListActivity/listView.py +++ b/packit/src/ui/pluginlistactivity/ListView.py @@ -32,12 +32,12 @@ except Exception as e: import android_utils as _au; _au.log(f"listView: import AndroidUtilities, R_tg failed: {e}") -from .helpers import uiHelpers +from .helpers import UiHelpers from .sheets.AISearchSheet import show_ai_search_sheet from .sheets.SortBottomSheet import show_sort_menu -from .filter.filterDrawer import show_tag_drawer -from ...utils.media import playSound -from .helpers.utils import _build_plugin_count_label +from .filter.FilterDrawer import show_tag_drawer +from ...utils.Media import playSound +from .helpers.Utils import _build_plugin_count_label @@ -274,7 +274,7 @@ 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 ...DexLoader import catalogChromeCreate from elyx import settings as _s live_search = bool(_s.get("live_search", True)) try: @@ -634,7 +634,7 @@ def build_list_view(self) -> View: self.content_view = FrameLayout(act) self.content_view.setBackgroundColor(self.main_bg_color) - from ...ui.AchievementsActivity.service.AchivementsEngine import register_bulletin_container + from ...ui.achievementsactivity.service.AchivementsEngine import register_bulletin_container register_bulletin_container(self.content_view) chrome = None try: @@ -936,7 +936,7 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): self.search.addTextChangedListener(_SearchTextWatcherWithClear(self, clear_btn)) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass diff --git a/packit/src/ui/PluginListActivity/__init__.py b/packit/src/ui/pluginlistactivity/__init__.py similarity index 100% rename from packit/src/ui/PluginListActivity/__init__.py rename to packit/src/ui/pluginlistactivity/__init__.py diff --git a/packit/src/ui/PluginListActivity/filter/filterDrawer.py b/packit/src/ui/pluginlistactivity/filter/FilterDrawer.py similarity index 99% rename from packit/src/ui/PluginListActivity/filter/filterDrawer.py rename to packit/src/ui/pluginlistactivity/filter/FilterDrawer.py index 03a0c57..fef950d 100644 --- a/packit/src/ui/PluginListActivity/filter/filterDrawer.py +++ b/packit/src/ui/pluginlistactivity/filter/FilterDrawer.py @@ -12,7 +12,7 @@ from java import dynamic_proxy from hook_utils import find_class from android_utils import OnClickListener -from . import filterEngine +from . import FilterEngine try: from org.telegram.ui.ActionBar import Theme except Exception as e: @@ -926,14 +926,14 @@ def _populate_tags(self): try: self._tags_list.removeAllViews() self._tag_rows.clear() - self._tags_summary = filterEngine.collect_tags(self.plugins) + self._tags_summary = FilterEngine.collect_tags(self.plugins) if self._current_selected is None: self._current_selected = set(self._tags_summary.keys()) for tag_name, count in self._tags_summary.items(): # use localized label for unsorted, keep key internal - if tag_name == filterEngine._UNSORTED_KEY: + if tag_name == FilterEngine._UNSORTED_KEY: try: from elyx import strings as _s display_name = str(_s["filter_tag_unsorted"]) diff --git a/packit/src/ui/PluginListActivity/filter/filterEngine.py b/packit/src/ui/pluginlistactivity/filter/FilterEngine.py similarity index 100% rename from packit/src/ui/PluginListActivity/filter/filterEngine.py rename to packit/src/ui/pluginlistactivity/filter/FilterEngine.py diff --git a/packit/src/ui/PluginListActivity/filter/tagLayoutListener.py b/packit/src/ui/pluginlistactivity/filter/TagLayoutListener.py similarity index 100% rename from packit/src/ui/PluginListActivity/filter/tagLayoutListener.py rename to packit/src/ui/pluginlistactivity/filter/TagLayoutListener.py diff --git a/packit/src/ui/PluginListActivity/filter/__init__.py b/packit/src/ui/pluginlistactivity/filter/__init__.py similarity index 100% rename from packit/src/ui/PluginListActivity/filter/__init__.py rename to packit/src/ui/pluginlistactivity/filter/__init__.py diff --git a/packit/src/ui/PluginListActivity/helpers/PluginActions.py b/packit/src/ui/pluginlistactivity/helpers/PluginActions.py similarity index 94% rename from packit/src/ui/PluginListActivity/helpers/PluginActions.py rename to packit/src/ui/pluginlistactivity/helpers/PluginActions.py index c999b40..15b65a7 100644 --- a/packit/src/ui/PluginListActivity/helpers/PluginActions.py +++ b/packit/src/ui/pluginlistactivity/helpers/PluginActions.py @@ -3,21 +3,21 @@ from packutil import logx -from ....utils.bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf from client_utils import get_last_fragment from hook_utils import find_class from .ReportService import report_plugin -from ....utils.translation import translate_plugin +from ....utils.Translation import translate_plugin try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, R as R_tg failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.net import Uri try: from org.telegram.messenger.browser import Browser @@ -30,7 +30,7 @@ def copy_plugin_link(plugin_info: dict, repo_title: str, sound_path: str = None): try: if sound_path: - from ....utils.media import playSound + from ....utils.Media import playSound playSound(sound_path, "sfx_copy_link") except Exception: pass @@ -50,7 +50,7 @@ def copy_plugin_link(plugin_info: dict, repo_title: str, sound_path: str = None) plugin_name = plugin_info.get("name") or plugin_info.get("id") or "Unknown" _pbf(container, resource_provider).createSimpleBulletin(R_tg.raw.voip_invite, strings("plugin_link_copied", plugin_name)).show() try: - from ....ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ....ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as e: logx(f"copy_plugin_link: achievements increment error: {e}", False) @@ -60,10 +60,10 @@ def copy_plugin_link(plugin_info: dict, repo_title: str, sound_path: str = None) def share_plugin_file(plugin_info: dict, display_name: str, activity): try: - from ....utils.share import share_plugin_file as _share_plugin_file + from ....utils.Share import share_plugin_file as _share_plugin_file _share_plugin_file(plugin_info, display_name, activity) try: - from ....ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ....ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception as e: logx(f"share_plugin_file: achievements increment error: {e}", False) @@ -183,7 +183,7 @@ def _show_download_ok(path): strings("download_saved", folder=folder) ).show() try: - from ....ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ....ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception as e: logx(f"download: achievements increment error: {e}", False) @@ -235,7 +235,7 @@ def view_plugin_code(plugin_info: dict, activity): _pbf(activity.getWindow().getDecorView(), None).createErrorBulletin(strings["failed_to_open_url"]).show() return try: - from ....ui.AchievementsActivity.service.AchivementsEngine import increment_category + from ....ui.achievementsactivity.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception as e: logx(f"view_plugin_code: achievements increment error: {e}", False) diff --git a/packit/src/ui/PluginListActivity/helpers/ReportService.py b/packit/src/ui/pluginlistactivity/helpers/ReportService.py similarity index 91% rename from packit/src/ui/PluginListActivity/helpers/ReportService.py rename to packit/src/ui/pluginlistactivity/helpers/ReportService.py index fe01710..b46aa41 100644 --- a/packit/src/ui/PluginListActivity/helpers/ReportService.py +++ b/packit/src/ui/pluginlistactivity/helpers/ReportService.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ....utils.bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf from android_utils import run_on_ui_thread from client_utils import get_last_fragment @@ -22,7 +22,7 @@ def report_plugin(plugin_info: dict, activity, repo_id: str = ""): rid = repo_id or str(plugin_info.get("repo_id") or plugin_info.get("_repo_id") or "") pid = str(plugin_info.get("id") or "") - from ....ui.reportDialog import _load_report_settings + from ....ui.ReportDialog import _load_report_settings from elyx import strings forum_username, topic_msg_id = _load_report_settings(rid) @@ -43,7 +43,7 @@ def _show_missing(field): run_on_ui_thread(lambda: _show_missing("topic_msg_id")) return - from ....ui.reportDialog import show_report_dialog + from ....ui.ReportDialog import show_report_dialog _name = name _rid = rid _pid = pid diff --git a/packit/src/ui/PluginListActivity/helpers/uiHelpers.py b/packit/src/ui/pluginlistactivity/helpers/UiHelpers.py similarity index 100% rename from packit/src/ui/PluginListActivity/helpers/uiHelpers.py rename to packit/src/ui/pluginlistactivity/helpers/UiHelpers.py diff --git a/packit/src/ui/PluginListActivity/helpers/utils.py b/packit/src/ui/pluginlistactivity/helpers/Utils.py similarity index 98% rename from packit/src/ui/PluginListActivity/helpers/utils.py rename to packit/src/ui/pluginlistactivity/helpers/Utils.py index 80d9554..e79327b 100644 --- a/packit/src/ui/PluginListActivity/helpers/utils.py +++ b/packit/src/ui/pluginlistactivity/helpers/Utils.py @@ -81,7 +81,7 @@ def _parse_version(v_str): return (0,) def _check_app_version(app_version_expr): - from ....utils.app_version import check_app_version + from ....utils.AppVersion import check_app_version return check_app_version(app_version_expr) def _filter_unavailable(plugins): diff --git a/packit/src/ui/PluginListActivity/helpers/__init__.py b/packit/src/ui/pluginlistactivity/helpers/__init__.py similarity index 100% rename from packit/src/ui/PluginListActivity/helpers/__init__.py rename to packit/src/ui/pluginlistactivity/helpers/__init__.py diff --git a/packit/src/ui/PluginListActivity/sheets/AISearchSheet.py b/packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py similarity index 98% rename from packit/src/ui/PluginListActivity/sheets/AISearchSheet.py rename to packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py index dfc5cd2..b70d5c2 100644 --- a/packit/src/ui/PluginListActivity/sheets/AISearchSheet.py +++ b/packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py @@ -2,8 +2,8 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ....utils.ripple import safe_ripple as _safe_ripple -from ....utils.bulletins import factory as _pbf +from ....utils.Ripple import safe_ripple as _safe_ripple +from ....utils.Bulletins import factory as _pbf import ctypes import json import base64 @@ -37,7 +37,7 @@ def _load_gemini_cache() -> dict: try: - from ....utils.paths import getGeminiCachePath + from ....utils.Paths import getGeminiCachePath path = getGeminiCachePath() if not os.path.exists(path): return {} @@ -51,7 +51,7 @@ def _load_gemini_cache() -> dict: def _save_gemini_cache(cache: dict) -> None: try: - from ....utils.paths import getGeminiCachePath + from ....utils.Paths import getGeminiCachePath path = getGeminiCachePath() os.makedirs(os.path.dirname(path), exist_ok=True) with open(path, "w", encoding="utf-8") as f: @@ -92,8 +92,8 @@ def _get_device_id() -> str: def _load_gemini_key() -> "str | None": # returns full key string or None try: - from ....nativeLoader import loadPackitKey - from ....utils.paths import getKeysDir + from ....NativeLoader import loadPackitKey + from ....utils.Paths import getKeysDir except Exception as e: logx(f"AISearchSheet: _load_gemini_key import failed: {e}", False) return None @@ -824,7 +824,7 @@ def _on_error(): sheet.setCustomView(root) try: - from ...viewUtils import applyFontToTree + from ...ViewUtils import applyFontToTree applyFontToTree(root) except Exception as e: logx(f"AISearchSheet: applyFontToTree failed: {e}", False) diff --git a/packit/src/ui/PluginListActivity/sheets/depsSheet.py b/packit/src/ui/pluginlistactivity/sheets/DepsSheet.py similarity index 99% rename from packit/src/ui/PluginListActivity/sheets/depsSheet.py rename to packit/src/ui/pluginlistactivity/sheets/DepsSheet.py index be04946..dac8122 100644 --- a/packit/src/ui/PluginListActivity/sheets/depsSheet.py +++ b/packit/src/ui/pluginlistactivity/sheets/DepsSheet.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ....utils.bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf from android.view import View, Gravity from android.widget import LinearLayout, TextView, FrameLayout, ImageView from android.util import TypedValue @@ -257,7 +257,7 @@ def on_cancel_click(v): sheet.setCustomView(root) try: - from ...viewUtils import applyFontToTree + from ...ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass @@ -326,7 +326,7 @@ def _make_dep_card(act, dep_id, dep_name, dep_version, dep_author, dep_min_versi icon_lp = LinearLayout.LayoutParams(icon_size_px, icon_size_px) icon_lp.rightMargin = AndroidUtilities.dp(10) main_row.addView(icon_view, icon_lp) - from ....utils.stickers import load_sticker + from ....utils.Stickers import load_sticker load_sticker(icon_view, dep_icon, icon_size_dp) except Exception as e: logx(f"depsSheet: icon init error for '{dep_id}': {e}", False) @@ -483,7 +483,7 @@ def _do_refresh(): logx(f"depsSheet: _do_refresh error for '{dep_id}': {e}", False) def on_install(v): - from ....core import install_plugin + from ....Core import install_plugin if observer_registered[0] is None: try: from java import dynamic_proxy diff --git a/packit/src/ui/PluginListActivity/sheets/RepoBottomSheet.py b/packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py similarity index 97% rename from packit/src/ui/PluginListActivity/sheets/RepoBottomSheet.py rename to packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py index 412fc2a..c4f7c3b 100644 --- a/packit/src/ui/PluginListActivity/sheets/RepoBottomSheet.py +++ b/packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py @@ -15,22 +15,22 @@ from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def show_repo_sheet(install_ui, repos, on_select=None): @@ -247,7 +247,7 @@ def on_close(v): root.addView(close_btn, LayoutHelper.createLinear(-1, -2, 0, 8, 0, 0)) sheet.setCustomView(root) try: - from ...viewUtils import applyFontToTree + from ...ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/PluginListActivity/sheets/SortBottomSheet.py b/packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py similarity index 97% rename from packit/src/ui/PluginListActivity/sheets/SortBottomSheet.py rename to packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py index 7e1d1a5..064d9f2 100644 --- a/packit/src/ui/PluginListActivity/sheets/SortBottomSheet.py +++ b/packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py @@ -15,22 +15,22 @@ from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ....utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ....utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() _SORT_ICONS = { "alpha_az": "msg_archive", @@ -234,7 +234,7 @@ def on_close_sort(v): sort_sheet.setCustomView(sort_root) try: - from ...viewUtils import applyFontToTree + from ...ViewUtils import applyFontToTree applyFontToTree(sort_root) except Exception: pass diff --git a/packit/src/ui/PluginListActivity/sheets/tgChannelSheet.py b/packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py similarity index 96% rename from packit/src/ui/PluginListActivity/sheets/tgChannelSheet.py rename to packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py index debcf26..c13697c 100644 --- a/packit/src/ui/PluginListActivity/sheets/tgChannelSheet.py +++ b/packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py @@ -44,7 +44,7 @@ def _on_touch(v, event): def show_tg_channel_sheet(activity, resource_provider): try: from elyx import strings - from ....utils.localConfig import LocalConfig + from ....utils.LocalConfig import LocalConfig sheet = BottomSheet(activity, False, resource_provider) sheet.fixNavigationBar() @@ -86,7 +86,7 @@ def onClick(self, v): sheet.dismiss() LocalConfig.set("showTgc", True) try: - from ....ui.AchievementsActivity.service.AchivementsEngine import unlock_secret + from ....ui.achievementsactivity.service.AchivementsEngine import unlock_secret unlock_secret("subscriber") except Exception as e: logx(f"tgChannelSheet: achievement unlock error: {e}", False) @@ -119,7 +119,7 @@ def onClick(self, v): scroll.addView(frame) sheet.setCustomView(scroll) try: - from ...viewUtils import applyFontToTree + from ...ViewUtils import applyFontToTree applyFontToTree(scroll) except Exception: pass diff --git a/packit/src/ui/PluginListActivity/sheets/__init__.py b/packit/src/ui/pluginlistactivity/sheets/__init__.py similarity index 100% rename from packit/src/ui/PluginListActivity/sheets/__init__.py rename to packit/src/ui/pluginlistactivity/sheets/__init__.py diff --git a/packit/src/ui/pluginsUpdates/clearIgnoreListDialog.py b/packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py similarity index 99% rename from packit/src/ui/pluginsUpdates/clearIgnoreListDialog.py rename to packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py index 2ba143b..14e051f 100644 --- a/packit/src/ui/pluginsUpdates/clearIgnoreListDialog.py +++ b/packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py @@ -174,7 +174,7 @@ def _make_btn(act, text: str, accent: bool): def _get_index_path(pkg: str, rm_rid: str) -> str: - from ...utils.paths import getRepoIndexPath + from ...utils.Paths import getRepoIndexPath return getRepoIndexPath(rm_rid) @@ -421,7 +421,7 @@ def _on_cancel_click(v): card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsUpdates/fragment.py b/packit/src/ui/pluginsupdates/Fragment.py similarity index 98% rename from packit/src/ui/pluginsUpdates/fragment.py rename to packit/src/ui/pluginsupdates/Fragment.py index b390984..8515fce 100644 --- a/packit/src/ui/pluginsUpdates/fragment.py +++ b/packit/src/ui/pluginsupdates/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.netQueue import run_io +from ...utils.NetQueue import run_io import json import os import threading @@ -19,22 +19,22 @@ from org.telegram.ui.ActionBar import Theme except Exception as e: logx(f"pluginsUpdates: import Theme failed: {e}", False) - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: logx(f"pluginsUpdates: import LayoutHelper failed: {e}", False) - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities, ApplicationLoader except Exception as e: logx(f"pluginsUpdates: import AndroidUtilities/ApplicationLoader failed: {e}", False) - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.ui.components.templates import UniversalFragment except Exception as e: logx(f"pluginsUpdates: import UniversalFragment failed: {e}", False) - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android_utils import OnClickListener @@ -43,11 +43,11 @@ from elyx import settings, strings except Exception as e: logx(f"pluginsUpdates: import elyx.settings failed: {e}", False) - from ...utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _get_index_path(pkg: str, rm_rid: str) -> str: - from ...utils.paths import getRepoIndexPath + from ...utils.Paths import getRepoIndexPath return getRepoIndexPath(rm_rid) @@ -77,8 +77,8 @@ def _read_index(pkg: str, rm_rid: str) -> list: def _get_repo_plugins_url(pkg: str, rm_rid: str, fallback_url: str) -> str: - from ...utils import cachedRepos - return cachedRepos.plugins_url(rm_rid, fallback_url) + from ...utils import CachedRepos + return CachedRepos.plugins_url(rm_rid, fallback_url) def _fetch_repo_plugins(url: str) -> dict: @@ -178,7 +178,7 @@ def _check_updates(pkg: str) -> list: repo_app_ver = str(repo_info.get("app_version") or "") if repo_app_ver: try: - from ...utils.app_version import check_app_version + from ...utils.AppVersion import check_app_version if not check_app_version(repo_app_ver): continue except Exception as e: @@ -332,7 +332,7 @@ def onFragmentCreate(self, *_): def onFragmentDestroy(self, *_): self._alive[0] = False try: - from ...core import remove_install_listener + from ...Core import remove_install_listener for fn in list(self._active_listeners): remove_install_listener(fn) self._active_listeners.clear() @@ -641,7 +641,7 @@ def _make_icon_btn(res_name: str): def _open_ignore_list_dialog(self): try: - from .clearIgnoreListDialog import show_clear_ignore_list_dialog + from .ClearIgnoreListDialog import show_clear_ignore_list_dialog show_clear_ignore_list_dialog(self._act, on_close=self._on_refresh_click) except Exception as e: logx(f"pluginsUpdates: _open_ignore_list_dialog error: {e}", False) @@ -663,7 +663,7 @@ def task(): run_on_queue(task) try: - from .hideAllDialog import show_hide_all_dialog + from .HideAllDialog import show_hide_all_dialog show_hide_all_dialog(self._act, on_confirm) except Exception as e: logx(f"pluginsUpdates: _on_ignore_all_click error: {e}", False) @@ -925,7 +925,7 @@ def task(): try: # purge stale entries first try: - from ...utils.installIndex import purge_missing + from ...utils.InstallIndex import purge_missing purge_missing() except Exception as e: logx(f"pluginsUpdates: purge_missing error: {e}", False) @@ -954,7 +954,7 @@ def task(): pass def _open_catalog(): try: - from ..PluginListActivity.fragment import InstallUI + from ..pluginlistactivity.Fragment import InstallUI InstallUI(self._plugin).open() except Exception as e: logx(f"pluginsUpdates: _open_catalog error: {e}", False) @@ -1242,7 +1242,7 @@ def _make_update_card(self, act, item: dict): icon_lp = LinearLayout.LayoutParams(dp(icon_size_dp), dp(icon_size_dp)) icon_lp.rightMargin = dp(12) top_row.addView(icon_view, icon_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"pluginsUpdates: icon init error for '{pid}': {e}", False) @@ -1427,8 +1427,8 @@ def task(): def on_ui(): try: - from ..PluginListActivity.fragment import InstallUI - from ..PluginActivity.fragment import show_plugin_profile + from ..pluginlistactivity.Fragment import InstallUI + from ..pluginactivity.Fragment import show_plugin_profile install_ui = InstallUI(plugin) all_plugins = [{"id": k, **v} for k, v in repo_plugins.items() if isinstance(v, dict)] show_plugin_profile(plugin_data, install_ui, all_plugins=all_plugins, repo_id=repo_id) @@ -1445,7 +1445,7 @@ def on_ui(): def _show_ignore_dialog(self, pid: str, repo_id: str, repo_version: str, card_view): try: - from .hideDialog import show_hide_dialog + from .HideDialog import show_hide_dialog def on_apply(forever: bool): self._apply_ignore(pid, repo_id, repo_version, forever, card_view) @@ -1535,11 +1535,11 @@ def set_btn_state(state: str): def task(): try: - from ...core import install_plugin + from ...Core import install_plugin from ...network import Storage - from ...utils import cachedRepos + from ...utils import CachedRepos - plugins_url = cachedRepos.plugins_url(repo) + plugins_url = CachedRepos.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) @@ -1564,7 +1564,7 @@ def on_finish(ok): if not ok: run_on_ui_thread(lambda: set_btn_state("idle")) - from ...core import add_install_listener, remove_install_listener + from ...Core import add_install_listener, remove_install_listener listener_ref = [None] @@ -2011,14 +2011,14 @@ def set_btn_state(state: str): def task(): try: - from ...core import install_plugin_silent - from ...utils.paths import getPluginsDir + from ...Core import install_plugin_silent + from ...utils.Paths import getPluginsDir from ...network import Storage - from ...utils import cachedRepos + from ...utils import CachedRepos import requests as _requests import os as _os - plugins_url = cachedRepos.plugins_url(repo) + plugins_url = CachedRepos.plugins_url(repo) if not plugins_url: logx(f"pluginsUpdates: _install_update_silent no plugins url for '{repo_id}'", True) run_on_ui_thread(lambda: set_btn_state("idle")) @@ -2126,7 +2126,7 @@ def _on_update_all_click(self): # elyx plugins install in parallel via existing _install_update (shows install dialog) # non-elyx plugins install sequentially via _install_update_silent: done[i] → start[i+1] - from ...core import _is_elyx_plugin + from ...Core import _is_elyx_plugin elyx_items = [(item, btn, icon) for item, btn, icon in pending if _is_elyx_plugin(item)] silent_items = [(item, btn, icon) for item, btn, icon in pending if not _is_elyx_plugin(item)] diff --git a/packit/src/ui/pluginsUpdates/hideAllDialog.py b/packit/src/ui/pluginsupdates/HideAllDialog.py similarity index 99% rename from packit/src/ui/pluginsUpdates/hideAllDialog.py rename to packit/src/ui/pluginsupdates/HideAllDialog.py index b10d7ce..0b98c88 100644 --- a/packit/src/ui/pluginsUpdates/hideAllDialog.py +++ b/packit/src/ui/pluginsupdates/HideAllDialog.py @@ -246,7 +246,7 @@ def _dismiss(on_end=None): card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsUpdates/hideDialog.py b/packit/src/ui/pluginsupdates/HideDialog.py similarity index 99% rename from packit/src/ui/pluginsUpdates/hideDialog.py rename to packit/src/ui/pluginsupdates/HideDialog.py index 63d28c4..3e3c132 100644 --- a/packit/src/ui/pluginsUpdates/hideDialog.py +++ b/packit/src/ui/pluginsupdates/HideDialog.py @@ -309,7 +309,7 @@ def _open_mode_picker(): card.setScaleY(0.92) try: - from ..viewUtils import applyFontToTree + from ..ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsUpdates/startupSheet.py b/packit/src/ui/pluginsupdates/StartupSheet.py similarity index 98% rename from packit/src/ui/pluginsUpdates/startupSheet.py rename to packit/src/ui/pluginsupdates/StartupSheet.py index 563928e..d457e78 100644 --- a/packit/src/ui/pluginsUpdates/startupSheet.py +++ b/packit/src/ui/pluginsupdates/StartupSheet.py @@ -30,7 +30,7 @@ except Exception as e: logx(f"startupSheet: import elyx failed: {e}", False) -from .fragment import ( +from .Fragment import ( _get_repos, _check_updates, _filter_ignored, _ignore_until_next, _ignore_forever, ) @@ -127,7 +127,7 @@ def _make_item_card(act, item: dict, plugin_ref, on_action): icon_lp = LinearLayout.LayoutParams(dp(icon_size_dp), dp(icon_size_dp)) icon_lp.rightMargin = dp(12) header_row.addView(icon_view, icon_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"startupSheet: icon error for '{pid}': {e}", False) @@ -631,9 +631,9 @@ def _do_install(item: dict, plugin, set_btn_state=None): def task(): try: - from ...deeplinks.install import _resolvePluginsUrl - from ...utils.paths import getPluginsDir - from ...core import install_plugin_silent + from ...deeplinks.Install import _resolvePluginsUrl + from ...utils.Paths import getPluginsDir + from ...Core import install_plugin_silent import requests as _req import os @@ -712,7 +712,7 @@ def check_and_show_startup_updates(plugin=None): def task(): try: try: - from ...utils.installIndex import purge_missing + from ...utils.InstallIndex import purge_missing purge_missing() except Exception as e: logx(f"startupSheet: purge_missing error: {e}", False) diff --git a/packit/src/ui/pluginsUpdates/__init__.py b/packit/src/ui/pluginsupdates/__init__.py similarity index 100% rename from packit/src/ui/pluginsUpdates/__init__.py rename to packit/src/ui/pluginsupdates/__init__.py diff --git a/packit/src/ui/ReposActivity/actions.py b/packit/src/ui/reposactivity/Actions.py similarity index 98% rename from packit/src/ui/ReposActivity/actions.py rename to packit/src/ui/reposactivity/Actions.py index c1c3ebb..70bda0d 100644 --- a/packit/src/ui/ReposActivity/actions.py +++ b/packit/src/ui/reposactivity/Actions.py @@ -29,8 +29,8 @@ except Exception as e: import android_utils as _au; _au.log(f"repos actions: import R failed: {e}") -from ...utils.bulletins import factory as _pbf -from ..contextMenu import show_plugin_context_menu +from ...utils.Bulletins import factory as _pbf +from ..ContextMenu import show_plugin_context_menu from . import notify_repos_changed @@ -276,12 +276,12 @@ def add_repository(act, delegate): if len(repos) >= 10: BulletinHelper.show_error(str(strings.max_repositories_allowed)) return - from .addSheet import show_add_repo_dialog + from .AddSheet import show_add_repo_dialog show_add_repo_dialog(act, delegate) def edit_repository(act, delegate, repo: dict): - from .addSheet import show_edit_repo_dialog + from .AddSheet import show_edit_repo_dialog show_edit_repo_dialog(act, delegate, repo) diff --git a/packit/src/ui/ReposActivity/addSheet.py b/packit/src/ui/reposactivity/AddSheet.py similarity index 99% rename from packit/src/ui/ReposActivity/addSheet.py rename to packit/src/ui/reposactivity/AddSheet.py index ed07a1b..8cada78 100644 --- a/packit/src/ui/ReposActivity/addSheet.py +++ b/packit/src/ui/reposactivity/AddSheet.py @@ -32,11 +32,11 @@ except Exception as e: import android_utils as _au; _au.log(f"repos dialog: import telegram classes failed: {e}") -from ...SettingsActivity.service.AddKeyDialog import ( +from ...settingsactivity.service.AddKeyDialog import ( _register_back_cb, _unregister_back_cb, _animate_in, _animate_out, _attach_keyboard_listener, _detach_keyboard_listener, ) -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf from ...RepositoryManager import REPO_NAME_MAX # addRepositoryWithUrl answers in lowercase english; the user gets their own diff --git a/packit/src/ui/ReposActivity/card.py b/packit/src/ui/reposactivity/Card.py similarity index 97% rename from packit/src/ui/ReposActivity/card.py rename to packit/src/ui/reposactivity/Card.py index a9cc03c..e8b14e2 100644 --- a/packit/src/ui/ReposActivity/card.py +++ b/packit/src/ui/reposactivity/Card.py @@ -35,8 +35,8 @@ except Exception as e: import android_utils as _au; _au.log(f"repos card: import elyx strings failed: {e}") -from . import repoIcon -from ..PluginListActivity.helpers.uiHelpers import ( +from . import RepoIcon +from ..pluginlistactivity.helpers.UiHelpers import ( apply_press_scale_on_target, resolve_icon, ) @@ -45,7 +45,7 @@ def _chip(ctx, text: str, tint: int): - # uiHelpers.make_info_chip fills at a third alpha and paints the label in a + # UiHelpers.make_info_chip fills at a third alpha and paints the label in a # palette colour. Both are wrong here: the fill has to be solid, and the # colour has to be the theme's, not a green borrowed from the avatar # palette that no other pixel on the screen is using. @@ -57,7 +57,7 @@ def _chip(ctx, text: str, tint: int): bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) bg.setCornerRadius(float(AndroidUtilities.dp(_ROW_H) / 2)) - bg.setColor(repoIcon.tonal(tint, surface, 0.16)) + bg.setColor(RepoIcon.tonal(tint, surface, 0.16)) tv = TextView(ctx) tv.setText(text) tv.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 12) @@ -107,8 +107,8 @@ def _round_icon_button(ctx, icon_name: str, tint: int, on_click, fill, pressed = _alpha(tint, 0x14), _alpha(tint, 0x28) else: surface = _theme("key_windowBackgroundWhite") - fill = repoIcon.tonal(tint, surface, 0.16) - pressed = repoIcon.tonal(tint, surface, 0.30) + fill = RepoIcon.tonal(tint, surface, 0.16) + pressed = RepoIcon.tonal(tint, surface, 0.30) bg = GradientDrawable() bg.setShape(GradientDrawable.RECTANGLE) bg.setCornerRadius(float(AndroidUtilities.dp(size_dp) / 2)) @@ -160,7 +160,7 @@ def make_repo_card(ctx, repo: dict, info: dict, callbacks: dict, handle: dict = repaint this card in place instead of building another one """ enabled = bool(repo.get("enabled", True)) - accent = repoIcon.accent_for(repo) + accent = RepoIcon.accent_for(repo) card = LinearLayout(ctx) card.setOrientation(LinearLayout.VERTICAL) @@ -194,7 +194,7 @@ def _icon_lp(): return lp icon_url = str(info.get("icon_url") or "") - icon_view = repoIcon.build_icon_view(ctx, repo, 48, 14, icon_url) + icon_view = RepoIcon.build_icon_view(ctx, repo, 48, 14, icon_url) icon_holder = [icon_view] # the avatar is swapped only when its url changes header.addView(icon_view, _icon_lp()) @@ -231,7 +231,7 @@ def _icon_lp(): def _fill_sub(r, i): text = str(i.get("maintainer") or "").strip() or _host_of(r.get("url")) # Set up exactly the way the plugin catalogue sets up its author line - # (PluginListActivity/card.py): fullyFormatText, grey body, + # (pluginlistactivity/Card.py): fullyFormatText, grey body, # windowBackgroundWhiteBlueText for the mention, LinkMovementMethod. # Left to itself the formatter paints mentions in its own colour, which # is why these came out a teal that appears nowhere else on the screen. @@ -467,7 +467,7 @@ def _update(new_repo, new_info): header.removeView(icon_holder[0]) except Exception: pass - replacement = repoIcon.build_icon_view(ctx, new_repo, 48, 14, new_url) + replacement = RepoIcon.build_icon_view(ctx, new_repo, 48, 14, new_url) replacement.setAlpha(1.0 if state["enabled"] else 0.55) header.addView(replacement, 0, _icon_lp()) icon_holder[0] = replacement diff --git a/packit/src/ui/ReposActivity/fragment.py b/packit/src/ui/reposactivity/Fragment.py similarity index 95% rename from packit/src/ui/ReposActivity/fragment.py rename to packit/src/ui/reposactivity/Fragment.py index 1d8006c..8ea45e6 100644 --- a/packit/src/ui/ReposActivity/fragment.py +++ b/packit/src/ui/reposactivity/Fragment.py @@ -43,9 +43,9 @@ import android_utils as _au; _au.log(f"repos fragment: import UniversalFragment failed: {e}") from . import register, unregister -from .card import make_repo_card -from ..viewUtils import applyFontToTree -from ...utils import cachedRepos +from .Card import make_repo_card +from ..ViewUtils import applyFontToTree +from ...utils import CachedRepos def _c(color: int) -> int: @@ -75,9 +75,9 @@ def read_repo_info(repo: dict) -> dict: # what the user has taken from this source, and what the catalogues counted # in it the last time they were opened try: - from ...utils import repoStats - info["installed"] = repoStats.installed_count(repo_id) - counted = repoStats.read(repo_id) + from ...utils import RepoStats + info["installed"] = RepoStats.installed_count(repo_id) + counted = RepoStats.read(repo_id) if isinstance(counted.get("plugins"), int): info["plugins"] = counted["plugins"] if isinstance(counted.get("icons"), int): @@ -85,7 +85,7 @@ def read_repo_info(repo: dict) -> dict: except Exception as e: logx(f"repos: stats unavailable for '{repo_id}': {e}", True) - cached = cachedRepos.read(repo_id) + cached = CachedRepos.read(repo_id) if not cached: return info @@ -93,9 +93,9 @@ def read_repo_info(repo: dict) -> dict: info["maintainer"] = str(meta.get("rm_maintainer") or "") info["telegram"] = str(meta.get("rm_telegram") or "") info["source"] = str(meta.get("rm_source") or "") - info["icon_url"] = cachedRepos.icon_url(repo_id) + info["icon_url"] = CachedRepos.icon_url(repo_id) info["status"] = "loaded" - info["updated_at"] = cachedRepos.mtime(repo_id) + info["updated_at"] = CachedRepos.mtime(repo_id) # a repomap that is itself the plugin list carries the count inline; the # usual shape only points at it by url, and that count comes from repoStats @@ -302,7 +302,7 @@ def _render(self, act, repos, infos): def _summary_text(self, count: int) -> str: try: - from ..PluginListActivity.helpers.utils import _format_plural + from ..pluginlistactivity.helpers.Utils import _format_plural return str(_format_plural(count, strings.repo_one, strings.repo_few, strings.repo_many, strings["plural_type"])) except Exception: @@ -335,7 +335,7 @@ def _index_of(self, repo: dict): return -1, repos def _callbacks_for(self, act, repo): - from . import actions + from . import Actions # the card passes its own repo dict back: a repaint replaces the one # captured here with the freshly parsed entry @@ -348,14 +348,14 @@ def _on_toggle(value, current): self.repoManager.updateRepoField(idx, "enabled", value) def _on_menu(anchor, current): - actions.show_card_menu(act, self, current, anchor) + Actions.show_card_menu(act, self, current, anchor) def _on_open_card(current, info): - from .repoSheet import show_repo_sheet + from .RepoSheet import show_repo_sheet show_repo_sheet(act, current, info) def _on_open(url): - actions.open_url(act, url) + Actions.open_url(act, url) return {"on_toggle": _on_toggle, "on_menu": _on_menu, "on_open": _on_open, "on_open_card": _on_open_card} @@ -363,11 +363,11 @@ def _on_open(url): # ------------------------------------------------------------------ pieces def _build_summary_row(self, act): # Built the way the plugin catalogue builds its own toolbar - # (listView.py): a 44dp frame, the count centred in a 16dp-radius pill + # (ListView.py): a 44dp frame, the count centred in a 16dp-radius pill # on the card surface, icon buttons of the same shape on the right. A # loose grey caption over a floating circle did not read as a control # strip at all — this is the same component the rest of the plugin uses. - from ..PluginListActivity.helpers.uiHelpers import get_theme_colors, apply_press_scale_on_target + from ..pluginlistactivity.helpers.UiHelpers import get_theme_colors, apply_press_scale_on_target colors = get_theme_colors() card_bg = colors.get("card_bg_color") card_pressed = colors.get("card_pressed_color") @@ -396,8 +396,8 @@ def _build_summary_row(self, act): # not a bare icon: an icon on its own says nothing about what is behind # it, and there is room on this row for the word. def _menu(v=None): - from . import actions - actions.show_bulk_menu(act, self, menu_btn) + from . import Actions + Actions.show_bulk_menu(act, self, menu_btn) menu_btn = LinearLayout(act) menu_btn.setOrientation(LinearLayout.HORIZONTAL) @@ -479,12 +479,12 @@ def _build_add_button(self, act): btn.addView(label, LayoutHelper.createLinear(-2, -2, Gravity.CENTER_VERTICAL)) def _add(v): - from . import actions - actions.add_repository(act, self) + from . import Actions + Actions.add_repository(act, self) btn.setOnClickListener(OnClickListener(_add)) try: - from ..PluginListActivity.helpers.uiHelpers import apply_press_scale + from ..pluginlistactivity.helpers.UiHelpers import apply_press_scale apply_press_scale(btn) except Exception: pass diff --git a/packit/src/ui/ReposActivity/repoIcon.py b/packit/src/ui/reposactivity/RepoIcon.py similarity index 97% rename from packit/src/ui/ReposActivity/repoIcon.py rename to packit/src/ui/reposactivity/RepoIcon.py index cd29211..a76a9bf 100644 --- a/packit/src/ui/ReposActivity/repoIcon.py +++ b/packit/src/ui/reposactivity/RepoIcon.py @@ -10,7 +10,7 @@ # # The view is a FrameLayout of two layers, monogram below and bitmap above, # because a Drawable subclass would have to be proxied into java just to paint -# one letter. Late answers are dropped by tag, the same guard utils/stickers.py +# one letter. Late answers are dropped by tag, the same guard utils/Stickers.py # uses, so a card reused for another repository cannot inherit its avatar. from packutil import logx @@ -30,9 +30,9 @@ AndroidUtilities = None Theme = None -from ...utils import imagePool +from ...utils import ImagePool from ...network import Storage -from ...utils import cachedRepos +from ...utils import CachedRepos def _c(color: int) -> int: # java setColor(int) rejects python ints >= 0x80000000 @@ -94,7 +94,7 @@ def _letter(repo: dict) -> str: def icon_url_for(repo: dict): - return cachedRepos.icon_url(repo) or None + return CachedRepos.icon_url(repo) or None def build_icon_view(ctx, repo: dict, size_dp: int = 48, radius_dp: int = 14, url=None): @@ -181,5 +181,5 @@ def _apply(): run_on_ui_thread(_apply) - imagePool.submit(_task) + ImagePool.submit(_task) return holder diff --git a/packit/src/ui/ReposActivity/repoSheet.py b/packit/src/ui/reposactivity/RepoSheet.py similarity index 97% rename from packit/src/ui/ReposActivity/repoSheet.py rename to packit/src/ui/reposactivity/RepoSheet.py index cc2dd86..e9c9e95 100644 --- a/packit/src/ui/ReposActivity/repoSheet.py +++ b/packit/src/ui/reposactivity/RepoSheet.py @@ -29,9 +29,9 @@ except Exception as e: import android_utils as _au; _au.log(f"repoSheet: import elyx strings failed: {e}") -from . import repoIcon -from ..viewUtils import applyFontToTree -from ..PluginListActivity.helpers.uiHelpers import setup_bottom_sheet, create_rounded_bg +from . import RepoIcon +from ..ViewUtils import applyFontToTree +from ..pluginlistactivity.helpers.UiHelpers import setup_bottom_sheet, create_rounded_bg def _c(color: int) -> int: @@ -97,7 +97,7 @@ def _show(): header.setOrientation(LinearLayout.HORIZONTAL) header.setGravity(Gravity.CENTER_VERTICAL) - icon = repoIcon.build_icon_view(act, repo, 52, 15, str(info.get("icon_url") or "")) + icon = RepoIcon.build_icon_view(act, repo, 52, 15, str(info.get("icon_url") or "")) icon_lp = LinearLayout.LayoutParams(AndroidUtilities.dp(52), AndroidUtilities.dp(52)) icon_lp.rightMargin = AndroidUtilities.dp(14) header.addView(icon, icon_lp) diff --git a/packit/src/ui/ReposActivity/__init__.py b/packit/src/ui/reposactivity/__init__.py similarity index 95% rename from packit/src/ui/ReposActivity/__init__.py rename to packit/src/ui/reposactivity/__init__.py index 1c477c0..4f2f7e3 100644 --- a/packit/src/ui/ReposActivity/__init__.py +++ b/packit/src/ui/reposactivity/__init__.py @@ -36,5 +36,5 @@ def notify_repos_changed(): def show_repos_fragment(repoManager): # imported lazily: the fragment pulls in a good chunk of the ui package and # nothing needs it until the row is actually tapped - from .fragment import show_repos_fragment as _show + from .Fragment import show_repos_fragment as _show return _show(repoManager) diff --git a/packit/src/ui/suggest/fragment.py b/packit/src/ui/suggest/Fragment.py similarity index 99% rename from packit/src/ui/suggest/fragment.py rename to packit/src/ui/suggest/Fragment.py index a7908a2..bc83fac 100644 --- a/packit/src/ui/suggest/fragment.py +++ b/packit/src/ui/suggest/Fragment.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf from android.view import Gravity, View from android.widget import FrameLayout, ImageView, LinearLayout, ScrollView, TextView from android.util import TypedValue @@ -56,7 +56,7 @@ def _apply_ripple(view, corner_dp=12, bounded=True): except Exception: pass existing = view.getBackground() - from ...utils.ripple import safe_ripple as _safe_ripple + from ...utils.Ripple import safe_ripple as _safe_ripple ripple = _safe_ripple( ColorStateList.valueOf(ripple_color), existing, @@ -987,13 +987,13 @@ def _worker(): repometa = None try: from ...network import Storage - from ...utils import cachedRepos + from ...utils import CachedRepos # which repository carries this plugin is not recorded here, so # every cached one is asked in turn - for rm_rid, _cached in cachedRepos.all_cached(): + for rm_rid, _cached in CachedRepos.all_cached(): try: - plugins_url = cachedRepos.plugins_url(rm_rid) + plugins_url = CachedRepos.plugins_url(rm_rid) if not plugins_url: continue entries, error = Storage.fetch_plugins(plugins_url) @@ -1041,8 +1041,8 @@ def _load_forked_plugins(repo_data: dict) -> list: if not rm_rid: return [] from ...network import Storage - from ...utils import cachedRepos - plugins_url = cachedRepos.plugins_url(rm_rid) + from ...utils import CachedRepos + plugins_url = CachedRepos.plugins_url(rm_rid) if not plugins_url: return [] plugins, error = Storage.fetch_plugins(plugins_url) @@ -1166,7 +1166,7 @@ def _make_forked_popup(act, plugins: list, on_select): icon_lp = LL.LayoutParams(icon_size_px, icon_size_px) icon_lp.rightMargin = dp(12) row.addView(icon_view, icon_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"suggest: popup icon error: {e}", False) @@ -1492,8 +1492,8 @@ def onFragmentCreate(self, *_): rm_rid = repometa.get("rm_rid") self._rm_rid = rm_rid or "default" if rm_rid: - from ...utils import cachedRepos - sp = cachedRepos.suggest_config(rm_rid) + from ...utils import CachedRepos + sp = CachedRepos.suggest_config(rm_rid) if sp is not None: self._suggest_config = sp logx(f"suggest: loaded suggest_plugins for {rm_rid}", True) @@ -2896,7 +2896,7 @@ def _show_forked_selected_card(self, act, plugin: dict): icon_lp = LinearLayout.LayoutParams(icon_size_px, icon_size_px) icon_lp.rightMargin = dp(10) card.addView(icon_view, icon_lp) - from ...utils.stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"suggest: forked selected icon error: {e}", False) @@ -3474,7 +3474,7 @@ def _task(): # app cache is preferred (isInternalUri lets those through), but # some ROMs deny writes there — get_cache_dir() pointed straight # at it and the whole submit died with EACCES. - from ...utils.paths import stageFileForUpload, isInternalPath + from ...utils.Paths import stageFileForUpload, isInternalPath def _stage_for_upload(src: str, display_name: str) -> str: suffix = "" diff --git a/packit/src/utils/app_version.py b/packit/src/utils/AppVersion.py similarity index 100% rename from packit/src/utils/app_version.py rename to packit/src/utils/AppVersion.py diff --git a/packit/src/utils/buildInfo.py b/packit/src/utils/BuildInfo.py similarity index 100% rename from packit/src/utils/buildInfo.py rename to packit/src/utils/BuildInfo.py diff --git a/packit/src/utils/bulletins.py b/packit/src/utils/Bulletins.py similarity index 100% rename from packit/src/utils/bulletins.py rename to packit/src/utils/Bulletins.py diff --git a/packit/src/utils/cachedRepos.py b/packit/src/utils/CachedRepos.py similarity index 98% rename from packit/src/utils/cachedRepos.py rename to packit/src/utils/CachedRepos.py index 47f226b..10cfa99 100644 --- a/packit/src/utils/cachedRepos.py +++ b/packit/src/utils/CachedRepos.py @@ -17,8 +17,8 @@ import json import os -from . import jsonx as _jsonx -from .paths import getRepoCachePath, getReposCacheDir +from . import Jsonx as _jsonx +from .Paths import getRepoCachePath, getReposCacheDir def path(rm_rid) -> str: diff --git a/packit/src/utils/copy.py b/packit/src/utils/Copy.py similarity index 89% rename from packit/src/utils/copy.py rename to packit/src/utils/Copy.py index ba04476..fe57d0d 100644 --- a/packit/src/utils/copy.py +++ b/packit/src/utils/Copy.py @@ -3,19 +3,19 @@ from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, R as R_tg failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from client_utils import get_last_fragment from hook_utils import find_class try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") diff --git a/packit/src/utils/drawable.py b/packit/src/utils/Drawable.py similarity index 100% rename from packit/src/utils/drawable.py rename to packit/src/utils/Drawable.py diff --git a/packit/src/utils/globalState.py b/packit/src/utils/GlobalState.py similarity index 100% rename from packit/src/utils/globalState.py rename to packit/src/utils/GlobalState.py diff --git a/packit/src/utils/hashUtil.py b/packit/src/utils/HashUtil.py similarity index 99% rename from packit/src/utils/hashUtil.py rename to packit/src/utils/HashUtil.py index 7c784a5..ea36dfa 100644 --- a/packit/src/utils/hashUtil.py +++ b/packit/src/utils/HashUtil.py @@ -27,7 +27,7 @@ def _getBitHashLib(): if _libLoaded: return _lib _libLoaded = True - from ..nativeLoader import loadBitHash + from ..NativeLoader import loadBitHash _lib = loadBitHash() if _lib is not None: logx("hashutil: libbithash.so loaded successfully!", True) diff --git a/packit/src/utils/imagePool.py b/packit/src/utils/ImagePool.py similarity index 100% rename from packit/src/utils/imagePool.py rename to packit/src/utils/ImagePool.py diff --git a/packit/src/utils/importFailed.py b/packit/src/utils/ImportFailed.py similarity index 100% rename from packit/src/utils/importFailed.py rename to packit/src/utils/ImportFailed.py diff --git a/packit/src/utils/installIndex.py b/packit/src/utils/InstallIndex.py similarity index 97% rename from packit/src/utils/installIndex.py rename to packit/src/utils/InstallIndex.py index 201af65..cac3329 100644 --- a/packit/src/utils/installIndex.py +++ b/packit/src/utils/InstallIndex.py @@ -15,7 +15,7 @@ def _get_index_path(rm_rid: str) -> str: - from .paths import getRepoIndexPath + from .Paths import getRepoIndexPath return getRepoIndexPath(rm_rid) @@ -30,7 +30,7 @@ def _hash_matches(p: dict) -> bool: return True local_path = str(p.get("local_path") or "") try: - from .hashUtil import matchesStoredHash + from .HashUtil import matchesStoredHash return matchesStoredHash( local_path, sha256=str(p.get("hash") or ""), @@ -147,7 +147,7 @@ def commit_pending(): return try: - from .paths import getPluginsDir, getRepoIndexPath + from .Paths import getPluginsDir, getRepoIndexPath except Exception as e: logx(f"installIndex: cannot import paths: {e}", False) return @@ -184,7 +184,7 @@ def commit_pending(): bithash_val = "" if file_exists: try: - from .hashUtil import _hashFileSha256, _hashFileBithash, _getBitHashLib + from .HashUtil import _hashFileSha256, _hashFileBithash, _getBitHashLib hash_val = _hashFileSha256(candidate_path) if _getBitHashLib() is not None: bithash_val = _hashFileBithash(candidate_path) @@ -242,7 +242,7 @@ def commit_elyx_pending(plugin_info: dict, rm_rid: str, original_path: str = "") return try: - from .paths import getPackitArchivesDir + from .Paths import getPackitArchivesDir except Exception as e: logx(f"installIndex.elyx: cannot import paths: {e}", False) return @@ -290,7 +290,7 @@ def commit_elyx_pending(plugin_info: dict, rm_rid: str, original_path: str = "") hash_source = local_path if local_path and local_path != "Unknown" else "" if hash_source: try: - from .hashUtil import _hashFileSha256, _hashFileBithash, _getBitHashLib + from .HashUtil import _hashFileSha256, _hashFileBithash, _getBitHashLib hash_val = _hashFileSha256(hash_source) if _getBitHashLib() is not None: bithash_val = _hashFileBithash(hash_source) diff --git a/packit/src/utils/jsonx.py b/packit/src/utils/Jsonx.py similarity index 100% rename from packit/src/utils/jsonx.py rename to packit/src/utils/Jsonx.py diff --git a/packit/src/utils/localConfig.py b/packit/src/utils/LocalConfig.py similarity index 96% rename from packit/src/utils/localConfig.py rename to packit/src/utils/LocalConfig.py index adaaad0..3e5fbba 100644 --- a/packit/src/utils/localConfig.py +++ b/packit/src/utils/LocalConfig.py @@ -10,16 +10,16 @@ from elyx import assets except Exception as e: import android_utils as _au; _au.log(f"import elyx import assets failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _get_configs_dir() -> str: - from .paths import getConfigsDir + from .Paths import getConfigsDir return getConfigsDir() @@ -28,7 +28,7 @@ def _get_config_path() -> str: def _get_cache_dir() -> str: - from .paths import getCacheRoot + from .Paths import getCacheRoot return getCacheRoot() diff --git a/packit/src/utils/markdown.py b/packit/src/utils/Markdown.py similarity index 100% rename from packit/src/utils/markdown.py rename to packit/src/utils/Markdown.py diff --git a/packit/src/utils/media.py b/packit/src/utils/Media.py similarity index 94% rename from packit/src/utils/media.py rename to packit/src/utils/Media.py index 13e5e4f..b9920fa 100644 --- a/packit/src/utils/media.py +++ b/packit/src/utils/Media.py @@ -9,7 +9,7 @@ from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.importFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from java import dynamic_proxy @@ -21,7 +21,7 @@ def playSound(soundPath: str, soundKey: str = None, check_pending: bool = True, if check_pending: try: - from ..ui.AchievementsActivity.service.AchivementsEngine import is_achievement_pending + from ..ui.achievementsactivity.service.AchivementsEngine import is_achievement_pending if is_achievement_pending(): return except Exception: diff --git a/packit/src/utils/netQueue.py b/packit/src/utils/NetQueue.py similarity index 100% rename from packit/src/utils/netQueue.py rename to packit/src/utils/NetQueue.py diff --git a/packit/src/utils/paths.py b/packit/src/utils/Paths.py similarity index 99% rename from packit/src/utils/paths.py rename to packit/src/utils/Paths.py index f985328..26500c9 100644 --- a/packit/src/utils/paths.py +++ b/packit/src/utils/Paths.py @@ -36,7 +36,7 @@ def getPackitArchivesDir() -> str: return _filesDir() + "/plugins/ElyxPlugins/packit" def getBitHashSoPath() -> str: - from ..nativeLoader import detectArch + from ..NativeLoader import detectArch return _filesDir() + f"/plugins/ElyxPlugins/shareui_packit/packit/native/{detectArch()}/libbithash.so" def getRepoCachePath(repoId: str) -> str: diff --git a/packit/src/utils/repoStats.py b/packit/src/utils/RepoStats.py similarity index 96% rename from packit/src/utils/repoStats.py rename to packit/src/utils/RepoStats.py index c4c7150..7017091 100644 --- a/packit/src/utils/repoStats.py +++ b/packit/src/utils/RepoStats.py @@ -19,7 +19,7 @@ def _path(rm_rid: str) -> str: - from .paths import getReposCacheDir + from .Paths import getReposCacheDir return os.path.join(getReposCacheDir(), _FILE.format(rm_rid)) @@ -66,7 +66,7 @@ def installed_count(rm_rid: str) -> int: if not rm_rid: return 0 try: - from .paths import getRepoIndexPath + from .Paths import getRepoIndexPath path = getRepoIndexPath(rm_rid) if not os.path.isfile(path): return 0 diff --git a/packit/src/utils/ripple.py b/packit/src/utils/Ripple.py similarity index 100% rename from packit/src/utils/ripple.py rename to packit/src/utils/Ripple.py diff --git a/packit/src/utils/search.py b/packit/src/utils/Search.py similarity index 99% rename from packit/src/utils/search.py rename to packit/src/utils/Search.py index 18a75b4..02e62bd 100644 --- a/packit/src/utils/search.py +++ b/packit/src/utils/Search.py @@ -27,7 +27,7 @@ def _load_native() -> bool: except Exception: pass - from ..nativeLoader import loadSearch + from ..NativeLoader import loadSearch lib = loadSearch() if lib is None: logx("search: failed to load libsearch.so, using python fallback", True) diff --git a/packit/src/utils/share.py b/packit/src/utils/Share.py similarity index 98% rename from packit/src/utils/share.py rename to packit/src/utils/Share.py index 620ba72..b45f92b 100644 --- a/packit/src/utils/share.py +++ b/packit/src/utils/Share.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf import os import requests import threading @@ -75,7 +75,7 @@ def dismiss_spinner(): # stage in EXTERNAL app cache — Telegram's isInternalUri() refuses to # send files from internal storage (getCacheDir -> /data/user/0/...), # which surfaced as "attachment not supported" or a silent no-op - from .paths import getShareCachePath + from .Paths import getShareCachePath file_path = getShareCachePath(filename) os.makedirs(os.path.dirname(file_path), exist_ok=True) logx(f"share: downloading {link} -> {file_path}", True) diff --git a/packit/src/utils/stickers.py b/packit/src/utils/Stickers.py similarity index 100% rename from packit/src/utils/stickers.py rename to packit/src/utils/Stickers.py diff --git a/packit/src/utils/translation.py b/packit/src/utils/Translation.py similarity index 97% rename from packit/src/utils/translation.py rename to packit/src/utils/Translation.py index 463c00c..62d73c2 100644 --- a/packit/src/utils/translation.py +++ b/packit/src/utils/Translation.py @@ -3,7 +3,7 @@ from packutil import logx -from ..utils.bulletins import factory as _pbf +from ..utils.Bulletins import factory as _pbf from client_utils import get_last_fragment from hook_utils import find_class from java import dynamic_proxy @@ -22,12 +22,12 @@ from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, R as R_tg failed: {e}") - from ..importFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..importFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.net import Uri try: from org.telegram.messenger.browser import Browser @@ -259,7 +259,7 @@ def on_close(v): translate_sheet.setCustomView(root) try: - from ..ui.viewUtils import applyFontToTree + from ..ui.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass From 7e2e59263e65d7d0a36eb905110854b88990edfb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:18:32 +0000 Subject: [PATCH 43/46] Sort the modules into folders that say what they are MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree had grown by accretion. Six modules sat loose at the root beside the entry point, ten more sat loose in ui/ beside the screen packages, a folder called other/ held whatever had not belonged anywhere, and the packages were named after the client's screens whether or not that is what they touched. core/ installing plugins, loading the dexes and the native libraries, and the repository list network/ what goes over the wire utils/ helpers, including where things live on disk ui/ the plugin's own screens, one package each, plus components/ for the pieces they are built from and dialogs/ for the sheets that belong to no one screen integrations/ everything that reaches into a screen the client owns: chat/, chatlist/, hooks/, decorations/ deeplinks/, scl/ unchanged settingsactivity moved under ui/ because it is the plugin's own settings, not the client's — the hook into the client's settings screen is a different file and stays in integrations/hooks. The screen packages lose the "activity" suffix they were carrying from the class names they mirror: pluginlistactivity is ui/plugins, reposactivity is ui/repos. Relative imports cannot be patched by substitution when the importing file is itself moving, since `..utils` means a different thing at a new depth. Each was resolved to the path it pointed at, put through the move table, and written out again as seen from the destination. All 659 of them resolve, and all 798 names they import exist where they now point. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- packit/meta.yml | 2 +- packit/src/Main.py | 72 +++++++++---------- packit/src/{ => core}/Core.py | 30 ++++---- packit/src/{ => core}/DexLoader.py | 2 +- packit/src/{ => core}/NativeLoader.py | 4 +- packit/src/{ => core}/RepositoryManager.py | 12 ++-- packit/src/core/__init__.py | 5 ++ packit/src/deeplinks/Install.py | 8 +-- packit/src/deeplinks/Plugin.py | 4 +- packit/src/deeplinks/Repo.py | 10 +-- packit/src/deeplinks/secret/Aytist.py | 2 +- packit/src/deeplinks/secret/Premium.py | 2 +- packit/src/deeplinks/secret/Terraria.py | 2 +- packit/src/integrations/__init__.py | 5 ++ .../chat}/AfpFile.py | 6 +- .../chat}/ConfirmImportBottomSheet.py | 6 +- .../chat}/ImportBottomSheet.py | 2 +- .../chat}/__init__.py | 0 .../chat}/export/DecryptorBottomSheet.py | 12 ++-- .../chat}/export/ImportBottomSheet.py | 2 +- .../chat}/export/__init__.py | 0 .../chat}/export/bin/Reader.py | 2 +- .../chat}/export/bin/Writer.py | 8 +-- .../chat}/export/bin/__init__.py | 0 .../chat}/inline/EnterView.py | 14 ++-- .../chat}/inline/InlineBtns.py | 4 +- .../chat}/inline/InlineState.py | 0 .../chat}/inline/MessageBuilder.py | 2 +- .../chat}/inline/__init__.py | 0 .../chat}/linksicons/LinksBottomSheet.py | 0 .../chat}/linksicons/__init__.py | 0 .../securitybottomsheets/HashBottomSheet.py | 10 +-- .../SignaturesBottomSheet.py | 4 +- .../chat}/securitybottomsheets/__init__.py | 0 .../chatlist}/BtnCAB.py | 4 +- .../chatlist}/BtnPluginsMenu.py | 2 +- .../chatlist}/BuildNotCorrect.py | 6 +- .../chatlist}/Button.py | 6 +- .../chatlist}/ChatDialogButton.py | 4 +- .../chatlist}/PackitUpdateSheet.py | 6 +- .../chatlist}/PillWidget.py | 4 +- .../chatlist}/UpdatesWidget.py | 14 ++-- .../chatlist}/__init__.py | 0 .../decorations}/Badges.py | 4 +- .../decorations}/ChatBadge.py | 0 .../decorations}/ChatTitleIcon.py | 0 .../decorations}/Everyone.py | 2 +- .../decorations}/IsBeta.py | 4 +- .../decorations}/ProfileTitleIcon.py | 0 .../decorations}/Text.py | 2 +- .../decorations}/__init__.py | 0 .../hooks}/AddIconsFab.py | 2 +- .../hooks}/AddPluginFab.py | 2 +- .../hooks}/InstallDismissHook.py | 4 +- .../hooks}/SettingsActivityHook.py | 0 .../hooks}/UniversalFragmentFix.py | 0 .../hooks}/__init__.py | 0 packit/src/scl/Native.py | 2 +- packit/src/{ => ui}/MainActivity.py | 38 +++++----- .../Fragment.py | 6 +- .../service => ui/achievements}/__init__.py | 0 .../service/AchivementsEngine.py | 4 +- .../achievements/service}/__init__.py | 0 packit/src/ui/{ => components}/ContextMenu.py | 0 packit/src/ui/{ => components}/FontManager.py | 0 packit/src/ui/{ => components}/Md3Slider.py | 0 packit/src/ui/{ => components}/ViewUtils.py | 0 packit/src/ui/components/__init__.py | 4 ++ .../ui/{ => dialogs}/DeeplinkBottomSheets.py | 12 ++-- .../src/ui/{ => dialogs}/ExportBottomSheet.py | 6 +- .../ui/{ => dialogs}/FontPickerBottomSheet.py | 8 +-- .../src/ui/{ => dialogs}/NoInternetBanner.py | 2 +- packit/src/ui/{ => dialogs}/ReportDialog.py | 8 +-- packit/src/ui/{ => dialogs}/RestartDialog.py | 4 +- packit/src/ui/dialogs/__init__.py | 4 ++ .../ui/{filesactivity => files}/Fragment.py | 0 .../ui/{filesactivity => files}/InfoDialog.py | 2 +- .../OpenFileFragment.py | 6 +- .../ui/{filesactivity => files}/Packlight.py | 2 +- .../{standalonehooks => ui/files}/__init__.py | 0 .../{iconslistactivity => icons}/Fragment.py | 12 ++-- .../RepoBottomSheet.py | 2 +- .../SortBottomSheet.py | 0 .../__init__.py | 0 .../ui/{pluginactivity => plugin}/Fragment.py | 16 ++--- .../VersionPicker.py | 4 +- .../service => plugin}/__init__.py | 0 .../{pluginlistactivity => plugins}/Card.py | 36 +++++----- .../Fragment.py | 8 +-- .../ListView.py | 6 +- .../ui/{filesactivity => plugins}/__init__.py | 0 .../filter/FilterDrawer.py | 0 .../filter/FilterEngine.py | 0 .../filter/TagLayoutListener.py | 0 .../filter}/__init__.py | 0 .../helpers/PluginActions.py | 8 +-- .../helpers/ReportService.py | 4 +- .../helpers/UiHelpers.py | 0 .../helpers/Utils.py | 0 .../helpers}/__init__.py | 0 .../sheets/AISearchSheet.py | 4 +- .../sheets/DepsSheet.py | 4 +- .../sheets/RepoBottomSheet.py | 2 +- .../sheets/SortBottomSheet.py | 2 +- .../sheets/TgChannelSheet.py | 4 +- .../sheets}/__init__.py | 0 .../ui/{reposactivity => repos}/Actions.py | 2 +- .../ui/{reposactivity => repos}/AddSheet.py | 4 +- .../src/ui/{reposactivity => repos}/Card.py | 4 +- .../ui/{reposactivity => repos}/Fragment.py | 8 +-- .../ui/{reposactivity => repos}/RepoIcon.py | 0 .../ui/{reposactivity => repos}/RepoSheet.py | 4 +- .../ui/{reposactivity => repos}/__init__.py | 0 .../settings}/DebugItems.py | 12 ++-- .../settings}/Deeplinks.py | 6 +- .../{settingsactivity => ui/settings}/Docs.py | 22 +++--- .../settings}/Profile.py | 14 ++-- .../settings}/Settings.py | 22 +++--- .../settings}/Utilities.py | 4 +- .../filter => settings}/__init__.py | 0 .../settings}/service/AddKeyDialog.py | 0 .../settings}/service/FastExpandableHook.py | 0 .../settings}/service/PluginsExport.py | 8 +-- .../helpers => settings/service}/__init__.py | 0 .../settings}/subsettings/Apikeys.py | 16 ++--- .../settings}/subsettings/Comps.py | 0 .../settings}/subsettings/Debug.py | 6 +- .../settings}/subsettings/FileSettings.py | 0 .../settings}/subsettings/Hotkeys.py | 0 .../settings}/subsettings/Inline.py | 2 +- .../settings}/subsettings/Interface.py | 0 .../settings}/subsettings/Misc.py | 0 .../settings}/subsettings/PluginCardEditor.py | 4 +- .../settings}/subsettings/PluginProfile.py | 0 .../settings}/subsettings/Sfx.py | 8 +-- .../settings}/subsettings/Updplugins.py | 0 .../subsettings}/__init__.py | 0 .../ClearIgnoreListDialog.py | 2 +- .../{pluginsupdates => updates}/Fragment.py | 16 ++--- .../HideAllDialog.py | 2 +- .../{pluginsupdates => updates}/HideDialog.py | 2 +- .../StartupSheet.py | 2 +- .../{pluginsupdates => updates}/__init__.py | 0 packit/src/utils/Copy.py | 6 +- packit/src/utils/HashUtil.py | 2 +- packit/src/utils/LocalConfig.py | 4 +- packit/src/utils/Media.py | 4 +- packit/src/utils/Paths.py | 2 +- packit/src/utils/Search.py | 2 +- packit/src/utils/Share.py | 2 +- packit/src/utils/Translation.py | 4 +- 151 files changed, 366 insertions(+), 348 deletions(-) rename packit/src/{ => core}/Core.py (96%) rename packit/src/{ => core}/DexLoader.py (99%) rename packit/src/{ => core}/NativeLoader.py (99%) rename packit/src/{ => core}/RepositoryManager.py (97%) create mode 100644 packit/src/core/__init__.py create mode 100644 packit/src/integrations/__init__.py rename packit/src/{chatactivity => integrations/chat}/AfpFile.py (98%) rename packit/src/{chatactivity => integrations/chat}/ConfirmImportBottomSheet.py (99%) rename packit/src/{chatactivity => integrations/chat}/ImportBottomSheet.py (99%) rename packit/src/{chatactivity => integrations/chat}/__init__.py (100%) rename packit/src/{chatactivity => integrations/chat}/export/DecryptorBottomSheet.py (90%) rename packit/src/{chatactivity => integrations/chat}/export/ImportBottomSheet.py (97%) rename packit/src/{chatactivity => integrations/chat}/export/__init__.py (100%) rename packit/src/{chatactivity => integrations/chat}/export/bin/Reader.py (97%) rename packit/src/{chatactivity => integrations/chat}/export/bin/Writer.py (95%) rename packit/src/{chatactivity => integrations/chat}/export/bin/__init__.py (100%) rename packit/src/{chatactivity => integrations/chat}/inline/EnterView.py (98%) rename packit/src/{chatactivity => integrations/chat}/inline/InlineBtns.py (99%) rename packit/src/{chatactivity => integrations/chat}/inline/InlineState.py (100%) rename packit/src/{chatactivity => integrations/chat}/inline/MessageBuilder.py (99%) rename packit/src/{chatactivity => integrations/chat}/inline/__init__.py (100%) rename packit/src/{chatactivity => integrations/chat}/linksicons/LinksBottomSheet.py (100%) rename packit/src/{chatactivity => integrations/chat}/linksicons/__init__.py (100%) rename packit/src/{chatactivity => integrations/chat}/securitybottomsheets/HashBottomSheet.py (99%) rename packit/src/{chatactivity => integrations/chat}/securitybottomsheets/SignaturesBottomSheet.py (99%) rename packit/src/{chatactivity => integrations/chat}/securitybottomsheets/__init__.py (100%) rename packit/src/{dialogsactivity => integrations/chatlist}/BtnCAB.py (98%) rename packit/src/{dialogsactivity => integrations/chatlist}/BtnPluginsMenu.py (96%) rename packit/src/{dialogsactivity => integrations/chatlist}/BuildNotCorrect.py (98%) rename packit/src/{dialogsactivity => integrations/chatlist}/Button.py (92%) rename packit/src/{dialogsactivity => integrations/chatlist}/ChatDialogButton.py (99%) rename packit/src/{dialogsactivity => integrations/chatlist}/PackitUpdateSheet.py (98%) rename packit/src/{dialogsactivity => integrations/chatlist}/PillWidget.py (99%) rename packit/src/{dialogsactivity => integrations/chatlist}/UpdatesWidget.py (97%) rename packit/src/{dialogsactivity => integrations/chatlist}/__init__.py (100%) rename packit/src/{other => integrations/decorations}/Badges.py (98%) rename packit/src/{other => integrations/decorations}/ChatBadge.py (100%) rename packit/src/{other => integrations/decorations}/ChatTitleIcon.py (100%) rename packit/src/{other => integrations/decorations}/Everyone.py (98%) rename packit/src/{other => integrations/decorations}/IsBeta.py (98%) rename packit/src/{other => integrations/decorations}/ProfileTitleIcon.py (100%) rename packit/src/{other => integrations/decorations}/Text.py (92%) rename packit/src/{other => integrations/decorations}/__init__.py (100%) rename packit/src/{standalonehooks => integrations/hooks}/AddIconsFab.py (98%) rename packit/src/{standalonehooks => integrations/hooks}/AddPluginFab.py (99%) rename packit/src/{standalonehooks => integrations/hooks}/InstallDismissHook.py (93%) rename packit/src/{standalonehooks => integrations/hooks}/SettingsActivityHook.py (100%) rename packit/src/{standalonehooks => integrations/hooks}/UniversalFragmentFix.py (100%) rename packit/src/{settingsactivity => integrations/hooks}/__init__.py (100%) rename packit/src/{ => ui}/MainActivity.py (92%) rename packit/src/ui/{achievementsactivity => achievements}/Fragment.py (99%) rename packit/src/{settingsactivity/service => ui/achievements}/__init__.py (100%) rename packit/src/ui/{achievementsactivity => achievements}/service/AchivementsEngine.py (99%) rename packit/src/{settingsactivity/subsettings => ui/achievements/service}/__init__.py (100%) rename packit/src/ui/{ => components}/ContextMenu.py (100%) rename packit/src/ui/{ => components}/FontManager.py (100%) rename packit/src/ui/{ => components}/Md3Slider.py (100%) rename packit/src/ui/{ => components}/ViewUtils.py (100%) create mode 100644 packit/src/ui/components/__init__.py rename packit/src/ui/{ => dialogs}/DeeplinkBottomSheets.py (98%) rename packit/src/ui/{ => dialogs}/ExportBottomSheet.py (99%) rename packit/src/ui/{ => dialogs}/FontPickerBottomSheet.py (98%) rename packit/src/ui/{ => dialogs}/NoInternetBanner.py (99%) rename packit/src/ui/{ => dialogs}/ReportDialog.py (99%) rename packit/src/ui/{ => dialogs}/RestartDialog.py (99%) create mode 100644 packit/src/ui/dialogs/__init__.py rename packit/src/ui/{filesactivity => files}/Fragment.py (100%) rename packit/src/ui/{filesactivity => files}/InfoDialog.py (99%) rename packit/src/ui/{filesactivity => files}/OpenFileFragment.py (99%) rename packit/src/ui/{filesactivity => files}/Packlight.py (98%) rename packit/src/{standalonehooks => ui/files}/__init__.py (100%) rename packit/src/ui/{iconslistactivity => icons}/Fragment.py (99%) rename packit/src/ui/{iconslistactivity => icons}/RepoBottomSheet.py (99%) rename packit/src/ui/{iconslistactivity => icons}/SortBottomSheet.py (100%) rename packit/src/ui/{achievementsactivity => icons}/__init__.py (100%) rename packit/src/ui/{pluginactivity => plugin}/Fragment.py (99%) rename packit/src/ui/{pluginactivity => plugin}/VersionPicker.py (99%) rename packit/src/ui/{achievementsactivity/service => plugin}/__init__.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/Card.py (95%) rename packit/src/ui/{pluginlistactivity => plugins}/Fragment.py (99%) rename packit/src/ui/{pluginlistactivity => plugins}/ListView.py (99%) rename packit/src/ui/{filesactivity => plugins}/__init__.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/filter/FilterDrawer.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/filter/FilterEngine.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/filter/TagLayoutListener.py (100%) rename packit/src/ui/{iconslistactivity => plugins/filter}/__init__.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/helpers/PluginActions.py (96%) rename packit/src/ui/{pluginlistactivity => plugins}/helpers/ReportService.py (93%) rename packit/src/ui/{pluginlistactivity => plugins}/helpers/UiHelpers.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/helpers/Utils.py (100%) rename packit/src/ui/{pluginactivity => plugins/helpers}/__init__.py (100%) rename packit/src/ui/{pluginlistactivity => plugins}/sheets/AISearchSheet.py (99%) rename packit/src/ui/{pluginlistactivity => plugins}/sheets/DepsSheet.py (99%) rename packit/src/ui/{pluginlistactivity => plugins}/sheets/RepoBottomSheet.py (99%) rename packit/src/ui/{pluginlistactivity => plugins}/sheets/SortBottomSheet.py (99%) rename packit/src/ui/{pluginlistactivity => plugins}/sheets/TgChannelSheet.py (97%) rename packit/src/ui/{pluginlistactivity => plugins/sheets}/__init__.py (100%) rename packit/src/ui/{reposactivity => repos}/Actions.py (99%) rename packit/src/ui/{reposactivity => repos}/AddSheet.py (99%) rename packit/src/ui/{reposactivity => repos}/Card.py (99%) rename packit/src/ui/{reposactivity => repos}/Fragment.py (98%) rename packit/src/ui/{reposactivity => repos}/RepoIcon.py (100%) rename packit/src/ui/{reposactivity => repos}/RepoSheet.py (98%) rename packit/src/ui/{reposactivity => repos}/__init__.py (100%) rename packit/src/{settingsactivity => ui/settings}/DebugItems.py (97%) rename packit/src/{settingsactivity => ui/settings}/Deeplinks.py (96%) rename packit/src/{settingsactivity => ui/settings}/Docs.py (92%) rename packit/src/{settingsactivity => ui/settings}/Profile.py (98%) rename packit/src/{settingsactivity => ui/settings}/Settings.py (99%) rename packit/src/{settingsactivity => ui/settings}/Utilities.py (96%) rename packit/src/ui/{pluginlistactivity/filter => settings}/__init__.py (100%) rename packit/src/{settingsactivity => ui/settings}/service/AddKeyDialog.py (100%) rename packit/src/{settingsactivity => ui/settings}/service/FastExpandableHook.py (100%) rename packit/src/{settingsactivity => ui/settings}/service/PluginsExport.py (98%) rename packit/src/ui/{pluginlistactivity/helpers => settings/service}/__init__.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Apikeys.py (96%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Comps.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Debug.py (99%) rename packit/src/{settingsactivity => ui/settings}/subsettings/FileSettings.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Hotkeys.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Inline.py (98%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Interface.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Misc.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/PluginCardEditor.py (99%) rename packit/src/{settingsactivity => ui/settings}/subsettings/PluginProfile.py (100%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Sfx.py (95%) rename packit/src/{settingsactivity => ui/settings}/subsettings/Updplugins.py (100%) rename packit/src/ui/{pluginlistactivity/sheets => settings/subsettings}/__init__.py (100%) rename packit/src/ui/{pluginsupdates => updates}/ClearIgnoreListDialog.py (99%) rename packit/src/ui/{pluginsupdates => updates}/Fragment.py (99%) rename packit/src/ui/{pluginsupdates => updates}/HideAllDialog.py (99%) rename packit/src/ui/{pluginsupdates => updates}/HideDialog.py (99%) rename packit/src/ui/{pluginsupdates => updates}/StartupSheet.py (99%) rename packit/src/ui/{pluginsupdates => updates}/__init__.py (100%) diff --git a/packit/meta.yml b/packit/meta.yml index f610466..57345a1 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.1.2-dev.32" +version: "0.1.2-dev.33" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/Main.py b/packit/src/Main.py index 8fe1507..22b62e5 100644 --- a/packit/src/Main.py +++ b/packit/src/Main.py @@ -5,8 +5,8 @@ from .utils.Bulletins import factory as _pbf import time from typing import Any -from .other import Text as _text -from .chatactivity.inline.EnterView import ( +from .integrations.decorations import Text as _text +from .integrations.chat.inline.EnterView import ( _packit_get_class, _packit_hook_enter_view_constructor, _packit_attach_text_watcher, @@ -44,14 +44,14 @@ def _clearLatestLog(): def startInit(plugin, launchStart): _clearLatestLog() - from .NativeLoader import detectArch + from .core.NativeLoader import detectArch detectArch() - from .RepositoryManager import RepositoryManager - from .Core import PackItCore - from .MainActivity import SettingsBuilder - from .dialogsactivity.Button import ChatButton - from .other.Badges import BadgeManager + from .core.RepositoryManager import RepositoryManager + from .core.Core import PackItCore + from .ui.MainActivity import SettingsBuilder + from .integrations.chatlist.Button import ChatButton + from .integrations.decorations.Badges import BadgeManager plugin._launch_start = launchStart plugin.repoManager = RepositoryManager() @@ -120,31 +120,31 @@ def loadPlugin(plugin): logx(f"PackIt: import settings failed: {e}", False) return - from .ui.pluginactivity.Fragment import process_start + from .ui.plugin.Fragment import process_start process_start() if RENAME_PACKITCACHE: _migrate_packitcache() if CHECK_PATHS: _check_paths() - from .NativeLoader import CHECK_SO_PATHS, checkSoPaths + from .core.NativeLoader import CHECK_SO_PATHS, checkSoPaths if CHECK_SO_PATHS: checkSoPaths() from .utils.LocalConfig import LocalConfig LocalConfig.init() - from .dialogsactivity.BuildNotCorrect import setup_build_not_correct_check + from .integrations.chatlist.BuildNotCorrect import setup_build_not_correct_check setup_build_not_correct_check() try: from .utils.InstallIndex import purge_missing purge_missing() except Exception as e: logx(f"PackIt: installIndex purge error: {e}", False) - from .other import IsBeta - from .other import Everyone as _everyone + from .integrations.decorations import IsBeta + from .integrations.decorations import Everyone as _everyone IsBeta.init() _everyone.init() try: - from .ui.achievementsactivity.service.AchivementsEngine import sync_accounts, sync_completed, _load_account, _save_account + from .ui.achievements.service.AchivementsEngine import sync_accounts, sync_completed, _load_account, _save_account sync_accounts() loaded, load_ok = _load_account() data, _ = sync_completed(loaded) @@ -165,36 +165,36 @@ def loadPlugin(plugin): plugin.deeplink_hook_ref = setup_deeplink_hook(plugin) plugin.chatUI.initialize_chat_menu() plugin.badgeManager.setup_hooks() - from .chatactivity.securitybottomsheets import setup_policy_button_hook, setup_hash_button_hook + from .integrations.chat.securitybottomsheets import setup_policy_button_hook, setup_hash_button_hook plugin.policy_button_hook_ref = setup_policy_button_hook(plugin) plugin.hash_button_hook_ref = setup_hash_button_hook(plugin, plugin.repoManager) - from .chatactivity.linksicons import setup_links_buttons_hook + from .integrations.chat.linksicons import setup_links_buttons_hook plugin.links_button_hook_ref = setup_links_buttons_hook(plugin) - from .standalonehooks.InstallDismissHook import setup_install_dismiss_hook + from .integrations.hooks.InstallDismissHook import setup_install_dismiss_hook plugin.install_dismiss_hook_ref = setup_install_dismiss_hook(plugin) - from .standalonehooks.UniversalFragmentFix import setup_universal_fragment_fix + from .integrations.hooks.UniversalFragmentFix import setup_universal_fragment_fix plugin.universal_fragment_fix_ref = setup_universal_fragment_fix(plugin) - from .chatactivity.export.DecryptorBottomSheet import setup_packit_file_hook + from .integrations.chat.export.DecryptorBottomSheet import setup_packit_file_hook setup_packit_file_hook(plugin) - from .chatactivity.AfpFile import setup_afp_file_hook + from .integrations.chat.AfpFile import setup_afp_file_hook setup_afp_file_hook(plugin) - from .standalonehooks.AddPluginFab import setup_plugins_activity_fab + from .integrations.hooks.AddPluginFab import setup_plugins_activity_fab plugin.plugins_activity_fab_ref = setup_plugins_activity_fab(plugin) - from .standalonehooks.AddIconsFab import setup_icon_packs_activity_fab + from .integrations.hooks.AddIconsFab import setup_icon_packs_activity_fab plugin.icon_packs_activity_fab_ref = setup_icon_packs_activity_fab(plugin) - from .standalonehooks.SettingsActivityHook import setup_settings_activity_hook + from .integrations.hooks.SettingsActivityHook import setup_settings_activity_hook plugin.settings_activity_hook_refs = setup_settings_activity_hook(plugin) - from .settingsactivity.service.FastExpandableHook import setup_fast_expandable_hook + from .ui.settings.service.FastExpandableHook import setup_fast_expandable_hook plugin.fast_expandable_hook_ref = setup_fast_expandable_hook(plugin, plugin.settingsBuilder.otherSettings) - from .dialogsactivity.PillWidget import setup_pill_widget + from .integrations.chatlist.PillWidget import setup_pill_widget setup_pill_widget(plugin) - from .dialogsactivity.UpdatesWidget import setup_updates_widget + from .integrations.chatlist.UpdatesWidget import setup_updates_widget setup_updates_widget(plugin) plugin.dialogs_menu_hook_ref = plugin.chatUI.setup_dialogs_menu_hook() plugin.everyone_hook_refs = _everyone.setup_hook(plugin) - from .chatactivity.inline.EnterView import setup_packit_autocomplete + from .integrations.chat.inline.EnterView import setup_packit_autocomplete plugin.packit_hook_constructor_ref = setup_packit_autocomplete(plugin) - from .chatactivity.inline.InlineBtns import setup_inline_translate_button + from .integrations.chat.inline.InlineBtns import setup_inline_translate_button setup_inline_translate_button(plugin) plugin._init_official_repository() plugin._check_for_update() @@ -221,7 +221,7 @@ def _show_startup_bulletin(plugin): def _check_for_update(plugin): try: - from .dialogsactivity.PackitUpdateSheet import check_and_show + from .integrations.chatlist.PackitUpdateSheet import check_and_show check_and_show() except Exception as e: logx(f"PackIt: update check error: {e}", False) @@ -229,7 +229,7 @@ def _check_for_update(plugin): def _check_startup_updates(plugin): try: - from .ui.pluginsupdates.StartupSheet import check_and_show_startup_updates + from .ui.updates.StartupSheet import check_and_show_startup_updates check_and_show_startup_updates(plugin=plugin) except Exception as e: logx(f"PackIt: startup updates check error: {e}", False) @@ -240,7 +240,7 @@ def _check_update_notifications_bulletin(plugin): def task(): try: - from .ui.pluginsupdates.Fragment import _check_updates, _filter_ignored + from .ui.updates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) if not updates: return @@ -291,7 +291,7 @@ def show(): if single_update is not None: def _install(): try: - from .ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url + from .ui.updates.Fragment import _get_repos, _get_repo_plugins_url import requests as _req pid = str(single_update.get("id") or "") repo_id = str(single_update.get("repo_id") or "") @@ -326,7 +326,7 @@ def _install(): if not plugin_item: logx(f"PackIt: update bulletin install: plugin '{pid}' not found in repo", True) return - from .Core import install_plugin + from .core.Core import install_plugin run_on_ui_thread(lambda: install_plugin(plugin_item, all_plugins=all_plugins, rm_rid=repo_id)) except Exception as _e: logx(f"PackIt: update bulletin install error: {_e}", True) @@ -335,7 +335,7 @@ def _install(): else: def _action(): try: - from .ui.pluginsupdates.Fragment import show_updates_fragment + from .ui.updates.Fragment import show_updates_fragment show_updates_fragment() except Exception as _e: logx(f"PackIt: update bulletin open error: {_e}", True) @@ -362,7 +362,7 @@ def _check_identity_achievement(plugin): return first_name = str(user.first_name) if user.first_name else "" if first_name.lower() in ("shareui", "fuchs"): - from .ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from .ui.achievements.service.AchivementsEngine import unlock_secret unlock_secret("identity") @@ -385,7 +385,7 @@ def on_send_message_hook(plugin, account: int, params: Any): if params.message.startswith(".deleteachievements"): try: import os - from .ui.achievementsactivity.service.AchivementsEngine import _get_db_path, _get_snap_path + from .ui.achievements.service.AchivementsEngine import _get_db_path, _get_snap_path for path in (_get_db_path(), _get_snap_path()): if os.path.exists(path): os.remove(path) diff --git a/packit/src/Core.py b/packit/src/core/Core.py similarity index 96% rename from packit/src/Core.py rename to packit/src/core/Core.py index 5c0ba05..a1689d4 100644 --- a/packit/src/Core.py +++ b/packit/src/core/Core.py @@ -19,17 +19,17 @@ from org.telegram.messenger import ApplicationLoader, AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins import PluginsController except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins import PluginsController failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import NotificationCenter except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import NotificationCenter failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() import time import signal @@ -119,7 +119,7 @@ def _is_elyx_plugin(plugin_info: dict) -> bool: def install_plugin(plugin_info: dict, icon_view=None, button=None, original_icon_id=None, loading_view=None, on_finish=None, install_ui=None, all_plugins: list = None, rm_rid: str = "", succ_download=None): deps = plugin_info.get("deps") or [] if deps: - from .ui.pluginlistactivity.sheets.DepsSheet import show_deps_sheet + from ..ui.plugins.sheets.DepsSheet import show_deps_sheet def on_confirmed(): _do_install(plugin_info, icon_view, button, original_icon_id, loading_view, on_finish, install_ui, rm_rid=rm_rid, succ_download=succ_download) show_deps_sheet(install_ui, plugin_info, on_confirmed, all_plugins=all_plugins, on_cancel=on_finish) @@ -155,10 +155,10 @@ def _commit_index(): return try: if _is_elyx_plugin(plugin_info): - from .utils.InstallIndex import commit_elyx_pending + from ..utils.InstallIndex import commit_elyx_pending commit_elyx_pending(plugin_info, rm_rid, original_path=temp_path) else: - from .utils.InstallIndex import commit_pending + from ..utils.InstallIndex import commit_pending commit_pending() except Exception as e: logx(f"core: index commit error: {e}", False) @@ -191,7 +191,7 @@ def _on_plugins_updated(): # to elyx only to avoid double counting. if _is_elyx_plugin(plugin_info): try: - from .ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..ui.achievements.service.AchivementsEngine import increment_category increment_category("Installing plugins") except Exception as e: logx(f"core: elyx achievement increment error: {e}", False) @@ -207,7 +207,7 @@ def _on_plugins_updated(): logx(f"core: check restart={restart}", True) if restart in ("required", "optional"): logx("core: calling show_restart_dialog", True) - from .ui.RestartDialog import show_restart_dialog + from ..ui.dialogs.RestartDialog import show_restart_dialog show_restart_dialog(restart, fragment) except Exception as e: logx(f"core: restart dialog error: {e}", False) @@ -261,7 +261,7 @@ def didReceivedNotification(self, id, account, *args): ElyxEngine.instance.showInstallDialog(fragment, install_params) else: if write_index: - from .utils.InstallIndex import set_pending + from ..utils.InstallIndex import set_pending set_pending(plugin_info, rm_rid) PluginsController.getInstance().showInstallDialog(fragment, temp_path, True) @@ -275,14 +275,14 @@ def didReceivedNotification(self, id, account, *args): pass -from .utils.HashUtil import hashFile, getHashMethod, METHOD_SHA256, METHOD_BITHASH, matchesStoredHash +from ..utils.HashUtil import hashFile, getHashMethod, METHOD_SHA256, METHOD_BITHASH, matchesStoredHash def _get_plugin_cache_path(pkg: str, filename: str) -> str: # cache is isolated per hash method method = getHashMethod() subdir = "BitHash" if method == METHOD_BITHASH else "sha256" - from .utils.Paths import getPluginCacheDir + from ..utils.Paths import getPluginCacheDir cache_dir = getPluginCacheDir(subdir) os.makedirs(cache_dir, exist_ok=True) return os.path.join(cache_dir, filename) @@ -346,7 +346,7 @@ def _do_install(plugin_info: dict, icon_view=None, button=None, original_icon_id def task(): try: - from .utils.Paths import getPluginsDir + from ..utils.Paths import getPluginsDir plugins_dir = getPluginsDir() try: os.makedirs(plugins_dir, exist_ok=True) @@ -520,7 +520,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(_s("core_iconpack_http_error", code=r.status_code))) return - from .utils.Paths import getIconPackTmpPath + from ..utils.Paths import getIconPackTmpPath tmp_path = getIconPackTmpPath(pack_id) content_length = r.headers.get("content-length") @@ -596,7 +596,7 @@ def install_plugin_silent(file_path: str, plugin_data: dict, repo_id: str, on_co from elyxcore._plugin_engine import ElyxEngine except ImportError: from elyxcore import ElyxPlugin, ElyxEngine # older SDKs - from .utils.InstallIndex import commit_elyx_pending + from ..utils.InstallIndex import commit_elyx_pending elyx_plugin = ElyxPlugin(plzip=ZipFile(file_path, "r"), raise_errors=False) @@ -633,7 +633,7 @@ def _elyx_error(error): try: from elyxcore import gen from org.telegram.messenger import Utilities - from .utils.InstallIndex import set_pending, commit_pending + from ..utils.InstallIndex import set_pending, commit_pending Callback = gen(Utilities.Callback, "run") python_engine = PluginsController.getEngines().get("python") diff --git a/packit/src/DexLoader.py b/packit/src/core/DexLoader.py similarity index 99% rename from packit/src/DexLoader.py rename to packit/src/core/DexLoader.py index d6f0200..a370f45 100644 --- a/packit/src/DexLoader.py +++ b/packit/src/core/DexLoader.py @@ -19,7 +19,7 @@ def _dexPath(name: str) -> str: - from .utils.Paths import _filesDir + from ..utils.Paths import _filesDir return _filesDir() + _DEX_BASE + "/" + name + ".dex" diff --git a/packit/src/NativeLoader.py b/packit/src/core/NativeLoader.py similarity index 99% rename from packit/src/NativeLoader.py rename to packit/src/core/NativeLoader.py index 40fee54..0608109 100644 --- a/packit/src/NativeLoader.py +++ b/packit/src/core/NativeLoader.py @@ -38,7 +38,7 @@ def detectArch() -> str: def _soPath(libName: str) -> str: - from .utils.Paths import _filesDir + from ..utils.Paths import _filesDir arch = detectArch() return _filesDir() + _BASE + "/" + arch + "/" + libName + ".so" @@ -140,7 +140,7 @@ def _retry(): # index 49 of the error pack; the shared loader binds it now if # cached, else on diceStickersDidLoad (no polling) - from .utils.Stickers import load_sticker + from ..utils.Stickers import load_sticker load_sticker(iv, "wtffffffffffDD/49", 100) linear.addView(iv, LayoutHelper.createLinear( diff --git a/packit/src/RepositoryManager.py b/packit/src/core/RepositoryManager.py similarity index 97% rename from packit/src/RepositoryManager.py rename to packit/src/core/RepositoryManager.py index d32c2e4..23c6504 100644 --- a/packit/src/RepositoryManager.py +++ b/packit/src/core/RepositoryManager.py @@ -2,22 +2,22 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from .utils.NetQueue import run_serial_io -from .network import Storage -from .utils import CachedRepos +from ..utils.NetQueue import run_serial_io +from ..network import Storage +from ..utils import CachedRepos import json from client_utils import get_last_fragment, run_on_queue try: from elyx import settings, strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings, strings failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() OFFICIAL_REPO_URL = "https://raw.githubusercontent.com/shareui/packit/refs/heads/main/configs/repomap.json" @@ -77,7 +77,7 @@ def setRepositories(self, repos): # the sources screen is a plain fragment with no adapter, so # rebuildAllItems never reaches it — it listens here instead try: - from .ui.reposactivity import notify_repos_changed + from ..ui.repos import notify_repos_changed notify_repos_changed() except Exception: pass diff --git a/packit/src/core/__init__.py b/packit/src/core/__init__.py new file mode 100644 index 0000000..9b5f771 --- /dev/null +++ b/packit/src/core/__init__.py @@ -0,0 +1,5 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# What the plugin is made of: installing and removing plugins, loading the +# dexes and the native libraries, and the repository list itself. diff --git a/packit/src/deeplinks/Install.py b/packit/src/deeplinks/Install.py index 69d9ddc..0dad392 100644 --- a/packit/src/deeplinks/Install.py +++ b/packit/src/deeplinks/Install.py @@ -8,8 +8,8 @@ from client_utils import get_last_fragment, run_on_queue from android_utils import run_on_ui_thread from urllib.parse import urlparse, parse_qs -from ..Core import install_plugin, install_icon_pack -from ..ui.pluginlistactivity.Fragment import InstallUI +from ..core.Core import install_plugin, install_icon_pack +from ..ui.plugins.Fragment import InstallUI try: from elyx import strings except Exception as e: @@ -117,7 +117,7 @@ def _is_version_ok(app_ver_expr: str) -> bool: def _find_best_compatible(plugin: dict) -> dict | None: # returns a plugin dict with link/app_version set to best available compatible version # checks root version first (newest), then versions dict descending - from ..ui.pluginactivity.VersionPicker import _build_version_entries + from ..ui.plugin.VersionPicker import _build_version_entries entries = _build_version_entries(plugin) for e in entries: if _is_version_ok(e["app_version"]): @@ -140,7 +140,7 @@ def _show_incompatible_sheet(requested_version: str, compatible_plugin: dict, al from org.telegram.ui.ActionBar import BottomSheet, Theme from org.telegram.ui.Components import LayoutHelper from org.telegram.messenger import AndroidUtilities - from ..Core import install_plugin + from ..core.Core import install_plugin fragment = get_last_fragment() if not fragment: diff --git a/packit/src/deeplinks/Plugin.py b/packit/src/deeplinks/Plugin.py index f5fec4c..dba6e9b 100644 --- a/packit/src/deeplinks/Plugin.py +++ b/packit/src/deeplinks/Plugin.py @@ -103,7 +103,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(str(strings("dl_plugin_not_found", plugin_id=pluginId)))) return - from ..ui.pluginlistactivity.Fragment import InstallUI + from ..ui.plugins.Fragment import InstallUI class _FakePlugin: def __init__(self, rm): @@ -112,7 +112,7 @@ def __init__(self, rm): installUI = InstallUI(_FakePlugin(repoManager)) def _show(_p=plugin, _all=all_plugins, _rid=repoId): - from ..ui.pluginactivity.Fragment import show_plugin_profile + from ..ui.plugin.Fragment import show_plugin_profile show_plugin_profile(_p, installUI, _all, repo_id=_rid) run_on_ui_thread(_show) diff --git a/packit/src/deeplinks/Repo.py b/packit/src/deeplinks/Repo.py index 0a66daa..3c8db2b 100644 --- a/packit/src/deeplinks/Repo.py +++ b/packit/src/deeplinks/Repo.py @@ -55,13 +55,13 @@ def _sheet_chip(act, text: str): # the same pill the source cards use, so the sheet that adds a source and # the card it becomes are recognisably the same thing - from ..ui.reposactivity.Card import _chip - from ..ui.reposactivity.RepoIcon import accent_for + from ..ui.repos.Card import _chip + from ..ui.repos.RepoIcon import accent_for return _chip(act, text, accent_for({})) def _sheet_chip_lp(margin_dp=3): - from ..ui.reposactivity.Card import _ROW_H + from ..ui.repos.Card import _ROW_H lp = LinearLayout.LayoutParams(-2, AndroidUtilities.dp(_ROW_H)) lp.leftMargin = AndroidUtilities.dp(margin_dp) lp.rightMargin = AndroidUtilities.dp(margin_dp) @@ -185,7 +185,7 @@ def _show_confirm_sheet(repometa, pluginCount, name, link, repoManager): # picture for every repository in existence, which told the reader # nothing about the one they were about to add. try: - from ..ui.reposactivity.RepoIcon import build_icon_view + from ..ui.repos.RepoIcon import build_icon_view icon_view = build_icon_view( act, {"id": rm_rid, "name": rm_name, "url": link}, 76, 22, rm_icon) linear.addView(icon_view, LayoutHelper.createLinear( @@ -299,7 +299,7 @@ def onClick(self, v): repoManager.setRepositories(currentRepos) BulletinHelper.show_success(strings.repo_add_success) try: - from ..ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..ui.achievements.service.AchivementsEngine import increment_category increment_category("Repositories") except Exception as e: logx(f"repo deeplink: achievements increment error: {e}", False) diff --git a/packit/src/deeplinks/secret/Aytist.py b/packit/src/deeplinks/secret/Aytist.py index 23afd3c..82fae17 100644 --- a/packit/src/deeplinks/secret/Aytist.py +++ b/packit/src/deeplinks/secret/Aytist.py @@ -22,7 +22,7 @@ def handle(url): if url != "tg://packit?aytist": return try: - from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ...ui.achievements.service.AchivementsEngine import unlock_secret unlock_secret("aytist") run_on_ui_thread(_startSpawnChain) except Exception as e: diff --git a/packit/src/deeplinks/secret/Premium.py b/packit/src/deeplinks/secret/Premium.py index 1ff14db..6930faf 100644 --- a/packit/src/deeplinks/secret/Premium.py +++ b/packit/src/deeplinks/secret/Premium.py @@ -17,7 +17,7 @@ def handle(url): return try: _playMaxVolume() - from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ...ui.achievements.service.AchivementsEngine import unlock_secret unlock_secret("premium") except Exception as e: logx(f"deeplinks.premium: error: {e}", False) diff --git a/packit/src/deeplinks/secret/Terraria.py b/packit/src/deeplinks/secret/Terraria.py index fe43444..9f99dd5 100644 --- a/packit/src/deeplinks/secret/Terraria.py +++ b/packit/src/deeplinks/secret/Terraria.py @@ -18,7 +18,7 @@ def handle(url): try: _playMaxVolume() logx(f"deeplinks.terraria: calling unlock_secret", True) - from ...ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ...ui.achievements.service.AchivementsEngine import unlock_secret unlock_secret("terraria") logx(f"deeplinks.terraria: unlock_secret returned", True) except Exception as e: diff --git a/packit/src/integrations/__init__.py b/packit/src/integrations/__init__.py new file mode 100644 index 0000000..67d022f --- /dev/null +++ b/packit/src/integrations/__init__.py @@ -0,0 +1,5 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Everything that reaches into a screen the client owns — the chat, the chat +# list, the profile, the client's own settings — rather than a screen of ours. diff --git a/packit/src/chatactivity/AfpFile.py b/packit/src/integrations/chat/AfpFile.py similarity index 98% rename from packit/src/chatactivity/AfpFile.py rename to packit/src/integrations/chat/AfpFile.py index f0b2794..53fbc6d 100644 --- a/packit/src/chatactivity/AfpFile.py +++ b/packit/src/integrations/chat/AfpFile.py @@ -29,9 +29,9 @@ def before_hooked_method(self, param): logx(f"afpFile: before_hooked_method error: {e}", False) def _read(self, file_path: str, filename: str): - from ..scl.Scl import parse - from ..scl.Opts import ParseOpts - from ..utils.Paths import getTempDir + from ...scl.Scl import parse + from ...scl.Opts import ParseOpts + from ...utils.Paths import getTempDir import os import shutil import time diff --git a/packit/src/chatactivity/ConfirmImportBottomSheet.py b/packit/src/integrations/chat/ConfirmImportBottomSheet.py similarity index 99% rename from packit/src/chatactivity/ConfirmImportBottomSheet.py rename to packit/src/integrations/chat/ConfirmImportBottomSheet.py index f748081..ddd3023 100644 --- a/packit/src/chatactivity/ConfirmImportBottomSheet.py +++ b/packit/src/integrations/chat/ConfirmImportBottomSheet.py @@ -314,8 +314,8 @@ def __init__(self): super().__init__() def onClick(self, v): import threading import zipfile - from ..Core import onlyLocalInstallNoUi - from ..utils.Paths import getTempDir + from ...core.Core import onlyLocalInstallNoUi + from ...utils.Paths import getTempDir from ui.bulletin import BulletinHelper from org.telegram.messenger import R as R_tg @@ -601,7 +601,7 @@ def _show(): def _run_installs(): try: import re as _re - from ..utils.AppVersion import _parse_version, _get_app_version + from ...utils.AppVersion import _parse_version, _get_app_version tmp_dir = getTempDir() os.makedirs(tmp_dir, exist_ok=True) with zipfile.ZipFile(file_path, "r") as zf: diff --git a/packit/src/chatactivity/ImportBottomSheet.py b/packit/src/integrations/chat/ImportBottomSheet.py similarity index 99% rename from packit/src/chatactivity/ImportBottomSheet.py rename to packit/src/integrations/chat/ImportBottomSheet.py index 8801ffe..73c0492 100644 --- a/packit/src/chatactivity/ImportBottomSheet.py +++ b/packit/src/integrations/chat/ImportBottomSheet.py @@ -26,7 +26,7 @@ def _make_icon_view(activity, icon_str: str, size_dp: int): iv.getImageReceiver().setCrossfadeWithOldImage(True) except Exception: pass - from ..utils.Stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(iv, icon_str, size_dp) return iv except Exception as e: diff --git a/packit/src/chatactivity/__init__.py b/packit/src/integrations/chat/__init__.py similarity index 100% rename from packit/src/chatactivity/__init__.py rename to packit/src/integrations/chat/__init__.py diff --git a/packit/src/chatactivity/export/DecryptorBottomSheet.py b/packit/src/integrations/chat/export/DecryptorBottomSheet.py similarity index 90% rename from packit/src/chatactivity/export/DecryptorBottomSheet.py rename to packit/src/integrations/chat/export/DecryptorBottomSheet.py index 8c36b69..aa85d86 100644 --- a/packit/src/chatactivity/export/DecryptorBottomSheet.py +++ b/packit/src/integrations/chat/export/DecryptorBottomSheet.py @@ -37,8 +37,8 @@ def before_hooked_method(self, param): def _prepare_and_show(self, file_path: str): try: - from ...chatactivity.export.bin.Writer import _get_user_id, _get_install_ts - from ...chatactivity.export.bin.Reader import read_file + from .bin.Writer import _get_user_id, _get_install_ts + from .bin.Reader import read_file from elyx import strings current_user_id = _get_user_id() @@ -54,14 +54,14 @@ def _prepare_and_show(self, file_path: str): import_xp = None if "achievements" in blocks: try: - from ...ui.achievementsactivity.service.AchivementsEngine import get_level_info + from ....ui.achievements.service.AchivementsEngine import get_level_info achievements_data = json.loads(blocks["achievements"]) def _is_hashed_id(k: str) -> bool: return len(k) == 16 and all(c in "0123456789abcdef" for c in k) if isinstance(achievements_data, dict) and achievements_data and all(_is_hashed_id(k) for k in achievements_data): - from ...ui.achievementsactivity.service.AchivementsEngine import _hash_account_id + from ....ui.achievements.service.AchivementsEngine import _hash_account_id account_data = achievements_data.get(_hash_account_id(export_user_id), {}) elif isinstance(achievements_data, dict): account_data = achievements_data @@ -76,7 +76,7 @@ def _is_hashed_id(k: str) -> bool: from .ImportBottomSheet import show_import_bottom_sheet def on_confirm(): - from ...ui.achievementsactivity.service.AchivementsEngine import _hash_account_id + from ....ui.achievements.service.AchivementsEngine import _hash_account_id account_id = _hash_account_id(export_user_id) threading.Thread(target=self._restore, args=(blocks, account_id), daemon=True).start() @@ -92,7 +92,7 @@ def show(): def _restore(self, blocks: dict, account_id: str): try: - from ...chatactivity.export.bin.Reader import _write_blocks + from .bin.Reader import _write_blocks from client_utils import get_last_fragment from ui.bulletin import BulletinHelper from org.telegram.messenger import R diff --git a/packit/src/chatactivity/export/ImportBottomSheet.py b/packit/src/integrations/chat/export/ImportBottomSheet.py similarity index 97% rename from packit/src/chatactivity/export/ImportBottomSheet.py rename to packit/src/integrations/chat/export/ImportBottomSheet.py index 10b035a..3f8ad94 100644 --- a/packit/src/chatactivity/export/ImportBottomSheet.py +++ b/packit/src/integrations/chat/export/ImportBottomSheet.py @@ -41,7 +41,7 @@ def show_import_bottom_sheet(fragment, num_blocks: int, on_confirm, import_level level = import_level xp_into = import_xp else: - from ...ui.achievementsactivity.service.AchivementsEngine import get_level_info, _load_account + from ....ui.achievements.service.AchivementsEngine import get_level_info, _load_account data, _ = _load_account() level, xp_into, _ = get_level_info(data) diff --git a/packit/src/chatactivity/export/__init__.py b/packit/src/integrations/chat/export/__init__.py similarity index 100% rename from packit/src/chatactivity/export/__init__.py rename to packit/src/integrations/chat/export/__init__.py diff --git a/packit/src/chatactivity/export/bin/Reader.py b/packit/src/integrations/chat/export/bin/Reader.py similarity index 97% rename from packit/src/chatactivity/export/bin/Reader.py rename to packit/src/integrations/chat/export/bin/Reader.py index e27e184..c045de3 100644 --- a/packit/src/chatactivity/export/bin/Reader.py +++ b/packit/src/integrations/chat/export/bin/Reader.py @@ -52,7 +52,7 @@ def _is_hashed_id(k: str) -> bool: account_data = account_data["d"] depth += 1 - from ....ui.achievementsactivity.service.AchivementsEngine import load_account_data_for_import + from .....ui.achievements.service.AchivementsEngine import load_account_data_for_import load_account_data_for_import(account_id, account_data) logx(f"exportBin: merged achievements for account {account_id}", True) diff --git a/packit/src/chatactivity/export/bin/Writer.py b/packit/src/integrations/chat/export/bin/Writer.py similarity index 95% rename from packit/src/chatactivity/export/bin/Writer.py rename to packit/src/integrations/chat/export/bin/Writer.py index 7a143e8..673960c 100644 --- a/packit/src/chatactivity/export/bin/Writer.py +++ b/packit/src/integrations/chat/export/bin/Writer.py @@ -25,7 +25,7 @@ def _get_configs_dir() -> str: - from ....utils.Paths import getConfigsDir + from .....utils.Paths import getConfigsDir return getConfigsDir() @@ -55,13 +55,13 @@ def _get_install_ts() -> int: def _get_lib(): - from ....NativeLoader import loadExport + from .....core.NativeLoader import loadExport return loadExport() def _read_achievements_block() -> str: try: - from ....ui.achievementsactivity.service.AchivementsEngine import ( + from .....ui.achievements.service.AchivementsEngine import ( _load_account, _get_current_account_id ) account_id = _get_current_account_id() @@ -76,7 +76,7 @@ def _read_achievements_block() -> str: def _read_saved_plugins_block() -> str: try: - from ....ui.pluginactivity.Fragment import _read_saved_plugins + from .....ui.plugin.Fragment import _read_saved_plugins data = _read_saved_plugins() content = json.dumps(data, ensure_ascii=False) logx(f"exportBin: saved_plugins block read ({len(data)} items)", True) diff --git a/packit/src/chatactivity/export/bin/__init__.py b/packit/src/integrations/chat/export/bin/__init__.py similarity index 100% rename from packit/src/chatactivity/export/bin/__init__.py rename to packit/src/integrations/chat/export/bin/__init__.py diff --git a/packit/src/chatactivity/inline/EnterView.py b/packit/src/integrations/chat/inline/EnterView.py similarity index 98% rename from packit/src/chatactivity/inline/EnterView.py rename to packit/src/integrations/chat/inline/EnterView.py index 3d615f9..d240c7c 100644 --- a/packit/src/chatactivity/inline/EnterView.py +++ b/packit/src/integrations/chat/inline/EnterView.py @@ -107,7 +107,7 @@ def _flag_match(plugin, flags): # app_version: each expression must pass check_app_version if "app_version" in flags: - from ...utils.AppVersion import check_app_version + from ....utils.AppVersion import check_app_version for expr in flags["app_version"]: if not check_app_version(expr): return False @@ -241,8 +241,8 @@ def do_search(): def _packit_load_plugins_from_cache(self): - from ...network import Storage - from ...utils import CachedRepos + from ....network import Storage + from ....utils import CachedRepos plugins_list = [] try: for repo in self.repoManager.getRepositories(): @@ -296,7 +296,7 @@ def _packit_search_in_background(self, search_key, token): run_on_ui_thread(lambda: self._packit_show_plugins_popup(result)) return - from ...utils.Search import build_index, score as search_score + from ....utils.Search import build_index, score as search_score index = build_index(candidates) @@ -563,8 +563,8 @@ def onItemClick(self, view, position): def open_profile(): try: - from ...ui.pluginlistactivity.Fragment import InstallUI - from ...ui.pluginactivity.Fragment import show_plugin_profile + from ....ui.plugins.Fragment import InstallUI + from ....ui.plugin.Fragment import show_plugin_profile class _FakePlugin: def __init__(self, rm): @@ -685,7 +685,7 @@ def _u16len(text) -> int: def _strip_markdown(text) -> str: # plain text for places that cannot render entities (the autocomplete popup # binds its rows to java Strings), so markers don't show up raw - from ...utils.Markdown import to_plain + from ....utils.Markdown import to_plain return to_plain(text) diff --git a/packit/src/chatactivity/inline/InlineBtns.py b/packit/src/integrations/chat/inline/InlineBtns.py similarity index 99% rename from packit/src/chatactivity/inline/InlineBtns.py rename to packit/src/integrations/chat/inline/InlineBtns.py index 0f1c541..1c649c8 100644 --- a/packit/src/chatactivity/inline/InlineBtns.py +++ b/packit/src/integrations/chat/inline/InlineBtns.py @@ -178,7 +178,7 @@ def _do_translate_inline(message_object): # runs on background thread: translates only the description, rebuilds message with formatting try: from client_utils import edit_message - from ...utils.Translation import _translate_text + from ....utils.Translation import _translate_text from java.util import Locale owner = message_object.messageOwner @@ -340,7 +340,7 @@ def _do_send_file_inline(message_object, plugin_ref): # resolve plugins url from repo cache link = None try: - from ...ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url, _fetch_repo_plugins + from ....ui.updates.Fragment import _get_repos, _get_repo_plugins_url, _fetch_repo_plugins repos = _get_repos() repo_url = None for r in repos: diff --git a/packit/src/chatactivity/inline/InlineState.py b/packit/src/integrations/chat/inline/InlineState.py similarity index 100% rename from packit/src/chatactivity/inline/InlineState.py rename to packit/src/integrations/chat/inline/InlineState.py diff --git a/packit/src/chatactivity/inline/MessageBuilder.py b/packit/src/integrations/chat/inline/MessageBuilder.py similarity index 99% rename from packit/src/chatactivity/inline/MessageBuilder.py rename to packit/src/integrations/chat/inline/MessageBuilder.py index 61b87c6..0668115 100644 --- a/packit/src/chatactivity/inline/MessageBuilder.py +++ b/packit/src/integrations/chat/inline/MessageBuilder.py @@ -86,7 +86,7 @@ def span(entity, start, text, **attrs): quote_start = offset if show_description and description: - from ...utils.Markdown import parse as md_parse + from ....utils.Markdown import parse as md_parse parsed = md_parse(description) if parsed is not None: desc_text = parsed.text diff --git a/packit/src/chatactivity/inline/__init__.py b/packit/src/integrations/chat/inline/__init__.py similarity index 100% rename from packit/src/chatactivity/inline/__init__.py rename to packit/src/integrations/chat/inline/__init__.py diff --git a/packit/src/chatactivity/linksicons/LinksBottomSheet.py b/packit/src/integrations/chat/linksicons/LinksBottomSheet.py similarity index 100% rename from packit/src/chatactivity/linksicons/LinksBottomSheet.py rename to packit/src/integrations/chat/linksicons/LinksBottomSheet.py diff --git a/packit/src/chatactivity/linksicons/__init__.py b/packit/src/integrations/chat/linksicons/__init__.py similarity index 100% rename from packit/src/chatactivity/linksicons/__init__.py rename to packit/src/integrations/chat/linksicons/__init__.py diff --git a/packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py b/packit/src/integrations/chat/securitybottomsheets/HashBottomSheet.py similarity index 99% rename from packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py rename to packit/src/integrations/chat/securitybottomsheets/HashBottomSheet.py index 4e423b9..383e30d 100644 --- a/packit/src/chatactivity/securitybottomsheets/HashBottomSheet.py +++ b/packit/src/integrations/chat/securitybottomsheets/HashBottomSheet.py @@ -26,7 +26,7 @@ logx(f"hashBottomSheet: import LayoutHelper error: {e}", False) -from ...utils.HashUtil import hashFile as _computeSha256 +from ....utils.HashUtil import hashFile as _computeSha256 def _extractPluginVersion(filePath: str) -> str | None: @@ -63,7 +63,7 @@ def _extractPluginId(filePath: str) -> str | None: def _loadCachedRepos() -> list: # [(name, pluginsUrl, repoId), …] for every repository with a usable cache - from ...utils import CachedRepos + from ....utils import CachedRepos result = [] for rm_rid, cached in CachedRepos.all_cached(): pluginsUrl = CachedRepos.plugins_url(rm_rid) @@ -77,7 +77,7 @@ def _loadCachedRepos() -> list: def _getRepoPluginInfo(pluginId: str, pluginsUrl: str) -> dict | None: # this walked r.json()["plugins"] as a list, so it found nothing at all in a # repository that keys its plugins by id — Storage answers in one shape - from ...network import Storage + from ....network import Storage entries, error = Storage.fetch_plugins(pluginsUrl) if error: if DEBUG_LOGS: @@ -116,7 +116,7 @@ def action(): def task(): try: - from ...network import Storage + from ....network import Storage entries, error = Storage.fetch_plugins(pluginsUrl) if error: dismissDlg() @@ -136,7 +136,7 @@ def task(): run_on_ui_thread(lambda: BulletinHelper.show_error(strings["sec_plugin_no_link"])) return - from ...utils.Paths import getPluginsDir + from ....utils.Paths import getPluginsDir pluginsDir = getPluginsDir() os.makedirs(pluginsDir, exist_ok=True) tempPath = os.path.join(pluginsDir, f".temp_{pluginId}.plugin") diff --git a/packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py b/packit/src/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py similarity index 99% rename from packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py rename to packit/src/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py index 1b0c74e..001666b 100644 --- a/packit/src/chatactivity/securitybottomsheets/SignaturesBottomSheet.py +++ b/packit/src/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py @@ -800,7 +800,7 @@ def _showResults(results: dict, act): def onLearnMore(v): try: - from ...utils.LocalConfig import LocalConfig + from ....utils.LocalConfig import LocalConfig LocalConfig.set("signatures", True) except Exception as ex: if DEBUG_LOGS: @@ -853,7 +853,7 @@ def onLearnMore(v): wrapper.addView(warningView, lp_warn) try: - from ...utils.LocalConfig import LocalConfig + from ....utils.LocalConfig import LocalConfig showLearnMore = not LocalConfig.get("signatures", False) except Exception: showLearnMore = True diff --git a/packit/src/chatactivity/securitybottomsheets/__init__.py b/packit/src/integrations/chat/securitybottomsheets/__init__.py similarity index 100% rename from packit/src/chatactivity/securitybottomsheets/__init__.py rename to packit/src/integrations/chat/securitybottomsheets/__init__.py diff --git a/packit/src/dialogsactivity/BtnCAB.py b/packit/src/integrations/chatlist/BtnCAB.py similarity index 98% rename from packit/src/dialogsactivity/BtnCAB.py rename to packit/src/integrations/chatlist/BtnCAB.py index bf7b402..d746e71 100644 --- a/packit/src/dialogsactivity/BtnCAB.py +++ b/packit/src/integrations/chatlist/BtnCAB.py @@ -9,13 +9,13 @@ from org.telegram.ui import ChatActivity except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui import ChatActivity failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from hook_utils import find_class try: from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() class BtnCAB: diff --git a/packit/src/dialogsactivity/BtnPluginsMenu.py b/packit/src/integrations/chatlist/BtnPluginsMenu.py similarity index 96% rename from packit/src/dialogsactivity/BtnPluginsMenu.py rename to packit/src/integrations/chatlist/BtnPluginsMenu.py index d0f9322..f0f9a5f 100644 --- a/packit/src/dialogsactivity/BtnPluginsMenu.py +++ b/packit/src/integrations/chatlist/BtnPluginsMenu.py @@ -7,7 +7,7 @@ from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() _SETTINGS_LINK = "https://t.me/exteraSettings?s=mainMenuSettings" diff --git a/packit/src/dialogsactivity/BuildNotCorrect.py b/packit/src/integrations/chatlist/BuildNotCorrect.py similarity index 98% rename from packit/src/dialogsactivity/BuildNotCorrect.py rename to packit/src/integrations/chatlist/BuildNotCorrect.py index 81e0279..98f4359 100644 --- a/packit/src/dialogsactivity/BuildNotCorrect.py +++ b/packit/src/integrations/chatlist/BuildNotCorrect.py @@ -22,7 +22,7 @@ def _getDismissedHash() -> str: try: - from ..utils.LocalConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig return LocalConfig.get(_HASH_CONFIG_KEY, "") except Exception as e: logx(f"buildNotCorrect: _getDismissedHash error: {e}", False) @@ -31,7 +31,7 @@ def _getDismissedHash() -> str: def _saveDismissedHash(hashVal: str): try: - from ..utils.LocalConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig LocalConfig.set(_HASH_CONFIG_KEY, hashVal) except Exception as e: logx(f"buildNotCorrect: _saveDismissedHash error: {e}", False) @@ -224,7 +224,7 @@ def onClick(self, v): def _checkAndShow(): try: - from ..utils.BuildInfo import ( + from ...utils.BuildInfo import ( getBuildClientPkg, getBuildClientName, getBuildStaticVersion, getCurrClientPkg, getCurrClientName, diff --git a/packit/src/dialogsactivity/Button.py b/packit/src/integrations/chatlist/Button.py similarity index 92% rename from packit/src/dialogsactivity/Button.py rename to packit/src/integrations/chatlist/Button.py index edfa0a7..e8707b2 100644 --- a/packit/src/dialogsactivity/Button.py +++ b/packit/src/integrations/chatlist/Button.py @@ -8,17 +8,17 @@ from com.exteragram.messenger.plugins import PluginsController except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins import PluginsController failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.ui import PluginSettingsActivity except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.ui import PluginSettingsActivity failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from .BtnCAB import BtnCAB from .BtnPluginsMenu import BtnPluginsMenu diff --git a/packit/src/dialogsactivity/ChatDialogButton.py b/packit/src/integrations/chatlist/ChatDialogButton.py similarity index 99% rename from packit/src/dialogsactivity/ChatDialogButton.py rename to packit/src/integrations/chatlist/ChatDialogButton.py index 41ee1a4..9dc0eb8 100644 --- a/packit/src/dialogsactivity/ChatDialogButton.py +++ b/packit/src/integrations/chatlist/ChatDialogButton.py @@ -161,10 +161,10 @@ def _open(): logx(f"ChatDialogButton: open settings error: {e}", False) run_on_ui_thread(_open) elif m == 2: - from ..ui.iconslistactivity.Fragment import InstallIconsUI + from ...ui.icons.Fragment import InstallIconsUI run_on_ui_thread(lambda: InstallIconsUI(plugin).open()) else: - from ..ui.pluginlistactivity.Fragment import InstallUI + from ...ui.plugins.Fragment import InstallUI run_on_ui_thread(lambda: InstallUI(plugin).open()) except Exception as e: logx(f"ChatDialogButton: onClick error: {e}", False) diff --git a/packit/src/dialogsactivity/PackitUpdateSheet.py b/packit/src/integrations/chatlist/PackitUpdateSheet.py similarity index 98% rename from packit/src/dialogsactivity/PackitUpdateSheet.py rename to packit/src/integrations/chatlist/PackitUpdateSheet.py index dfd4206..b7f595c 100644 --- a/packit/src/dialogsactivity/PackitUpdateSheet.py +++ b/packit/src/integrations/chatlist/PackitUpdateSheet.py @@ -69,7 +69,7 @@ def _get_current_version() -> str: def _get_dismissed_ver() -> str: try: - from ..utils.LocalConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig v = LocalConfig.get("update_dismissed_ver", "") logx(f"updateSheet: dismissed_ver='{v}'", True) return v @@ -80,7 +80,7 @@ def _get_dismissed_ver() -> str: def _save_dismissed_ver(ver: str): try: - from ..utils.LocalConfig import LocalConfig + from ...utils.LocalConfig import LocalConfig LocalConfig.set("update_dismissed_ver", ver) logx(f"updateSheet: saved dismissed_ver='{ver}'", True) except Exception as e: @@ -127,7 +127,7 @@ def _show_update_sheet(new_ver: str, changelog: str, sticker: str, download_url: iv.getImageReceiver().setCrossfadeWithOldImage(True) except Exception as e: logx(f"updateSheet: setCrossfadeWithOldImage error: {e}", False) - from ..utils.Stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(iv, sticker, sticker_size_dp) linear.addView(iv, LayoutHelper.createLinear( sticker_size_dp, sticker_size_dp, Gravity.CENTER_HORIZONTAL, 0, 16, 0, 0 diff --git a/packit/src/dialogsactivity/PillWidget.py b/packit/src/integrations/chatlist/PillWidget.py similarity index 99% rename from packit/src/dialogsactivity/PillWidget.py rename to packit/src/integrations/chatlist/PillWidget.py index a1c9cf1..bf68cd1 100644 --- a/packit/src/dialogsactivity/PillWidget.py +++ b/packit/src/integrations/chatlist/PillWidget.py @@ -429,7 +429,7 @@ def _open_settings(plugin): def _open_install(plugin): try: - from ..ui.pluginlistactivity.Fragment import InstallUI + from ...ui.plugins.Fragment import InstallUI InstallUI(plugin).open() except Exception as e: logx(f"PillWidget: _open_install error: {e}", False) @@ -437,7 +437,7 @@ def _open_install(plugin): def _open_icons(plugin): try: - from ..ui.iconslistactivity.Fragment import InstallIconsUI + from ...ui.icons.Fragment import InstallIconsUI InstallIconsUI(plugin).open() except Exception as e: logx(f"PillWidget: _open_icons error: {e}", False) diff --git a/packit/src/dialogsactivity/UpdatesWidget.py b/packit/src/integrations/chatlist/UpdatesWidget.py similarity index 97% rename from packit/src/dialogsactivity/UpdatesWidget.py rename to packit/src/integrations/chatlist/UpdatesWidget.py index 3161c25..5e420af 100644 --- a/packit/src/dialogsactivity/UpdatesWidget.py +++ b/packit/src/integrations/chatlist/UpdatesWidget.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.NetQueue import run_io +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 @@ -206,7 +206,7 @@ def _prefetch_and_register(plugin): def task(): try: - from ..ui.pluginsupdates.Fragment import _check_updates, _filter_ignored + from ...ui.updates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) _updates_count[0] = len(updates) _updates_list[0] = updates @@ -382,7 +382,7 @@ def _on_click(plugin, pill): def _install_single(plugin, item): # installs the single available update directly, then re-checks on success from client_utils import run_on_queue - from ..ui.pluginsupdates.Fragment import _get_repos, _get_repo_plugins_url + from ...ui.updates.Fragment import _get_repos, _get_repo_plugins_url import requests as _req pid = str(item.get("id") or "") @@ -392,7 +392,7 @@ def _on_installed(installed_pid): if installed_pid != pid: return try: - from ..Core import remove_install_listener + from ...core.Core import remove_install_listener remove_install_listener(_on_installed) except Exception as e: logx(f"UpdatesWidget: remove_install_listener error: {e}", False) @@ -431,7 +431,7 @@ def task(): if not plugin_data: logx(f"UpdatesWidget: _install_single plugin '{pid}' not found in repo", True) return - from ..Core import install_plugin, add_install_listener + from ...core.Core import install_plugin, add_install_listener add_install_listener(_on_installed) from android_utils import run_on_ui_thread run_on_ui_thread(lambda: install_plugin(plugin_data, all_plugins=all_plugins, rm_rid=repo_id)) @@ -443,7 +443,7 @@ def task(): def _open_updates(plugin): try: - from ..ui.pluginsupdates.Fragment import show_updates_fragment + from ...ui.updates.Fragment import show_updates_fragment show_updates_fragment(plugin) except Exception as e: logx(f"UpdatesWidget: _open_updates error: {e}", False) @@ -477,7 +477,7 @@ def finish_loading(count, updates): def task(): try: - from ..ui.pluginsupdates.Fragment import _check_updates, _filter_ignored + from ...ui.updates.Fragment import _check_updates, _filter_ignored updates = _filter_ignored(None, _check_updates(None)) count = len(updates) run_on_ui_thread(lambda: finish_loading(count, updates)) diff --git a/packit/src/dialogsactivity/__init__.py b/packit/src/integrations/chatlist/__init__.py similarity index 100% rename from packit/src/dialogsactivity/__init__.py rename to packit/src/integrations/chatlist/__init__.py diff --git a/packit/src/other/Badges.py b/packit/src/integrations/decorations/Badges.py similarity index 98% rename from packit/src/other/Badges.py rename to packit/src/integrations/decorations/Badges.py index 2da973e..dcbacb0 100644 --- a/packit/src/other/Badges.py +++ b/packit/src/integrations/decorations/Badges.py @@ -128,7 +128,7 @@ def setup_hooks(self): # primary path: precompiled Kotlin dex (config fetch + cache + hooks # all live in packit/dex//badges.dex, source in /kotlin/) try: - from ..DexLoader import loadBadges + from ...core.DexLoader import loadBadges if loadBadges(self.context, enabled): self._dex_loaded = True logx("[Packit Badges] using precompiled dex", True) @@ -206,7 +206,7 @@ def cleanup(self): try: if self._dex_loaded: try: - from ..DexLoader import unloadBadges + from ...core.DexLoader import unloadBadges unloadBadges() except Exception as e: logx(f"[Packit Badges] dex unload error: {e}", False) diff --git a/packit/src/other/ChatBadge.py b/packit/src/integrations/decorations/ChatBadge.py similarity index 100% rename from packit/src/other/ChatBadge.py rename to packit/src/integrations/decorations/ChatBadge.py diff --git a/packit/src/other/ChatTitleIcon.py b/packit/src/integrations/decorations/ChatTitleIcon.py similarity index 100% rename from packit/src/other/ChatTitleIcon.py rename to packit/src/integrations/decorations/ChatTitleIcon.py diff --git a/packit/src/other/Everyone.py b/packit/src/integrations/decorations/Everyone.py similarity index 98% rename from packit/src/other/Everyone.py rename to packit/src/integrations/decorations/Everyone.py index 91b2b11..c80175c 100644 --- a/packit/src/other/Everyone.py +++ b/packit/src/integrations/decorations/Everyone.py @@ -23,7 +23,7 @@ def _get_cache_path() -> str: - from ..utils.Paths import getCacheRoot + from ...utils.Paths import getCacheRoot cache_dir = getCacheRoot() os.makedirs(cache_dir, exist_ok=True) return os.path.join(cache_dir, _CACHE_FILENAME) diff --git a/packit/src/other/IsBeta.py b/packit/src/integrations/decorations/IsBeta.py similarity index 98% rename from packit/src/other/IsBeta.py rename to packit/src/integrations/decorations/IsBeta.py index 8945ea7..90c0b5a 100644 --- a/packit/src/other/IsBeta.py +++ b/packit/src/integrations/decorations/IsBeta.py @@ -18,8 +18,8 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from ..utils.LocalConfig import LocalConfig + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from ...utils.LocalConfig import LocalConfig BETA = False _COUNTDOWN_SEC = 5 diff --git a/packit/src/other/ProfileTitleIcon.py b/packit/src/integrations/decorations/ProfileTitleIcon.py similarity index 100% rename from packit/src/other/ProfileTitleIcon.py rename to packit/src/integrations/decorations/ProfileTitleIcon.py diff --git a/packit/src/other/Text.py b/packit/src/integrations/decorations/Text.py similarity index 92% rename from packit/src/other/Text.py rename to packit/src/integrations/decorations/Text.py index 0782525..1275983 100644 --- a/packit/src/other/Text.py +++ b/packit/src/integrations/decorations/Text.py @@ -16,7 +16,7 @@ def check_message(text: str): return lower = text.lower().strip() try: - from ..ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ...ui.achievements.service.AchivementsEngine import unlock_secret if lower == _TRIGGER_TALKING: unlock_secret("talking_about_you") elif lower == _TRIGGER_UTILS: diff --git a/packit/src/other/__init__.py b/packit/src/integrations/decorations/__init__.py similarity index 100% rename from packit/src/other/__init__.py rename to packit/src/integrations/decorations/__init__.py diff --git a/packit/src/standalonehooks/AddIconsFab.py b/packit/src/integrations/hooks/AddIconsFab.py similarity index 98% rename from packit/src/standalonehooks/AddIconsFab.py rename to packit/src/integrations/hooks/AddIconsFab.py index 793e322..bb93f1d 100644 --- a/packit/src/standalonehooks/AddIconsFab.py +++ b/packit/src/integrations/hooks/AddIconsFab.py @@ -142,7 +142,7 @@ def _inject_fab(plugin, frag_view, fragment=None): def on_fab_click(v): try: - from ..ui.iconslistactivity.Fragment import InstallIconsUI + from ...ui.icons.Fragment import InstallIconsUI InstallIconsUI(plugin).open() except Exception as e: logx(f"addIconsFab: on_fab_click error: {e}", False) diff --git a/packit/src/standalonehooks/AddPluginFab.py b/packit/src/integrations/hooks/AddPluginFab.py similarity index 99% rename from packit/src/standalonehooks/AddPluginFab.py rename to packit/src/integrations/hooks/AddPluginFab.py index 70008de..7054542 100644 --- a/packit/src/standalonehooks/AddPluginFab.py +++ b/packit/src/integrations/hooks/AddPluginFab.py @@ -108,7 +108,7 @@ def _inject_fab(plugin, frag_view): def on_fab_click(v): try: - from ..ui.pluginlistactivity.Fragment import InstallUI + from ...ui.plugins.Fragment import InstallUI InstallUI(plugin).open() except Exception as e: logx(f"addPluginFab: on_fab_click error: {e}", False) diff --git a/packit/src/standalonehooks/InstallDismissHook.py b/packit/src/integrations/hooks/InstallDismissHook.py similarity index 93% rename from packit/src/standalonehooks/InstallDismissHook.py rename to packit/src/integrations/hooks/InstallDismissHook.py index 23a820d..044c1ab 100644 --- a/packit/src/standalonehooks/InstallDismissHook.py +++ b/packit/src/integrations/hooks/InstallDismissHook.py @@ -20,14 +20,14 @@ def after_hooked_method(self, param): if error_str is not None: return try: - from ..ui.achievementsactivity.service.AchivementsEngine import increment_category + from ...ui.achievements.service.AchivementsEngine import increment_category increment_category("Installing plugins") except Exception as e: logx(f"installSuccessHook: achievements increment error: {e}", False) # backstop index write; core's pluginsUpdated observer is the # primary path. commit_pending consumes _pending -> idempotent. try: - from ..utils.InstallIndex import commit_pending + from ...utils.InstallIndex import commit_pending commit_pending() except Exception as e: logx(f"installSuccessHook: commit_pending error: {e}", False) diff --git a/packit/src/standalonehooks/SettingsActivityHook.py b/packit/src/integrations/hooks/SettingsActivityHook.py similarity index 100% rename from packit/src/standalonehooks/SettingsActivityHook.py rename to packit/src/integrations/hooks/SettingsActivityHook.py diff --git a/packit/src/standalonehooks/UniversalFragmentFix.py b/packit/src/integrations/hooks/UniversalFragmentFix.py similarity index 100% rename from packit/src/standalonehooks/UniversalFragmentFix.py rename to packit/src/integrations/hooks/UniversalFragmentFix.py diff --git a/packit/src/settingsactivity/__init__.py b/packit/src/integrations/hooks/__init__.py similarity index 100% rename from packit/src/settingsactivity/__init__.py rename to packit/src/integrations/hooks/__init__.py diff --git a/packit/src/scl/Native.py b/packit/src/scl/Native.py index 137c1bd..3b51ed2 100644 --- a/packit/src/scl/Native.py +++ b/packit/src/scl/Native.py @@ -3,7 +3,7 @@ def _soPath() -> str: from ..utils.Paths import _filesDir - from ..NativeLoader import detectArch + from ..core.NativeLoader import detectArch arch = detectArch() return _filesDir() + f"/plugins/ElyxPlugins/shareui_packit/packit/native/{arch}/libscl.so" diff --git a/packit/src/MainActivity.py b/packit/src/ui/MainActivity.py similarity index 92% rename from packit/src/MainActivity.py rename to packit/src/ui/MainActivity.py index d76a8c9..794ded2 100644 --- a/packit/src/MainActivity.py +++ b/packit/src/ui/MainActivity.py @@ -7,13 +7,13 @@ from elyx import strings, metainfo except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, metainfo failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from .settingsactivity.Deeplinks import DeeplinksSettings -from .settingsactivity.Settings import OtherSettings -from .settingsactivity.Docs import DocumentationSettings -from .ui.contributors.Fragment import show_contributors_fragment -from .settingsactivity.Profile import ProfileSettings -from .settingsactivity.Utilities import UtilitiesSettings + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from .settings.Deeplinks import DeeplinksSettings +from .settings.Settings import OtherSettings +from .settings.Docs import DocumentationSettings +from .contributors.Fragment import show_contributors_fragment +from .settings.Profile import ProfileSettings +from .settings.Utilities import UtilitiesSettings from ui.bulletin import BulletinHelper from base_plugin import BasePlugin, MethodHook from android_utils import run_on_ui_thread @@ -22,17 +22,17 @@ from org.telegram.ui.ActionBar import Theme, BottomSheet except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme, BottomSheet failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper, UItem, BackupImageView, EffectsTextView, BulletinFactory except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper, UItem, BackupImageView, EffectsTextView, BulletinFactory failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from com.exteragram.messenger.plugins.models import HeaderSetting except Exception as e: import android_utils as _au; _au.log(f"import com.exteragram.messenger.plugins.models import HeaderSetting failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.widget import FrameLayout, TextView, LinearLayout, ScrollView from android.graphics.drawable import GradientDrawable from android.view import Gravity @@ -41,15 +41,15 @@ from org.telegram.messenger import AndroidUtilities, ImageLocation, MediaDataController, R except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, ImageLocation, MediaDataController, R failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from .ui.pluginlistactivity.Fragment import InstallUI -from .ui.iconslistactivity.Fragment import InstallIconsUI + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from .plugins.Fragment import InstallUI +from .icons.Fragment import InstallIconsUI from client_utils import get_last_fragment try: from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from .utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.net import Uri from java import dynamic_proxy as dyp from android_utils import OnClickListener, OnLongClickListener @@ -147,12 +147,12 @@ def _create_settings_header(self, context): imageView = BackupImageView(context) imageView.setRoundRadius(AndroidUtilities.dp(45)) - from .utils.Stickers import load_sticker + from ..utils.Stickers import load_sticker load_sticker(imageView, __icon__, 130) def _on_sticker_long_click(): try: - from .settingsactivity.DebugItems import show_debug_menu + from .settings.DebugItems import show_debug_menu show_debug_menu() except Exception as _e: logx(f"MainActivity: sticker long click error: {_e}", True) @@ -193,7 +193,7 @@ def _open_install_plugin(self, view): def _check_updates(self, view): try: - from .ui.pluginsupdates.Fragment import show_updates_fragment + from .updates.Fragment import show_updates_fragment show_updates_fragment(self.plugin) except Exception as e: @@ -201,7 +201,7 @@ def _check_updates(self, view): def _open_repositories(self, view): try: - from .ui.reposactivity import show_repos_fragment + from .repos import show_repos_fragment show_repos_fragment(self.repoManager) except Exception as e: logx(f"MainActivity: _open_repositories error: {e}", False) @@ -358,7 +358,7 @@ def buildMainSettings(self): ] def _build_client_label(self): - from .utils.BuildInfo import getBuildClientName, getBuildStaticVersion + from ..utils.BuildInfo import getBuildClientName, getBuildStaticVersion client_str = getBuildClientName() static_ver = getBuildStaticVersion() diff --git a/packit/src/ui/achievementsactivity/Fragment.py b/packit/src/ui/achievements/Fragment.py similarity index 99% rename from packit/src/ui/achievementsactivity/Fragment.py rename to packit/src/ui/achievements/Fragment.py index 3ae4972..bb7a702 100644 --- a/packit/src/ui/achievementsactivity/Fragment.py +++ b/packit/src/ui/achievements/Fragment.py @@ -462,7 +462,7 @@ def afterCreateView(self, view): _add_actionbar_glow(view) _add_bottom_glow(view) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(view) except Exception: pass @@ -630,7 +630,7 @@ def afterCreateView(self, view): _add_actionbar_glow(view) _add_bottom_glow(view) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(view) except Exception: pass @@ -761,7 +761,7 @@ def show_hint_sheet(achievement: dict): sheet.setCustomView(root) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/settingsactivity/service/__init__.py b/packit/src/ui/achievements/__init__.py similarity index 100% rename from packit/src/settingsactivity/service/__init__.py rename to packit/src/ui/achievements/__init__.py diff --git a/packit/src/ui/achievementsactivity/service/AchivementsEngine.py b/packit/src/ui/achievements/service/AchivementsEngine.py similarity index 99% rename from packit/src/ui/achievementsactivity/service/AchivementsEngine.py rename to packit/src/ui/achievements/service/AchivementsEngine.py index 69d0b31..7bc4cf3 100644 --- a/packit/src/ui/achievementsactivity/service/AchivementsEngine.py +++ b/packit/src/ui/achievements/service/AchivementsEngine.py @@ -78,7 +78,7 @@ def _get_current_account_id() -> str: return "0" def _load_lib(): - from ....NativeLoader import loadPackitDb + from ....core.NativeLoader import loadPackitDb lib = loadPackitDb() if lib is not None: logx("packitdb: libpackitdb loaded ok", True) @@ -389,7 +389,7 @@ def show(): return def _open(): - from ....ui.achievementsactivity.Fragment import show_hint_sheet + from ..Fragment import show_hint_sheet show_hint_sheet(achievement) ctx = fragment.getContext() diff --git a/packit/src/settingsactivity/subsettings/__init__.py b/packit/src/ui/achievements/service/__init__.py similarity index 100% rename from packit/src/settingsactivity/subsettings/__init__.py rename to packit/src/ui/achievements/service/__init__.py diff --git a/packit/src/ui/ContextMenu.py b/packit/src/ui/components/ContextMenu.py similarity index 100% rename from packit/src/ui/ContextMenu.py rename to packit/src/ui/components/ContextMenu.py diff --git a/packit/src/ui/FontManager.py b/packit/src/ui/components/FontManager.py similarity index 100% rename from packit/src/ui/FontManager.py rename to packit/src/ui/components/FontManager.py diff --git a/packit/src/ui/Md3Slider.py b/packit/src/ui/components/Md3Slider.py similarity index 100% rename from packit/src/ui/Md3Slider.py rename to packit/src/ui/components/Md3Slider.py diff --git a/packit/src/ui/ViewUtils.py b/packit/src/ui/components/ViewUtils.py similarity index 100% rename from packit/src/ui/ViewUtils.py rename to packit/src/ui/components/ViewUtils.py diff --git a/packit/src/ui/components/__init__.py b/packit/src/ui/components/__init__.py new file mode 100644 index 0000000..50b2625 --- /dev/null +++ b/packit/src/ui/components/__init__.py @@ -0,0 +1,4 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Pieces the plugin's screens are assembled from, none of which is a screen. diff --git a/packit/src/ui/DeeplinkBottomSheets.py b/packit/src/ui/dialogs/DeeplinkBottomSheets.py similarity index 98% rename from packit/src/ui/DeeplinkBottomSheets.py rename to packit/src/ui/dialogs/DeeplinkBottomSheets.py index b954d7c..e4dc481 100644 --- a/packit/src/ui/DeeplinkBottomSheets.py +++ b/packit/src/ui/dialogs/DeeplinkBottomSheets.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.Bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf from android.view import View, MotionEvent from android.widget import LinearLayout, TextView, FrameLayout, ScrollView, ImageView from android.view import Gravity @@ -17,22 +17,22 @@ from org.telegram.ui.ActionBar import BottomSheet, Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import BottomSheet, Theme failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() DEEPLINKS_DATA = { @@ -474,7 +474,7 @@ def copy_link(v): root.addView(close_btn, LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) sheet.setCustomView(root) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/ExportBottomSheet.py b/packit/src/ui/dialogs/ExportBottomSheet.py similarity index 99% rename from packit/src/ui/ExportBottomSheet.py rename to packit/src/ui/dialogs/ExportBottomSheet.py index 3bda9d8..a036b9a 100644 --- a/packit/src/ui/ExportBottomSheet.py +++ b/packit/src/ui/dialogs/ExportBottomSheet.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.Ripple import safe_ripple as _safe_ripple +from ...utils.Ripple import safe_ripple as _safe_ripple import os import ast import re @@ -288,7 +288,7 @@ def _createCheckRow(act, label, version_str, icon_str, checked, on_change): except Exception: pass row.addView(icon_view, LayoutHelper.createLinear(icon_size_dp, icon_size_dp, Gravity.CENTER_VERTICAL, 0, 0, 10, 0)) - from ..utils.Stickers import load_sticker + from ...utils.Stickers import load_sticker load_sticker(icon_view, icon_str, icon_size_dp) except Exception as e: logx(f"ExportBottomSheet._createCheckRow: icon error: {e}\n{traceback.format_exc()}", False) @@ -788,7 +788,7 @@ def _onExport(): sheet.setCustomView(outer) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass diff --git a/packit/src/ui/FontPickerBottomSheet.py b/packit/src/ui/dialogs/FontPickerBottomSheet.py similarity index 98% rename from packit/src/ui/FontPickerBottomSheet.py rename to packit/src/ui/dialogs/FontPickerBottomSheet.py index 0eb0483..2908c01 100644 --- a/packit/src/ui/FontPickerBottomSheet.py +++ b/packit/src/ui/dialogs/FontPickerBottomSheet.py @@ -35,7 +35,7 @@ logx(f"FontPickerBottomSheet: import AndroidUtilities failed: {e}", False) AndroidUtilities = None -from .FontManager import listFontFiles, setFont, getSelectedFilename +from ..components.FontManager import listFontFiles, setFont, getSelectedFilename from java import dynamic_proxy from android.view import MotionEvent @@ -240,7 +240,7 @@ def _createRadioIndicator(act, is_selected): def _loadTypefaceForFile(filename): # loads Typeface from res/fonts/filename, returns None on failure try: - from .FontManager import getFontPath + from ..components.FontManager import getFontPath from android.graphics import Typeface path = getFontPath(filename) if path: @@ -412,7 +412,7 @@ def _showStyleSheet(act, family, styles, selected_filename, parent_sheet, on_sel root.addView(_createCloseButton(act, sheet.dismiss), LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass @@ -486,7 +486,7 @@ def showFontPicker(act, on_select=None): root.addView(_createCloseButton(act, sheet.dismiss), LayoutHelper.createLinear(-1, -2, 0, 16, 0, 8)) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/NoInternetBanner.py b/packit/src/ui/dialogs/NoInternetBanner.py similarity index 99% rename from packit/src/ui/NoInternetBanner.py rename to packit/src/ui/dialogs/NoInternetBanner.py index 35f7f98..fc1a389 100644 --- a/packit/src/ui/NoInternetBanner.py +++ b/packit/src/ui/dialogs/NoInternetBanner.py @@ -333,7 +333,7 @@ def _create_banner(self): except Exception: pass try: - from .ViewUtils import applyFont + from ..components.ViewUtils import applyFont applyFont(tv) except Exception: pass diff --git a/packit/src/ui/ReportDialog.py b/packit/src/ui/dialogs/ReportDialog.py similarity index 99% rename from packit/src/ui/ReportDialog.py rename to packit/src/ui/dialogs/ReportDialog.py index 147be71..2051c93 100644 --- a/packit/src/ui/ReportDialog.py +++ b/packit/src/ui/dialogs/ReportDialog.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.Bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf import ctypes import json import os @@ -122,13 +122,13 @@ def onAnimationRepeat(self, a, *args): pass def _load_reasons(repo_id: str) -> list: - from ..utils import CachedRepos + from ...utils import CachedRepos return CachedRepos.reasons(repo_id) def _load_report_settings(repo_id: str): # (forum_username, topic_msg_id), or (None, None) - from ..utils import CachedRepos + from ...utils import CachedRepos return CachedRepos.report_settings(repo_id) @@ -1031,7 +1031,7 @@ def _on_submit(v): card.setScaleY(0.92) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/RestartDialog.py b/packit/src/ui/dialogs/RestartDialog.py similarity index 99% rename from packit/src/ui/RestartDialog.py rename to packit/src/ui/dialogs/RestartDialog.py index ab67ea2..80ae691 100644 --- a/packit/src/ui/RestartDialog.py +++ b/packit/src/ui/dialogs/RestartDialog.py @@ -326,7 +326,7 @@ def _dismiss(on_end=None): def _do_restart(): try: - from ..deeplinks import Pkill + from ...deeplinks import Pkill Pkill.handle("tg://packit?pkill") except Exception as e: logx(f"restartDialog: pkill error: {e}", False) @@ -347,7 +347,7 @@ def _do_restart(): card.setScaleY(0.92) try: - from .ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/dialogs/__init__.py b/packit/src/ui/dialogs/__init__.py new file mode 100644 index 0000000..864b431 --- /dev/null +++ b/packit/src/ui/dialogs/__init__.py @@ -0,0 +1,4 @@ +# pyright: reportMissingImports=false +# SPDX-License-Identifier: GPL-3.0-or-later + +# Dialogs and bottom sheets that belong to no single screen. diff --git a/packit/src/ui/filesactivity/Fragment.py b/packit/src/ui/files/Fragment.py similarity index 100% rename from packit/src/ui/filesactivity/Fragment.py rename to packit/src/ui/files/Fragment.py diff --git a/packit/src/ui/filesactivity/InfoDialog.py b/packit/src/ui/files/InfoDialog.py similarity index 99% rename from packit/src/ui/filesactivity/InfoDialog.py rename to packit/src/ui/files/InfoDialog.py index 58a8da6..17fc927 100644 --- a/packit/src/ui/filesactivity/InfoDialog.py +++ b/packit/src/ui/files/InfoDialog.py @@ -366,7 +366,7 @@ def _dismiss(): card.setScaleY(0.92) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/filesactivity/OpenFileFragment.py b/packit/src/ui/files/OpenFileFragment.py similarity index 99% rename from packit/src/ui/filesactivity/OpenFileFragment.py rename to packit/src/ui/files/OpenFileFragment.py index d22f16d..1602439 100644 --- a/packit/src/ui/filesactivity/OpenFileFragment.py +++ b/packit/src/ui/files/OpenFileFragment.py @@ -173,7 +173,7 @@ def onTouch(self, v, event): sheet.setCustomView(container) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(container) except Exception: pass @@ -220,7 +220,7 @@ def onFragmentDestroy(self, *_): self._load_cancelled = True self._highlight_cancelled = True try: - from ...DexLoader import openFileCancel + from ...core.DexLoader import openFileCancel if self._viewer_view is not None: openFileCancel(self._viewer_view) except Exception as e: @@ -446,7 +446,7 @@ def _attach_viewer(self, tt, ts, te, ck, cv): if self._load_cancelled or self._viewer_container is None: return try: - from ...DexLoader import openFileCreate, openFileCancel + from ...core.DexLoader import openFileCreate, openFileCancel # drop a previous viewer (rebuild after save) if self._viewer_view is not None: try: diff --git a/packit/src/ui/filesactivity/Packlight.py b/packit/src/ui/files/Packlight.py similarity index 98% rename from packit/src/ui/filesactivity/Packlight.py rename to packit/src/ui/files/Packlight.py index 5343dba..4721f33 100644 --- a/packit/src/ui/filesactivity/Packlight.py +++ b/packit/src/ui/files/Packlight.py @@ -72,7 +72,7 @@ def _loadLib(): global _lib if _lib is not None: return _lib - from ...NativeLoader import loadPackLight + from ...core.NativeLoader import loadPackLight _lib = loadPackLight() if _lib is not None: _setupArgtypes(_lib) diff --git a/packit/src/standalonehooks/__init__.py b/packit/src/ui/files/__init__.py similarity index 100% rename from packit/src/standalonehooks/__init__.py rename to packit/src/ui/files/__init__.py diff --git a/packit/src/ui/iconslistactivity/Fragment.py b/packit/src/ui/icons/Fragment.py similarity index 99% rename from packit/src/ui/iconslistactivity/Fragment.py rename to packit/src/ui/icons/Fragment.py index 62a7d94..b323c87 100644 --- a/packit/src/ui/iconslistactivity/Fragment.py +++ b/packit/src/ui/icons/Fragment.py @@ -355,7 +355,7 @@ 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 + from ...core.DexLoader import catalogIconsChromeCreate live_search = bool(settings.get("live_search", True)) try: accent = Theme.getColor(Theme.key_featuredStickers_addButton) @@ -889,7 +889,7 @@ def __init__(self, install_ui, title, icons, show_loading_initial=False, repo_id def onFragmentCreate(self, *_): try: - from ..NoInternetBanner import NoInternetBanner as _NIB + from ..dialogs.NoInternetBanner import NoInternetBanner as _NIB self._no_internet_banner = _NIB(None) def _on_restore(): @@ -1320,7 +1320,7 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): main_layout.addView(scroll, LinearLayout.LayoutParams(-1, 0, 1.0)) self.search.addTextChangedListener(_IconsSearchTextWatcher(self, clear_btn, act)) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass @@ -1718,7 +1718,7 @@ def make_item(self, icon): try: _q = getattr(self, "last_search_query", None) if _q: - from ..ViewUtils import highlightQuery + from ..components.ViewUtils import highlightQuery _hl = highlightQuery( _display_name, str(_q), Theme.getColor(Theme.key_featuredStickers_addButton), @@ -1872,7 +1872,7 @@ def fetch_first(urls=all_urls, attempt=0): card.setFocusable(True) def _on_click(v, _icon=icon): try: - from ...Core import install_icon_pack + from ...core.Core import install_icon_pack install_icon_pack(_icon) except Exception as ex: logx(f"icons: card click error: {ex}", True) @@ -1903,7 +1903,7 @@ def _on_long_click(v, _icon=icon, _repo_id=self.repo_id): except Exception as _be: logx(f"icons: copy bulletin error: {_be}", True) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception: pass diff --git a/packit/src/ui/iconslistactivity/RepoBottomSheet.py b/packit/src/ui/icons/RepoBottomSheet.py similarity index 99% rename from packit/src/ui/iconslistactivity/RepoBottomSheet.py rename to packit/src/ui/icons/RepoBottomSheet.py index b403c82..04cc1cc 100644 --- a/packit/src/ui/iconslistactivity/RepoBottomSheet.py +++ b/packit/src/ui/icons/RepoBottomSheet.py @@ -239,7 +239,7 @@ def on_all_click(v): sheet.setCustomView(root) try: - from ...ui.ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/iconslistactivity/SortBottomSheet.py b/packit/src/ui/icons/SortBottomSheet.py similarity index 100% rename from packit/src/ui/iconslistactivity/SortBottomSheet.py rename to packit/src/ui/icons/SortBottomSheet.py diff --git a/packit/src/ui/achievementsactivity/__init__.py b/packit/src/ui/icons/__init__.py similarity index 100% rename from packit/src/ui/achievementsactivity/__init__.py rename to packit/src/ui/icons/__init__.py diff --git a/packit/src/ui/pluginactivity/Fragment.py b/packit/src/ui/plugin/Fragment.py similarity index 99% rename from packit/src/ui/pluginactivity/Fragment.py rename to packit/src/ui/plugin/Fragment.py index a9a8d6d..030536c 100644 --- a/packit/src/ui/pluginactivity/Fragment.py +++ b/packit/src/ui/plugin/Fragment.py @@ -469,8 +469,8 @@ def _on_click(v): def _show_plugin_menu(act, p, anchor_view, repo_id: str = ""): try: - from ..pluginlistactivity.helpers.PluginActions import share_plugin_file, view_plugin_code, download_plugin_file - from ..pluginlistactivity.helpers.ReportService import report_plugin + from ..plugins.helpers.PluginActions import share_plugin_file, view_plugin_code, download_plugin_file + from ..plugins.helpers.ReportService import report_plugin from org.telegram.ui.Components import ItemOptions from org.telegram.ui.ActionBar import ActionBarMenuSubItem from org.telegram.messenger import R as R_tg, AndroidUtilities @@ -504,7 +504,7 @@ def _copy_and_dismiss(link): AndroidUtilities.addToClipboard(link) options.dismiss() try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as _ae: logx(f"pluginProfile: achievement increment error: {_ae}", True) @@ -584,7 +584,7 @@ def onFragmentCreate(self, *_): def onFragmentDestroy(self, *_): logx(f"pluginProfile: onFragmentDestroy plugin={self.plugin.get('id')}", True) try: - from ...ui.achievementsactivity.service.AchivementsEngine import unregister_bulletin_container + from ..achievements.service.AchivementsEngine import unregister_bulletin_container unregister_bulletin_container(self.content_view) except Exception as e: logx(f"pluginProfile: unregister_bulletin_container error: {e}", False) @@ -688,7 +688,7 @@ def beforeCreateView(self): self.content_view.setBackgroundColor(bg_color) try: - from ...ui.achievementsactivity.service.AchivementsEngine import register_bulletin_container + from ..achievements.service.AchivementsEngine import register_bulletin_container register_bulletin_container(self.content_view) except Exception as e: logx(f"pluginProfile: register_bulletin_container error: {e}", False) @@ -1050,7 +1050,7 @@ def _set_loading(_btn, _label, _btn_text_color, _act, isLoading): def _do_install(_p, _install_ui, _all, _btn, _label, _btn_text_color, _act, on_finish_override=None, succ_download=None): - from ...Core import install_plugin + from ...core.Core import install_plugin if not on_finish_override: _set_loading(_btn, _label, _btn_text_color, _act, True) @@ -1239,7 +1239,7 @@ def onInstallClickFab(v, _p=p, _install_ui=_install_ui_ref, _all=_all_plugins_re # rebind _do_install so its internal loader uses fab dimensions def _do_install(_p, _install_ui, _all, _btn, _label, _btn_text_color, _act, on_finish_override=None, succ_download=None): - from ...Core import install_plugin + from ...core.Core import install_plugin if not on_finish_override: _set_loading_fab(_btn, _label, _btn_text_color, _act, True) @@ -3262,7 +3262,7 @@ def onTouch(self, v, event): logx(f"pluginProfile: beforeCreateView done, content_view={self.content_view}", True) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass diff --git a/packit/src/ui/pluginactivity/VersionPicker.py b/packit/src/ui/plugin/VersionPicker.py similarity index 99% rename from packit/src/ui/pluginactivity/VersionPicker.py rename to packit/src/ui/plugin/VersionPicker.py index 4286fa6..cd7d4bc 100644 --- a/packit/src/ui/pluginactivity/VersionPicker.py +++ b/packit/src/ui/plugin/VersionPicker.py @@ -51,7 +51,7 @@ def _build_version_entries(plugin): # returns list sorted newest first def _ver_key(v): try: - from ..pluginlistactivity.helpers.Utils import _parse_version + from ..plugins.helpers.Utils import _parse_version return _parse_version(v) except Exception: return [] @@ -842,7 +842,7 @@ def _unwrap(): card.setScaleX(0.92) card.setScaleY(0.92) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/achievementsactivity/service/__init__.py b/packit/src/ui/plugin/__init__.py similarity index 100% rename from packit/src/ui/achievementsactivity/service/__init__.py rename to packit/src/ui/plugin/__init__.py diff --git a/packit/src/ui/pluginlistactivity/Card.py b/packit/src/ui/plugins/Card.py similarity index 95% rename from packit/src/ui/pluginlistactivity/Card.py rename to packit/src/ui/plugins/Card.py index 671fc24..668d43f 100644 --- a/packit/src/ui/pluginlistactivity/Card.py +++ b/packit/src/ui/plugins/Card.py @@ -44,7 +44,7 @@ from .helpers.PluginActions import copy_plugin_link, share_plugin_file, view_plugin_code, report_plugin, download_plugin_file, translate_plugin from .filter.TagLayoutListener import _TagsOverflowListener from .helpers.Utils import _check_app_version -from ..ViewUtils import highlightQuery as _highlight_query +from ..components.ViewUtils import highlightQuery as _highlight_query def make_plugin_card(self, p): @@ -135,7 +135,7 @@ def create_icon_pill(icon_name, handler): def onIconClick(v, plugin=p): try: - from ..pluginactivity.Fragment import show_plugin_profile + from ..plugin.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -317,7 +317,7 @@ def onTagClick(v, url=tag_url): def onPlusClick(v, plugin=p): try: - from ..pluginactivity.Fragment import show_plugin_profile + from ..plugin.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or ""), scroll_to_tags=True) @@ -479,7 +479,7 @@ def _position_and_show(): pass try: - from ..pluginactivity.Fragment import show_plugin_profile + from ..plugin.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -487,7 +487,7 @@ def _position_and_show(): def onCardClick(v, plugin=p, row_ref=row, hint_ref=current_hint_ref, available=is_available): if not self._s_show_view_button: try: - from ..pluginactivity.Fragment import show_plugin_profile + from ..plugin.Fragment import show_plugin_profile show_plugin_profile(plugin, self.install_ui, self.plugins, repo_id=self.repo_id or str(plugin.get("_repo_id") or "")) except Exception as e: pass @@ -556,7 +556,7 @@ def do_install(): pass return try: - from ...Core import install_plugin + from ...core.Core import install_plugin install_plugin( p, install_ui=self.install_ui, @@ -569,7 +569,7 @@ def do_install(): def do_download_relocated(): download_plugin_file(p) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception as e: pass @@ -577,7 +577,7 @@ def do_download_relocated(): def do_copy_relocated(): copy_plugin_link(p, self.repo_id or self.title, copyLinkSoundPath) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as e: pass @@ -585,7 +585,7 @@ def do_copy_relocated(): def do_share_relocated(): share_plugin_file(p, str(display_name), act_for_share) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception as e: pass @@ -593,7 +593,7 @@ def do_share_relocated(): def do_code_relocated(): view_plugin_code(p, act) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception as e: pass @@ -604,7 +604,7 @@ def do_translate_relocated(): def do_report_relocated(): report_plugin(p, act, repo_id=self.repo_id or str(p.get("_repo_id") or "")) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Reporting") except Exception as e: pass @@ -628,10 +628,10 @@ def do_report_relocated(): def show_plugin_actions_menu(anchor_view): try: - from ..ContextMenu import show_plugin_context_menu + from ..components.ContextMenu import show_plugin_context_menu def do_install(): - from ...Core import install_plugin + from ...core.Core import install_plugin install_plugin( p, install_ui=self.install_ui, @@ -642,7 +642,7 @@ def do_install(): def do_download(): download_plugin_file(p) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception: pass @@ -650,7 +650,7 @@ def do_download(): def do_copy(): copy_plugin_link(p, self.repo_id or self.title, copyLinkSoundPath) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception: pass @@ -658,7 +658,7 @@ def do_copy(): def do_share(): share_plugin_file(p, str(display_name), act_for_share) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception: pass @@ -666,7 +666,7 @@ def do_share(): def do_code(): view_plugin_code(p, act) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception: pass @@ -677,7 +677,7 @@ def do_translate(): def do_report(): report_plugin(p, act, repo_id=self.repo_id or str(p.get("_repo_id") or "")) try: - from ...ui.achievementsactivity.service.AchivementsEngine import increment_category + from ..achievements.service.AchivementsEngine import increment_category increment_category("Reporting") except Exception: pass diff --git a/packit/src/ui/pluginlistactivity/Fragment.py b/packit/src/ui/plugins/Fragment.py similarity index 99% rename from packit/src/ui/pluginlistactivity/Fragment.py rename to packit/src/ui/plugins/Fragment.py index e738363..7e67e26 100644 --- a/packit/src/ui/pluginlistactivity/Fragment.py +++ b/packit/src/ui/plugins/Fragment.py @@ -105,7 +105,7 @@ from .filter import FilterEngine as tag_mod from .helpers.PluginActions import copy_plugin_link, share_plugin_file, view_plugin_code, report_plugin, download_plugin_file, translate_plugin from ...utils.Media import playSound -from ...Core import install_plugin +from ...core.Core import install_plugin from . import Card as _card @@ -424,7 +424,7 @@ def __init__(self, install_ui, title, plugins, show_loading_initial=False, repo_ def onFragmentCreate(self, *_): try: - from ..NoInternetBanner import NoInternetBanner as _NIB + from ..dialogs.NoInternetBanner import NoInternetBanner as _NIB self._no_internet_banner = _NIB(None) def _on_restore(): @@ -460,7 +460,7 @@ def onFragmentDestroy(self, *_): logx(f"InstallUI: NoInternetBanner unregister error: {e}", False) try: if hasattr(self, 'content_view') and self.content_view is not None: - from ...ui.achievementsactivity.service.AchivementsEngine import unregister_bulletin_container + from ..achievements.service.AchivementsEngine import unregister_bulletin_container unregister_bulletin_container(self.content_view) parent = self.content_view.getParent() if parent is not None: @@ -691,7 +691,7 @@ def build_list_with_sort(self, sort_type: str, q=None): filtered = [] elif getattr(self, "selected_saved", None) is not None and self.selected_saved != {"saved", "unsaved"}: try: - from ..pluginactivity.Fragment import _read_saved_plugins + from ..plugin.Fragment import _read_saved_plugins saved_ids = set(_read_saved_plugins()) show_saved = "saved" in self.selected_saved show_unsaved = "unsaved" in self.selected_saved diff --git a/packit/src/ui/pluginlistactivity/ListView.py b/packit/src/ui/plugins/ListView.py similarity index 99% rename from packit/src/ui/pluginlistactivity/ListView.py rename to packit/src/ui/plugins/ListView.py index 3229491..5dcbb21 100644 --- a/packit/src/ui/pluginlistactivity/ListView.py +++ b/packit/src/ui/plugins/ListView.py @@ -274,7 +274,7 @@ 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 ...core.DexLoader import catalogChromeCreate from elyx import settings as _s live_search = bool(_s.get("live_search", True)) try: @@ -634,7 +634,7 @@ def build_list_view(self) -> View: self.content_view = FrameLayout(act) self.content_view.setBackgroundColor(self.main_bg_color) - from ...ui.achievementsactivity.service.AchivementsEngine import register_bulletin_container + from ..achievements.service.AchivementsEngine import register_bulletin_container register_bulletin_container(self.content_view) chrome = None try: @@ -936,7 +936,7 @@ def onScrollChange(self, v, scrollX, scrollY, oldScrollX, oldScrollY): self.search.addTextChangedListener(_SearchTextWatcherWithClear(self, clear_btn)) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(self.content_view) except Exception: pass diff --git a/packit/src/ui/filesactivity/__init__.py b/packit/src/ui/plugins/__init__.py similarity index 100% rename from packit/src/ui/filesactivity/__init__.py rename to packit/src/ui/plugins/__init__.py diff --git a/packit/src/ui/pluginlistactivity/filter/FilterDrawer.py b/packit/src/ui/plugins/filter/FilterDrawer.py similarity index 100% rename from packit/src/ui/pluginlistactivity/filter/FilterDrawer.py rename to packit/src/ui/plugins/filter/FilterDrawer.py diff --git a/packit/src/ui/pluginlistactivity/filter/FilterEngine.py b/packit/src/ui/plugins/filter/FilterEngine.py similarity index 100% rename from packit/src/ui/pluginlistactivity/filter/FilterEngine.py rename to packit/src/ui/plugins/filter/FilterEngine.py diff --git a/packit/src/ui/pluginlistactivity/filter/TagLayoutListener.py b/packit/src/ui/plugins/filter/TagLayoutListener.py similarity index 100% rename from packit/src/ui/pluginlistactivity/filter/TagLayoutListener.py rename to packit/src/ui/plugins/filter/TagLayoutListener.py diff --git a/packit/src/ui/iconslistactivity/__init__.py b/packit/src/ui/plugins/filter/__init__.py similarity index 100% rename from packit/src/ui/iconslistactivity/__init__.py rename to packit/src/ui/plugins/filter/__init__.py diff --git a/packit/src/ui/pluginlistactivity/helpers/PluginActions.py b/packit/src/ui/plugins/helpers/PluginActions.py similarity index 96% rename from packit/src/ui/pluginlistactivity/helpers/PluginActions.py rename to packit/src/ui/plugins/helpers/PluginActions.py index 15b65a7..4fd4dae 100644 --- a/packit/src/ui/pluginlistactivity/helpers/PluginActions.py +++ b/packit/src/ui/plugins/helpers/PluginActions.py @@ -50,7 +50,7 @@ def copy_plugin_link(plugin_info: dict, repo_title: str, sound_path: str = None) plugin_name = plugin_info.get("name") or plugin_info.get("id") or "Unknown" _pbf(container, resource_provider).createSimpleBulletin(R_tg.raw.voip_invite, strings("plugin_link_copied", plugin_name)).show() try: - from ....ui.achievementsactivity.service.AchivementsEngine import increment_category + from ...achievements.service.AchivementsEngine import increment_category increment_category("Copying links") except Exception as e: logx(f"copy_plugin_link: achievements increment error: {e}", False) @@ -63,7 +63,7 @@ def share_plugin_file(plugin_info: dict, display_name: str, activity): from ....utils.Share import share_plugin_file as _share_plugin_file _share_plugin_file(plugin_info, display_name, activity) try: - from ....ui.achievementsactivity.service.AchivementsEngine import increment_category + from ...achievements.service.AchivementsEngine import increment_category increment_category("Sharing") except Exception as e: logx(f"share_plugin_file: achievements increment error: {e}", False) @@ -183,7 +183,7 @@ def _show_download_ok(path): strings("download_saved", folder=folder) ).show() try: - from ....ui.achievementsactivity.service.AchivementsEngine import increment_category + from ...achievements.service.AchivementsEngine import increment_category increment_category("Downloading") except Exception as e: logx(f"download: achievements increment error: {e}", False) @@ -235,7 +235,7 @@ def view_plugin_code(plugin_info: dict, activity): _pbf(activity.getWindow().getDecorView(), None).createErrorBulletin(strings["failed_to_open_url"]).show() return try: - from ....ui.achievementsactivity.service.AchivementsEngine import increment_category + from ...achievements.service.AchivementsEngine import increment_category increment_category("Viewing code") except Exception as e: logx(f"view_plugin_code: achievements increment error: {e}", False) diff --git a/packit/src/ui/pluginlistactivity/helpers/ReportService.py b/packit/src/ui/plugins/helpers/ReportService.py similarity index 93% rename from packit/src/ui/pluginlistactivity/helpers/ReportService.py rename to packit/src/ui/plugins/helpers/ReportService.py index b46aa41..f1335db 100644 --- a/packit/src/ui/pluginlistactivity/helpers/ReportService.py +++ b/packit/src/ui/plugins/helpers/ReportService.py @@ -22,7 +22,7 @@ def report_plugin(plugin_info: dict, activity, repo_id: str = ""): rid = repo_id or str(plugin_info.get("repo_id") or plugin_info.get("_repo_id") or "") pid = str(plugin_info.get("id") or "") - from ....ui.ReportDialog import _load_report_settings + from ...dialogs.ReportDialog import _load_report_settings from elyx import strings forum_username, topic_msg_id = _load_report_settings(rid) @@ -43,7 +43,7 @@ def _show_missing(field): run_on_ui_thread(lambda: _show_missing("topic_msg_id")) return - from ....ui.ReportDialog import show_report_dialog + from ...dialogs.ReportDialog import show_report_dialog _name = name _rid = rid _pid = pid diff --git a/packit/src/ui/pluginlistactivity/helpers/UiHelpers.py b/packit/src/ui/plugins/helpers/UiHelpers.py similarity index 100% rename from packit/src/ui/pluginlistactivity/helpers/UiHelpers.py rename to packit/src/ui/plugins/helpers/UiHelpers.py diff --git a/packit/src/ui/pluginlistactivity/helpers/Utils.py b/packit/src/ui/plugins/helpers/Utils.py similarity index 100% rename from packit/src/ui/pluginlistactivity/helpers/Utils.py rename to packit/src/ui/plugins/helpers/Utils.py diff --git a/packit/src/ui/pluginactivity/__init__.py b/packit/src/ui/plugins/helpers/__init__.py similarity index 100% rename from packit/src/ui/pluginactivity/__init__.py rename to packit/src/ui/plugins/helpers/__init__.py diff --git a/packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py b/packit/src/ui/plugins/sheets/AISearchSheet.py similarity index 99% rename from packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py rename to packit/src/ui/plugins/sheets/AISearchSheet.py index b70d5c2..1047c6b 100644 --- a/packit/src/ui/pluginlistactivity/sheets/AISearchSheet.py +++ b/packit/src/ui/plugins/sheets/AISearchSheet.py @@ -92,7 +92,7 @@ def _get_device_id() -> str: def _load_gemini_key() -> "str | None": # returns full key string or None try: - from ....NativeLoader import loadPackitKey + from ....core.NativeLoader import loadPackitKey from ....utils.Paths import getKeysDir except Exception as e: logx(f"AISearchSheet: _load_gemini_key import failed: {e}", False) @@ -824,7 +824,7 @@ def _on_error(): sheet.setCustomView(root) try: - from ...ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception as e: logx(f"AISearchSheet: applyFontToTree failed: {e}", False) diff --git a/packit/src/ui/pluginlistactivity/sheets/DepsSheet.py b/packit/src/ui/plugins/sheets/DepsSheet.py similarity index 99% rename from packit/src/ui/pluginlistactivity/sheets/DepsSheet.py rename to packit/src/ui/plugins/sheets/DepsSheet.py index dac8122..8cea35b 100644 --- a/packit/src/ui/pluginlistactivity/sheets/DepsSheet.py +++ b/packit/src/ui/plugins/sheets/DepsSheet.py @@ -257,7 +257,7 @@ def on_cancel_click(v): sheet.setCustomView(root) try: - from ...ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass @@ -483,7 +483,7 @@ def _do_refresh(): logx(f"depsSheet: _do_refresh error for '{dep_id}': {e}", False) def on_install(v): - from ....Core import install_plugin + from ....core.Core import install_plugin if observer_registered[0] is None: try: from java import dynamic_proxy diff --git a/packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py b/packit/src/ui/plugins/sheets/RepoBottomSheet.py similarity index 99% rename from packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py rename to packit/src/ui/plugins/sheets/RepoBottomSheet.py index c4f7c3b..af29b09 100644 --- a/packit/src/ui/pluginlistactivity/sheets/RepoBottomSheet.py +++ b/packit/src/ui/plugins/sheets/RepoBottomSheet.py @@ -247,7 +247,7 @@ def on_close(v): root.addView(close_btn, LayoutHelper.createLinear(-1, -2, 0, 8, 0, 0)) sheet.setCustomView(root) try: - from ...ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass diff --git a/packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py b/packit/src/ui/plugins/sheets/SortBottomSheet.py similarity index 99% rename from packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py rename to packit/src/ui/plugins/sheets/SortBottomSheet.py index 064d9f2..e8f9887 100644 --- a/packit/src/ui/pluginlistactivity/sheets/SortBottomSheet.py +++ b/packit/src/ui/plugins/sheets/SortBottomSheet.py @@ -234,7 +234,7 @@ def on_close_sort(v): sort_sheet.setCustomView(sort_root) try: - from ...ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(sort_root) except Exception: pass diff --git a/packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py b/packit/src/ui/plugins/sheets/TgChannelSheet.py similarity index 97% rename from packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py rename to packit/src/ui/plugins/sheets/TgChannelSheet.py index c13697c..3badeb1 100644 --- a/packit/src/ui/pluginlistactivity/sheets/TgChannelSheet.py +++ b/packit/src/ui/plugins/sheets/TgChannelSheet.py @@ -86,7 +86,7 @@ def onClick(self, v): sheet.dismiss() LocalConfig.set("showTgc", True) try: - from ....ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ...achievements.service.AchivementsEngine import unlock_secret unlock_secret("subscriber") except Exception as e: logx(f"tgChannelSheet: achievement unlock error: {e}", False) @@ -119,7 +119,7 @@ def onClick(self, v): scroll.addView(frame) sheet.setCustomView(scroll) try: - from ...ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(scroll) except Exception: pass diff --git a/packit/src/ui/pluginlistactivity/__init__.py b/packit/src/ui/plugins/sheets/__init__.py similarity index 100% rename from packit/src/ui/pluginlistactivity/__init__.py rename to packit/src/ui/plugins/sheets/__init__.py diff --git a/packit/src/ui/reposactivity/Actions.py b/packit/src/ui/repos/Actions.py similarity index 99% rename from packit/src/ui/reposactivity/Actions.py rename to packit/src/ui/repos/Actions.py index 70bda0d..79525db 100644 --- a/packit/src/ui/reposactivity/Actions.py +++ b/packit/src/ui/repos/Actions.py @@ -30,7 +30,7 @@ import android_utils as _au; _au.log(f"repos actions: import R failed: {e}") from ...utils.Bulletins import factory as _pbf -from ..ContextMenu import show_plugin_context_menu +from ..components.ContextMenu import show_plugin_context_menu from . import notify_repos_changed diff --git a/packit/src/ui/reposactivity/AddSheet.py b/packit/src/ui/repos/AddSheet.py similarity index 99% rename from packit/src/ui/reposactivity/AddSheet.py rename to packit/src/ui/repos/AddSheet.py index 8cada78..d187039 100644 --- a/packit/src/ui/reposactivity/AddSheet.py +++ b/packit/src/ui/repos/AddSheet.py @@ -32,12 +32,12 @@ except Exception as e: import android_utils as _au; _au.log(f"repos dialog: import telegram classes failed: {e}") -from ...settingsactivity.service.AddKeyDialog import ( +from ..settings.service.AddKeyDialog import ( _register_back_cb, _unregister_back_cb, _animate_in, _animate_out, _attach_keyboard_listener, _detach_keyboard_listener, ) from ...utils.Bulletins import factory as _pbf -from ...RepositoryManager import REPO_NAME_MAX +from ...core.RepositoryManager import REPO_NAME_MAX # addRepositoryWithUrl answers in lowercase english; the user gets their own # language and, where possible, a hint at what to do about it diff --git a/packit/src/ui/reposactivity/Card.py b/packit/src/ui/repos/Card.py similarity index 99% rename from packit/src/ui/reposactivity/Card.py rename to packit/src/ui/repos/Card.py index e8b14e2..efc121f 100644 --- a/packit/src/ui/reposactivity/Card.py +++ b/packit/src/ui/repos/Card.py @@ -36,7 +36,7 @@ import android_utils as _au; _au.log(f"repos card: import elyx strings failed: {e}") from . import RepoIcon -from ..pluginlistactivity.helpers.UiHelpers import ( +from ..plugins.helpers.UiHelpers import ( apply_press_scale_on_target, resolve_icon, ) @@ -231,7 +231,7 @@ def _icon_lp(): def _fill_sub(r, i): text = str(i.get("maintainer") or "").strip() or _host_of(r.get("url")) # Set up exactly the way the plugin catalogue sets up its author line - # (pluginlistactivity/Card.py): fullyFormatText, grey body, + # (ui/plugins/Card.py): fullyFormatText, grey body, # windowBackgroundWhiteBlueText for the mention, LinkMovementMethod. # Left to itself the formatter paints mentions in its own colour, which # is why these came out a teal that appears nowhere else on the screen. diff --git a/packit/src/ui/reposactivity/Fragment.py b/packit/src/ui/repos/Fragment.py similarity index 98% rename from packit/src/ui/reposactivity/Fragment.py rename to packit/src/ui/repos/Fragment.py index 8ea45e6..49e4677 100644 --- a/packit/src/ui/reposactivity/Fragment.py +++ b/packit/src/ui/repos/Fragment.py @@ -44,7 +44,7 @@ from . import register, unregister from .Card import make_repo_card -from ..ViewUtils import applyFontToTree +from ..components.ViewUtils import applyFontToTree from ...utils import CachedRepos @@ -302,7 +302,7 @@ def _render(self, act, repos, infos): def _summary_text(self, count: int) -> str: try: - from ..pluginlistactivity.helpers.Utils import _format_plural + from ..plugins.helpers.Utils import _format_plural return str(_format_plural(count, strings.repo_one, strings.repo_few, strings.repo_many, strings["plural_type"])) except Exception: @@ -367,7 +367,7 @@ def _build_summary_row(self, act): # on the card surface, icon buttons of the same shape on the right. A # loose grey caption over a floating circle did not read as a control # strip at all — this is the same component the rest of the plugin uses. - from ..pluginlistactivity.helpers.UiHelpers import get_theme_colors, apply_press_scale_on_target + from ..plugins.helpers.UiHelpers import get_theme_colors, apply_press_scale_on_target colors = get_theme_colors() card_bg = colors.get("card_bg_color") card_pressed = colors.get("card_pressed_color") @@ -484,7 +484,7 @@ def _add(v): btn.setOnClickListener(OnClickListener(_add)) try: - from ..pluginlistactivity.helpers.UiHelpers import apply_press_scale + from ..plugins.helpers.UiHelpers import apply_press_scale apply_press_scale(btn) except Exception: pass diff --git a/packit/src/ui/reposactivity/RepoIcon.py b/packit/src/ui/repos/RepoIcon.py similarity index 100% rename from packit/src/ui/reposactivity/RepoIcon.py rename to packit/src/ui/repos/RepoIcon.py diff --git a/packit/src/ui/reposactivity/RepoSheet.py b/packit/src/ui/repos/RepoSheet.py similarity index 98% rename from packit/src/ui/reposactivity/RepoSheet.py rename to packit/src/ui/repos/RepoSheet.py index e9c9e95..b8bf2f7 100644 --- a/packit/src/ui/reposactivity/RepoSheet.py +++ b/packit/src/ui/repos/RepoSheet.py @@ -30,8 +30,8 @@ import android_utils as _au; _au.log(f"repoSheet: import elyx strings failed: {e}") from . import RepoIcon -from ..ViewUtils import applyFontToTree -from ..pluginlistactivity.helpers.UiHelpers import setup_bottom_sheet, create_rounded_bg +from ..components.ViewUtils import applyFontToTree +from ..plugins.helpers.UiHelpers import setup_bottom_sheet, create_rounded_bg def _c(color: int) -> int: diff --git a/packit/src/ui/reposactivity/__init__.py b/packit/src/ui/repos/__init__.py similarity index 100% rename from packit/src/ui/reposactivity/__init__.py rename to packit/src/ui/repos/__init__.py diff --git a/packit/src/settingsactivity/DebugItems.py b/packit/src/ui/settings/DebugItems.py similarity index 97% rename from packit/src/settingsactivity/DebugItems.py rename to packit/src/ui/settings/DebugItems.py index 712ea2e..891fe2d 100644 --- a/packit/src/settingsactivity/DebugItems.py +++ b/packit/src/ui/settings/DebugItems.py @@ -22,7 +22,7 @@ def _test_native_error(): _ = 123 / 0 except Exception as e: logx(f"debugItems: test native error triggered: {e}", False) - from ..NativeLoader import showNativeErrorSheet + from ...core.NativeLoader import showNativeErrorSheet showNativeErrorSheet("libpackitdb.so", str(e)) @@ -31,7 +31,7 @@ def _migrate_achievements(): import os import ctypes import zlib - from ..ui.achievementsactivity.service.AchivementsEngine import ( + from ..achievements.service.AchivementsEngine import ( _get_current_account_id, _get_configs_dir, _save_account, _db_to_dict, _lib, _BUF_SIZE ) @@ -263,7 +263,7 @@ def _dump_class_info(class_name: str, methods: bool = True, fields: bool = True) def _check_build_info(): try: - from ..utils.BuildInfo import ( + from ...utils.BuildInfo import ( getBuildClientName, getBuildClientPkg, getCurrClientName, getCurrClientPkg, getBuildStaticVersion, getClientVersion @@ -281,7 +281,7 @@ def _check_build_info(): def _migrate_installdate_to_b64(): try: import os, json, base64 - from ..utils.LocalConfig import _get_install_date_path + from ...utils.LocalConfig import _get_install_date_path path = _get_install_date_path() if not os.path.exists(path): _show_bulletin("InstallDate not found") @@ -327,11 +327,11 @@ def show_debug_menu(): return def _trigger_startup_sheet(): - from ..ui.pluginsupdates.StartupSheet import check_and_show_startup_updates + from ..updates.StartupSheet import check_and_show_startup_updates check_and_show_startup_updates() def _update_repos_cache(): - from ..RepositoryManager import RepositoryManager + from ...core.RepositoryManager import RepositoryManager RepositoryManager().updateAllCaches( on_complete=lambda: _show_bulletin("Repos cache updated") ) diff --git a/packit/src/settingsactivity/Deeplinks.py b/packit/src/ui/settings/Deeplinks.py similarity index 96% rename from packit/src/settingsactivity/Deeplinks.py rename to packit/src/ui/settings/Deeplinks.py index 8d5f5d7..e0a6f28 100644 --- a/packit/src/settingsactivity/Deeplinks.py +++ b/packit/src/ui/settings/Deeplinks.py @@ -7,7 +7,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from client_utils import get_last_fragment from android.content import Intent from android.net import Uri @@ -15,8 +15,8 @@ from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from ..ui.DeeplinkBottomSheets import show_deeplink_sheet + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from ..dialogs.DeeplinkBottomSheets import show_deeplink_sheet class DeeplinksSettings: diff --git a/packit/src/settingsactivity/Docs.py b/packit/src/ui/settings/Docs.py similarity index 92% rename from packit/src/settingsactivity/Docs.py rename to packit/src/ui/settings/Docs.py index 9641469..d0b9432 100644 --- a/packit/src/settingsactivity/Docs.py +++ b/packit/src/ui/settings/Docs.py @@ -11,52 +11,52 @@ from android.widget import LinearLayout, TextView, ImageView, FrameLayout except Exception as e: import android_utils as _au; _au.log(f"import android.widget failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.graphics.drawable import GradientDrawable except Exception as e: import android_utils as _au; _au.log(f"import android.graphics.drawable import GradientDrawable failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.view import Gravity except Exception as e: import android_utils as _au; _au.log(f"import android.view import Gravity failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from android.util import TypedValue except Exception as e: import android_utils as _au; _au.log(f"import android.util import TypedValue failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from androidx.core.content import ContextCompat except Exception as e: import android_utils as _au; _au.log(f"import androidx.core.content import ContextCompat failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger.browser import Browser except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger.browser import Browser failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _makeBanner(context, icon_name, title_text, subtitle_text): @@ -163,7 +163,7 @@ def _openEnlightenment(self, view, *_): BulletinHelper.show_info(strings.enlighten_11, fragment) settings.set_setting("enlighten_clicks", 0) try: - from ..ui.achievementsactivity.service.AchivementsEngine import unlock_secret + from ..achievements.service.AchivementsEngine import unlock_secret logx(f"docs._openEnlightenment: calling unlock_secret enlightened", True) unlock_secret("enlightened") logx(f"docs._openEnlightenment: unlock_secret done", True) diff --git a/packit/src/settingsactivity/Profile.py b/packit/src/ui/settings/Profile.py similarity index 98% rename from packit/src/settingsactivity/Profile.py rename to packit/src/ui/settings/Profile.py index ed2eeb9..eaeb32e 100644 --- a/packit/src/settingsactivity/Profile.py +++ b/packit/src/ui/settings/Profile.py @@ -2,20 +2,20 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.Bulletins import factory as _pbf +from ...utils.Bulletins import factory as _pbf from ui.settings import Header, Text, Divider, Custom from ui.bulletin import BulletinHelper from client_utils import get_last_fragment -from ..ui.achievementsactivity.Fragment import show_achievements +from ..achievements.Fragment import show_achievements import threading import time try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() -from ..ui.achievementsactivity.service.AchivementsEngine import get_all_with_progress, get_stats + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() +from ..achievements.service.AchivementsEngine import get_all_with_progress, get_stats def _get_greeting(first_name: str) -> str: @@ -180,7 +180,7 @@ def _show_achievements(self, view): def _do_export(self, include_local_config: bool, include_achievements: bool, include_saved_plugins: bool): try: - from ..chatactivity.export.bin.Writer import build_binary, _rand_suffix + from ...integrations.chat.export.bin.Writer import build_binary, _rand_suffix from android_utils import run_on_ui_thread from java import jclass, dynamic_proxy from java.io import File, FileOutputStream @@ -625,7 +625,7 @@ def onShare(): pass try: - from ..ui.ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass @@ -657,7 +657,7 @@ def _make_stats_card(self, context): level, xp_a, xp_b = s["level_info"] try: - from ..utils.LocalConfig import days_since_install + from ...utils.LocalConfig import days_since_install days = days_since_install() except Exception: days = 0 diff --git a/packit/src/settingsactivity/Settings.py b/packit/src/ui/settings/Settings.py similarity index 99% rename from packit/src/settingsactivity/Settings.py rename to packit/src/ui/settings/Settings.py index 61b9e93..dee6bca 100644 --- a/packit/src/settingsactivity/Settings.py +++ b/packit/src/ui/settings/Settings.py @@ -10,22 +10,22 @@ from org.telegram.messenger import ApplicationLoader, AndroidUtilities, R except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader, AndroidUtilities failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.ActionBar import Theme, BottomSheet except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.ActionBar import Theme failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.ui.Components import LayoutHelper except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.ui.Components import LayoutHelper failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from elyx import strings, settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings, settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() from android.widget import LinearLayout, TextView, FrameLayout from android.view import Gravity from android.net import Uri @@ -42,8 +42,8 @@ from typing import List, Any, Callable from dataclasses import dataclass, field -from ..ui.FontPickerBottomSheet import showFontPicker -from ..ui.FontManager import getSelectedFilename +from ..dialogs.FontPickerBottomSheet import showFontPicker +from ..components.FontManager import getSelectedFilename from extera_utils.classes import Base, java_subclass, joverride def _reload_plugin_settings(): @@ -1561,13 +1561,13 @@ def _open_pill_stack_settings(self, view): def _open_files_browser(self): try: - from ..ui.filesactivity.Fragment import show_files_browser + from ..files.Fragment import show_files_browser show_files_browser(plugin=self.plugin) except Exception as e: logx(f"OtherSettings: _open_files_browser error: {e}", False) def _getCacheDir(self) -> str: - from ..utils.Paths import getCacheRoot + from ...utils.Paths import getCacheRoot return getCacheRoot() def _killProcess(self, *_): @@ -1646,7 +1646,7 @@ def _onClearPluginCacheClick(self, view, update_callback=None): def onConfirm(b, w): b.dismiss() try: - from ..utils.Paths import getCacheRoot + from ...utils.Paths import getCacheRoot plugin_cache_dir = getCacheRoot() + "/.cache/plugins" if os.path.exists(plugin_cache_dir): shutil.rmtree(plugin_cache_dir) @@ -1797,7 +1797,7 @@ def _open_debug_page(self): def _onClearIgnoreListClick(self, view): try: - from ..ui.pluginsupdates.ClearIgnoreListDialog import show_clear_ignore_list_dialog + from ..updates.ClearIgnoreListDialog import show_clear_ignore_list_dialog frag = get_last_fragment() act = frag.getParentActivity() if frag else None if not act: @@ -1943,7 +1943,7 @@ def build(self): red=True )) - from ..utils.Paths import getCacheRoot + from ...utils.Paths import getCacheRoot pluginCacheDir = getCacheRoot() + "/.cache/plugins" pluginCacheCard, pluginCacheUpdateFunc = _buildCacheCard(ctx, pluginCacheDir, lambda v: self._onClearPluginCacheClick(v, pluginCacheUpdateFunc), title=strings.clear_plugin_cache) if pluginCacheCard is not None: diff --git a/packit/src/settingsactivity/Utilities.py b/packit/src/ui/settings/Utilities.py similarity index 96% rename from packit/src/settingsactivity/Utilities.py rename to packit/src/ui/settings/Utilities.py index 48adafc..a14a06e 100644 --- a/packit/src/settingsactivity/Utilities.py +++ b/packit/src/ui/settings/Utilities.py @@ -14,7 +14,7 @@ from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from ...utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() def _calcPluginsDirSize(): @@ -111,7 +111,7 @@ def dismiss_spinner(): def load_and_show(): try: - from ..ui.ExportBottomSheet import loadPlugins, show as showExportSheet + from ..dialogs.ExportBottomSheet import loadPlugins, show as showExportSheet plugins = loadPlugins() run_on_ui_thread(lambda: (dismiss_spinner(), showExportSheet(plugins, self._on_export))) except Exception as e: diff --git a/packit/src/ui/pluginlistactivity/filter/__init__.py b/packit/src/ui/settings/__init__.py similarity index 100% rename from packit/src/ui/pluginlistactivity/filter/__init__.py rename to packit/src/ui/settings/__init__.py diff --git a/packit/src/settingsactivity/service/AddKeyDialog.py b/packit/src/ui/settings/service/AddKeyDialog.py similarity index 100% rename from packit/src/settingsactivity/service/AddKeyDialog.py rename to packit/src/ui/settings/service/AddKeyDialog.py diff --git a/packit/src/settingsactivity/service/FastExpandableHook.py b/packit/src/ui/settings/service/FastExpandableHook.py similarity index 100% rename from packit/src/settingsactivity/service/FastExpandableHook.py rename to packit/src/ui/settings/service/FastExpandableHook.py diff --git a/packit/src/settingsactivity/service/PluginsExport.py b/packit/src/ui/settings/service/PluginsExport.py similarity index 98% rename from packit/src/settingsactivity/service/PluginsExport.py rename to packit/src/ui/settings/service/PluginsExport.py index 521e59b..7c9ddb0 100644 --- a/packit/src/settingsactivity/service/PluginsExport.py +++ b/packit/src/ui/settings/service/PluginsExport.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.Bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf import os import json import zipfile @@ -36,7 +36,7 @@ def _resolvePluginsDir() -> str | None: def _resolveLocalConfigPath() -> str | None: try: - from ...utils.Paths import getConfigsDir + from ....utils.Paths import getConfigsDir return os.path.join(getConfigsDir(), "localConfig.json") except Exception as e: logx(f"pluginsExport._resolveLocalConfigPath: {e}", False) @@ -79,7 +79,7 @@ def _readPluginMeta(filepath: str) -> dict: def _buildConfigScl(selected_files: list, export_settings: bool, export_locally: bool, plugins_dir: str, local_cfg_path: str | None) -> str: - from ...scl.Scl import Doc + from ....scl.Scl import Doc doc = Doc.new() doc.set("type", "local" if export_locally else "external") doc.set("settings", export_settings) @@ -89,7 +89,7 @@ def _buildConfigScl(selected_files: list, export_settings: bool, export_locally: def _buildLocalScl(selected_files: list, plugins_dir: str) -> str: # list of plugin descriptors: id, name, path (relative to archive root), version - from ...scl.Scl import Doc + from ....scl.Scl import Doc doc = Doc.new() listBuilder = doc.newList() diff --git a/packit/src/ui/pluginlistactivity/helpers/__init__.py b/packit/src/ui/settings/service/__init__.py similarity index 100% rename from packit/src/ui/pluginlistactivity/helpers/__init__.py rename to packit/src/ui/settings/service/__init__.py diff --git a/packit/src/settingsactivity/subsettings/Apikeys.py b/packit/src/ui/settings/subsettings/Apikeys.py similarity index 96% rename from packit/src/settingsactivity/subsettings/Apikeys.py rename to packit/src/ui/settings/subsettings/Apikeys.py index ed0fc06..03c7da9 100644 --- a/packit/src/settingsactivity/subsettings/Apikeys.py +++ b/packit/src/ui/settings/subsettings/Apikeys.py @@ -41,8 +41,8 @@ def _get_gemini_key_preview() -> "str | None": # returns "AB..xyz" preview or None if key not set try: import ctypes - from ...NativeLoader import loadPackitKey - from ...utils.Paths import getKeysDir + from ....core.NativeLoader import loadPackitKey + from ....utils.Paths import getKeysDir lib = loadPackitKey() if not lib: @@ -83,8 +83,8 @@ def _get_gemini_key_preview() -> "str | None": def _save_gemini_key(keyValue: str): try: import ctypes - from ...NativeLoader import loadPackitKey - from ...utils.Paths import getKeysDir + from ....core.NativeLoader import loadPackitKey + from ....utils.Paths import getKeysDir import os keysDir = getKeysDir() @@ -117,8 +117,8 @@ def _save_gemini_key(keyValue: str): def _delete_gemini_key(): try: - from ...NativeLoader import loadPackitKey - from ...utils.Paths import getKeysDir + from ....core.NativeLoader import loadPackitKey + from ....utils.Paths import getKeysDir lib = loadPackitKey() if not lib: @@ -142,7 +142,7 @@ def _delete_gemini_key(): def _has_gemini_cache() -> bool: try: import json, os - from ...utils.Paths import getGeminiCachePath + from ....utils.Paths import getGeminiCachePath path = getGeminiCachePath() if not os.path.exists(path): return False @@ -166,7 +166,7 @@ def _on_confirm(b, w): b.dismiss() try: import os - from ...utils.Paths import getGeminiCachePath + from ....utils.Paths import getGeminiCachePath path = getGeminiCachePath() if os.path.exists(path): os.remove(path) diff --git a/packit/src/settingsactivity/subsettings/Comps.py b/packit/src/ui/settings/subsettings/Comps.py similarity index 100% rename from packit/src/settingsactivity/subsettings/Comps.py rename to packit/src/ui/settings/subsettings/Comps.py diff --git a/packit/src/settingsactivity/subsettings/Debug.py b/packit/src/ui/settings/subsettings/Debug.py similarity index 99% rename from packit/src/settingsactivity/subsettings/Debug.py rename to packit/src/ui/settings/subsettings/Debug.py index 895e0ef..31639be 100644 --- a/packit/src/settingsactivity/subsettings/Debug.py +++ b/packit/src/ui/settings/subsettings/Debug.py @@ -214,7 +214,7 @@ def pair_row(left, right): ) try: - from ...ui.ViewUtils import applyFontToTree + from ...components.ViewUtils import applyFontToTree applyFontToTree(outer) except Exception: pass @@ -271,7 +271,7 @@ def _onWriteLogsChange(enabled): def _sendLatestLog(view): try: - from ...utils.Paths import getCacheRoot, getLogShareCachePath + from ....utils.Paths import getCacheRoot, getLogShareCachePath log_path = getCacheRoot() + "/latestlog.txt" logx(f"sendLatestLog: log_path={log_path}", True) if not os.path.exists(log_path): @@ -353,7 +353,7 @@ def _copyLatestLogPath(view): def _getLatestLogPath(): try: - from ...utils.Paths import getCacheRoot + from ....utils.Paths import getCacheRoot return getCacheRoot() + "/latestlog.txt" except Exception: return None diff --git a/packit/src/settingsactivity/subsettings/FileSettings.py b/packit/src/ui/settings/subsettings/FileSettings.py similarity index 100% rename from packit/src/settingsactivity/subsettings/FileSettings.py rename to packit/src/ui/settings/subsettings/FileSettings.py diff --git a/packit/src/settingsactivity/subsettings/Hotkeys.py b/packit/src/ui/settings/subsettings/Hotkeys.py similarity index 100% rename from packit/src/settingsactivity/subsettings/Hotkeys.py rename to packit/src/ui/settings/subsettings/Hotkeys.py diff --git a/packit/src/settingsactivity/subsettings/Inline.py b/packit/src/ui/settings/subsettings/Inline.py similarity index 98% rename from packit/src/settingsactivity/subsettings/Inline.py rename to packit/src/ui/settings/subsettings/Inline.py index ff35585..3b79677 100644 --- a/packit/src/settingsactivity/subsettings/Inline.py +++ b/packit/src/ui/settings/subsettings/Inline.py @@ -3,7 +3,7 @@ from ui.settings import Header, Switch, Divider, Input, Text from elyx import strings -from ...chatactivity.inline import InlineState +from ....integrations.chat.inline import InlineState def build_inline_page(other_settings, fmt_inline_str, reload_plugin_settings, open_url): diff --git a/packit/src/settingsactivity/subsettings/Interface.py b/packit/src/ui/settings/subsettings/Interface.py similarity index 100% rename from packit/src/settingsactivity/subsettings/Interface.py rename to packit/src/ui/settings/subsettings/Interface.py diff --git a/packit/src/settingsactivity/subsettings/Misc.py b/packit/src/ui/settings/subsettings/Misc.py similarity index 100% rename from packit/src/settingsactivity/subsettings/Misc.py rename to packit/src/ui/settings/subsettings/Misc.py diff --git a/packit/src/settingsactivity/subsettings/PluginCardEditor.py b/packit/src/ui/settings/subsettings/PluginCardEditor.py similarity index 99% rename from packit/src/settingsactivity/subsettings/PluginCardEditor.py rename to packit/src/ui/settings/subsettings/PluginCardEditor.py index 4ac7f95..99e6b62 100644 --- a/packit/src/settingsactivity/subsettings/PluginCardEditor.py +++ b/packit/src/ui/settings/subsettings/PluginCardEditor.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ...utils.Bulletins import factory as _pbf +from ....utils.Bulletins import factory as _pbf import ctypes from android.view import View, Gravity from android.widget import LinearLayout, TextView, FrameLayout, ScrollView, ImageView, SeekBar @@ -1532,7 +1532,7 @@ def on_change(v): md3 = None try: - from ...ui.Md3Slider import createMd3Slider + from ...components.Md3Slider import createMd3Slider md3 = createMd3Slider(ctx, min_val, max_val, val, on_change) except Exception as e: logx(f"PCE: md3 slider create error: {e}", False) diff --git a/packit/src/settingsactivity/subsettings/PluginProfile.py b/packit/src/ui/settings/subsettings/PluginProfile.py similarity index 100% rename from packit/src/settingsactivity/subsettings/PluginProfile.py rename to packit/src/ui/settings/subsettings/PluginProfile.py diff --git a/packit/src/settingsactivity/subsettings/Sfx.py b/packit/src/ui/settings/subsettings/Sfx.py similarity index 95% rename from packit/src/settingsactivity/subsettings/Sfx.py rename to packit/src/ui/settings/subsettings/Sfx.py index a51f384..c181d37 100644 --- a/packit/src/settingsactivity/subsettings/Sfx.py +++ b/packit/src/ui/settings/subsettings/Sfx.py @@ -26,7 +26,7 @@ def _reload(): def _make_expandable(other_settings, ctx): try: from android_utils import OnClickListener - from ...DexLoader import sfxExpandableCreate + from ....core.DexLoader import sfxExpandableCreate checked_count = sum( 1 for key, _, default in _SFX_CHILDREN if settings.get(key, default) @@ -72,7 +72,7 @@ def switch_click(view): def _make_child(ctx, key, text, default): try: - from ...DexLoader import sfxChildCreate + from ....core.DexLoader import sfxChildCreate item = sfxChildCreate( ctx, @@ -100,7 +100,7 @@ def _make_volume_slider(ctx): try: from java import dynamic_proxy from java.lang.reflect import InvocationHandler - from ...DexLoader import sfxVolumeSliderCreate + from ....core.DexLoader import sfxVolumeSliderCreate class _VolumeChange(dynamic_proxy(InvocationHandler)): def invoke(self, proxy, method, args): @@ -120,7 +120,7 @@ def invoke(self, proxy, method, args): _VolumeChange(), ) if view is None: - from ...ui.Md3Slider import createMd3Slider + from ...components.Md3Slider import createMd3Slider def on_change(value): settings.set("sfx_volume", int(value), reload_settings=False) diff --git a/packit/src/settingsactivity/subsettings/Updplugins.py b/packit/src/ui/settings/subsettings/Updplugins.py similarity index 100% rename from packit/src/settingsactivity/subsettings/Updplugins.py rename to packit/src/ui/settings/subsettings/Updplugins.py diff --git a/packit/src/ui/pluginlistactivity/sheets/__init__.py b/packit/src/ui/settings/subsettings/__init__.py similarity index 100% rename from packit/src/ui/pluginlistactivity/sheets/__init__.py rename to packit/src/ui/settings/subsettings/__init__.py diff --git a/packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py b/packit/src/ui/updates/ClearIgnoreListDialog.py similarity index 99% rename from packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py rename to packit/src/ui/updates/ClearIgnoreListDialog.py index 14e051f..9510539 100644 --- a/packit/src/ui/pluginsupdates/ClearIgnoreListDialog.py +++ b/packit/src/ui/updates/ClearIgnoreListDialog.py @@ -421,7 +421,7 @@ def _on_cancel_click(v): card.setScaleY(0.92) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsupdates/Fragment.py b/packit/src/ui/updates/Fragment.py similarity index 99% rename from packit/src/ui/pluginsupdates/Fragment.py rename to packit/src/ui/updates/Fragment.py index 8515fce..78eb355 100644 --- a/packit/src/ui/pluginsupdates/Fragment.py +++ b/packit/src/ui/updates/Fragment.py @@ -332,7 +332,7 @@ def onFragmentCreate(self, *_): def onFragmentDestroy(self, *_): self._alive[0] = False try: - from ...Core import remove_install_listener + from ...core.Core import remove_install_listener for fn in list(self._active_listeners): remove_install_listener(fn) self._active_listeners.clear() @@ -954,7 +954,7 @@ def task(): pass def _open_catalog(): try: - from ..pluginlistactivity.Fragment import InstallUI + from ..plugins.Fragment import InstallUI InstallUI(self._plugin).open() except Exception as e: logx(f"pluginsUpdates: _open_catalog error: {e}", False) @@ -1427,8 +1427,8 @@ def task(): def on_ui(): try: - from ..pluginlistactivity.Fragment import InstallUI - from ..pluginactivity.Fragment import show_plugin_profile + from ..plugins.Fragment import InstallUI + from ..plugin.Fragment import show_plugin_profile install_ui = InstallUI(plugin) all_plugins = [{"id": k, **v} for k, v in repo_plugins.items() if isinstance(v, dict)] show_plugin_profile(plugin_data, install_ui, all_plugins=all_plugins, repo_id=repo_id) @@ -1535,7 +1535,7 @@ def set_btn_state(state: str): def task(): try: - from ...Core import install_plugin + from ...core.Core import install_plugin from ...network import Storage from ...utils import CachedRepos @@ -1564,7 +1564,7 @@ def on_finish(ok): if not ok: run_on_ui_thread(lambda: set_btn_state("idle")) - from ...Core import add_install_listener, remove_install_listener + from ...core.Core import add_install_listener, remove_install_listener listener_ref = [None] @@ -2011,7 +2011,7 @@ def set_btn_state(state: str): def task(): try: - from ...Core import install_plugin_silent + from ...core.Core import install_plugin_silent from ...utils.Paths import getPluginsDir from ...network import Storage from ...utils import CachedRepos @@ -2126,7 +2126,7 @@ def _on_update_all_click(self): # elyx plugins install in parallel via existing _install_update (shows install dialog) # non-elyx plugins install sequentially via _install_update_silent: done[i] → start[i+1] - from ...Core import _is_elyx_plugin + from ...core.Core import _is_elyx_plugin elyx_items = [(item, btn, icon) for item, btn, icon in pending if _is_elyx_plugin(item)] silent_items = [(item, btn, icon) for item, btn, icon in pending if not _is_elyx_plugin(item)] diff --git a/packit/src/ui/pluginsupdates/HideAllDialog.py b/packit/src/ui/updates/HideAllDialog.py similarity index 99% rename from packit/src/ui/pluginsupdates/HideAllDialog.py rename to packit/src/ui/updates/HideAllDialog.py index 0b98c88..6ed9a12 100644 --- a/packit/src/ui/pluginsupdates/HideAllDialog.py +++ b/packit/src/ui/updates/HideAllDialog.py @@ -246,7 +246,7 @@ def _dismiss(on_end=None): card.setScaleY(0.92) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsupdates/HideDialog.py b/packit/src/ui/updates/HideDialog.py similarity index 99% rename from packit/src/ui/pluginsupdates/HideDialog.py rename to packit/src/ui/updates/HideDialog.py index 3e3c132..6a02202 100644 --- a/packit/src/ui/pluginsupdates/HideDialog.py +++ b/packit/src/ui/updates/HideDialog.py @@ -309,7 +309,7 @@ def _open_mode_picker(): card.setScaleY(0.92) try: - from ..ViewUtils import applyFontToTree + from ..components.ViewUtils import applyFontToTree applyFontToTree(card) except Exception: pass diff --git a/packit/src/ui/pluginsupdates/StartupSheet.py b/packit/src/ui/updates/StartupSheet.py similarity index 99% rename from packit/src/ui/pluginsupdates/StartupSheet.py rename to packit/src/ui/updates/StartupSheet.py index d457e78..b428f8c 100644 --- a/packit/src/ui/pluginsupdates/StartupSheet.py +++ b/packit/src/ui/updates/StartupSheet.py @@ -633,7 +633,7 @@ def task(): try: from ...deeplinks.Install import _resolvePluginsUrl from ...utils.Paths import getPluginsDir - from ...Core import install_plugin_silent + from ...core.Core import install_plugin_silent import requests as _req import os diff --git a/packit/src/ui/pluginsupdates/__init__.py b/packit/src/ui/updates/__init__.py similarity index 100% rename from packit/src/ui/pluginsupdates/__init__.py rename to packit/src/ui/updates/__init__.py diff --git a/packit/src/utils/Copy.py b/packit/src/utils/Copy.py index fe57d0d..c8f591c 100644 --- a/packit/src/utils/Copy.py +++ b/packit/src/utils/Copy.py @@ -3,19 +3,19 @@ from packutil import logx -from ..utils.Bulletins import factory as _pbf +from .Bulletins import factory as _pbf try: from org.telegram.messenger import AndroidUtilities, R as R_tg except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import AndroidUtilities, R as R_tg failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() from client_utils import get_last_fragment from hook_utils import find_class try: from elyx import strings except Exception as e: import android_utils as _au; _au.log(f"import elyx import strings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() BulletinFactory = find_class("org.telegram.ui.Components.BulletinFactory") diff --git a/packit/src/utils/HashUtil.py b/packit/src/utils/HashUtil.py index ea36dfa..60b58e0 100644 --- a/packit/src/utils/HashUtil.py +++ b/packit/src/utils/HashUtil.py @@ -27,7 +27,7 @@ def _getBitHashLib(): if _libLoaded: return _lib _libLoaded = True - from ..NativeLoader import loadBitHash + from ..core.NativeLoader import loadBitHash _lib = loadBitHash() if _lib is not None: logx("hashutil: libbithash.so loaded successfully!", True) diff --git a/packit/src/utils/LocalConfig.py b/packit/src/utils/LocalConfig.py index 3e5fbba..367d16c 100644 --- a/packit/src/utils/LocalConfig.py +++ b/packit/src/utils/LocalConfig.py @@ -10,12 +10,12 @@ from elyx import assets except Exception as e: import android_utils as _au; _au.log(f"import elyx import assets failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() try: from org.telegram.messenger import ApplicationLoader except Exception as e: import android_utils as _au; _au.log(f"import org.telegram.messenger import ApplicationLoader failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() def _get_configs_dir() -> str: diff --git a/packit/src/utils/Media.py b/packit/src/utils/Media.py index b9920fa..c25ced9 100644 --- a/packit/src/utils/Media.py +++ b/packit/src/utils/Media.py @@ -9,7 +9,7 @@ from elyx import settings except Exception as e: import android_utils as _au; _au.log(f"import elyx import settings failed: {e}") - from ..utils.ImportFailed import showImportFailedAlert as _sifa; _sifa() + from .ImportFailed import showImportFailedAlert as _sifa; _sifa() from java import dynamic_proxy @@ -21,7 +21,7 @@ def playSound(soundPath: str, soundKey: str = None, check_pending: bool = True, if check_pending: try: - from ..ui.achievementsactivity.service.AchivementsEngine import is_achievement_pending + from ..ui.achievements.service.AchivementsEngine import is_achievement_pending if is_achievement_pending(): return except Exception: diff --git a/packit/src/utils/Paths.py b/packit/src/utils/Paths.py index 26500c9..97c45b6 100644 --- a/packit/src/utils/Paths.py +++ b/packit/src/utils/Paths.py @@ -36,7 +36,7 @@ def getPackitArchivesDir() -> str: return _filesDir() + "/plugins/ElyxPlugins/packit" def getBitHashSoPath() -> str: - from ..NativeLoader import detectArch + from ..core.NativeLoader import detectArch return _filesDir() + f"/plugins/ElyxPlugins/shareui_packit/packit/native/{detectArch()}/libbithash.so" def getRepoCachePath(repoId: str) -> str: diff --git a/packit/src/utils/Search.py b/packit/src/utils/Search.py index 02e62bd..5f3e4e7 100644 --- a/packit/src/utils/Search.py +++ b/packit/src/utils/Search.py @@ -27,7 +27,7 @@ def _load_native() -> bool: except Exception: pass - from ..NativeLoader import loadSearch + from ..core.NativeLoader import loadSearch lib = loadSearch() if lib is None: logx("search: failed to load libsearch.so, using python fallback", True) diff --git a/packit/src/utils/Share.py b/packit/src/utils/Share.py index b45f92b..521ece2 100644 --- a/packit/src/utils/Share.py +++ b/packit/src/utils/Share.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later from packutil import logx -from ..utils.Bulletins import factory as _pbf +from .Bulletins import factory as _pbf import os import requests import threading diff --git a/packit/src/utils/Translation.py b/packit/src/utils/Translation.py index 62d73c2..0e123af 100644 --- a/packit/src/utils/Translation.py +++ b/packit/src/utils/Translation.py @@ -3,7 +3,7 @@ from packutil import logx -from ..utils.Bulletins import factory as _pbf +from .Bulletins import factory as _pbf from client_utils import get_last_fragment from hook_utils import find_class from java import dynamic_proxy @@ -259,7 +259,7 @@ def on_close(v): translate_sheet.setCustomView(root) try: - from ..ui.ViewUtils import applyFontToTree + from ..ui.components.ViewUtils import applyFontToTree applyFontToTree(root) except Exception: pass From f1544b594b930656456e0504e186247d6e490892 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:24:20 +0000 Subject: [PATCH 44/46] Write down where things go MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tree was just rearranged and nothing said why, so the next person to add a file would have had to guess from what was already there — which is how it got disorganised the first time. CONTRIBUTING.md now opens with the layout, a table of "I am adding X, where does it go", the two filenames that must not be renamed and why, and the one rule about repository access that is easy to get wrong: CachedRepos reads the disk and is safe anywhere, Storage goes to the network and must not run on the UI thread. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- CONTRIBUTING.md | 96 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f0f2485..9cd66ac 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,5 +1,101 @@ # Contributing +## Where things live + +``` +packit/ + meta.yml plugin name, id, version, minimum client and SDK + locales/ strings_{en,ru,de,be}.json — the only place UI text belongs + res/ fonts, sounds, drawables shipped with the plugin + dex/ compiled Kotlin, built from kotlin/ + native/ .so files per ABI + src/ everything below + +kotlin/ Kotlin sources for the dexes in packit/dex +scripts/ one-off tooling; yours goes in scripts/{username}/ +``` + +`packit/src` is laid out by what a module *is*, not by which client screen it +happens to touch: + +``` +src/ + BasePlugin.py the entry point — the class the loader looks for + Main.py startup, hooks, lifecycle + + core/ installing and removing plugins, loading dexes and native + libraries, the repository list itself + network/ everything that goes over the wire to a repository + utils/ helpers with no UI of their own, including where files + live on disk + scl/ the TOML parser (native-backed) used for .afp files and + plugin export + + ui/ the plugin's own screens + MainActivity.py builds the plugin's settings root + components/ pieces screens are assembled from — never a screen + dialogs/ sheets and dialogs that belong to no single screen + settings/ the plugin's settings pages + plugins/ plugin/ icons/ repos/ updates/ files/ + achievements/ contributors/ suggest/ + + integrations/ code that reaches into a screen the *client* owns + chat/ the chat screen: import sheets, inline mode, security + chatlist/ the dialogs list: buttons, widgets, update sheet + hooks/ hooks into the client's own settings and fragments + decorations/ badges and title icons drawn into client UI + + deeplinks/ one module per tg://packit?… route, dispatched by + DeepHandler +``` + +### Where do I put a new file? + +| What you are adding | Where it goes | +|---|---| +| A screen of the plugin's own | `ui//Fragment.py`, with its sheets and helpers beside it | +| A dialog or sheet used by one screen | that screen's package | +| A dialog or sheet used by several | `ui/dialogs/` | +| A reusable widget, or a view helper | `ui/components/` | +| A page in the plugin's settings | `ui/settings/subsettings/` and register it in `ui/settings/Settings.py` | +| Something drawn into the client's chat, chat list or profile | `integrations//` | +| A hook into a client class | `integrations/hooks/` | +| A new `tg://packit?…` route | `deeplinks/.py`, registered in `deeplinks/DeepHandler.py` | +| A pure helper — parsing, hashing, paths, formatting | `utils/` | +| Anything that downloads from a repository | `network/Storage.py` — do not add a second one | +| Anything that reads `reposCache/{rm_rid}.json` | `utils/CachedRepos.py` — same rule | +| User-visible text | `packit/locales/strings_*.json`, all four in lockstep | + +If a file does not obviously belong anywhere, that is usually a sign it does +two things. Split it before inventing a folder for it. + +### Naming + +- **Folders are lowercase**, no separators: `ui/plugins`, `integrations/chatlist`. +- **Modules are PascalCase**: `CachedRepos.py`, `AddSheet.py`, `EnterView.py`. +- Two names are fixed and must not be renamed: `BasePlugin.py`, which + `refmap.yml` and the builder's `compilationIgnore` both point at by path, and + `__init__.py`, which is Python's. + +### Imports + +All imports inside `packit/src` are relative — `from ..utils import Paths`, +never `from packit.src.utils import Paths`. When you move a file, remember that +the number of dots changes with its depth. + +Two modules are the only way to reach a repository, and which one you want is +readable from the call: + +```python +from ..utils import CachedRepos # off disk, no network, safe anywhere +from ..network import Storage # over the network, never on the UI thread + +url = CachedRepos.plugins_url(repo) +plugins, error = Storage.fetch_plugins(url) +``` + +--- + ## Logging **Always use `logx` from `packutil` for all logs. Never use `log` from `android_utils` directly.** From b958d6a02120546be0ab5b36c3d6a45e6a5ebbf4 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:27:49 +0000 Subject: [PATCH 45/46] Move the python tree under src/python MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit src/ now holds one folder per language the plugin is written in, with the whole package under src/python. Nothing inside it changes: the tree moved as a unit, so every relative import still points where it did — all 659 of them resolve unchanged. Two files outside it had to follow, both of which name the package root by path: refmap.yml's main, and the builder's source and compilationIgnore. The artifact comes out the same shape as before, one directory deeper — the package root with its __init__ at packit/src/python, BasePlugin.py uncompiled beside it. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- CONTRIBUTING.md | 17 +++++++++++------ packit/.elyxbuilder/config.yml | 4 ++-- packit/meta.yml | 2 +- packit/src/{ => python}/BasePlugin.py | 0 packit/src/{ => python}/Main.py | 0 packit/src/{ => python}/__init__.py | 0 packit/src/{ => python}/core/Core.py | 0 packit/src/{ => python}/core/DexLoader.py | 0 packit/src/{ => python}/core/NativeLoader.py | 0 .../src/{ => python}/core/RepositoryManager.py | 0 packit/src/{ => python}/core/__init__.py | 0 .../src/{ => python}/deeplinks/Contributors.py | 0 .../src/{ => python}/deeplinks/DeepHandler.py | 0 .../src/{ => python}/deeplinks/DeeplinkMenu.py | 0 packit/src/{ => python}/deeplinks/Docs.py | 0 packit/src/{ => python}/deeplinks/Forum.py | 0 packit/src/{ => python}/deeplinks/Install.py | 0 packit/src/{ => python}/deeplinks/MainMenu.py | 0 packit/src/{ => python}/deeplinks/Other.py | 0 packit/src/{ => python}/deeplinks/Pkill.py | 0 packit/src/{ => python}/deeplinks/Plugin.py | 0 packit/src/{ => python}/deeplinks/Problems.py | 0 packit/src/{ => python}/deeplinks/Repo.py | 0 packit/src/{ => python}/deeplinks/Settings.py | 0 packit/src/{ => python}/deeplinks/Suggestion.py | 0 packit/src/{ => python}/deeplinks/Update.py | 0 packit/src/{ => python}/deeplinks/__init__.py | 0 .../src/{ => python}/deeplinks/secret/Aytist.py | 0 .../{ => python}/deeplinks/secret/Premium.py | 0 .../{ => python}/deeplinks/secret/Terraria.py | 0 .../{ => python}/deeplinks/secret/__init__.py | 0 .../src/{ => python}/integrations/__init__.py | 0 .../{ => python}/integrations/chat/AfpFile.py | 0 .../chat/ConfirmImportBottomSheet.py | 0 .../integrations/chat/ImportBottomSheet.py | 0 .../{ => python}/integrations/chat/__init__.py | 0 .../chat/export/DecryptorBottomSheet.py | 0 .../chat/export/ImportBottomSheet.py | 0 .../integrations/chat/export/__init__.py | 0 .../integrations/chat/export/bin/Reader.py | 0 .../integrations/chat/export/bin/Writer.py | 0 .../integrations/chat/export/bin/__init__.py | 0 .../integrations/chat/inline/EnterView.py | 0 .../integrations/chat/inline/InlineBtns.py | 0 .../integrations/chat/inline/InlineState.py | 0 .../integrations/chat/inline/MessageBuilder.py | 0 .../integrations/chat/inline/__init__.py | 0 .../chat/linksicons/LinksBottomSheet.py | 0 .../integrations/chat/linksicons/__init__.py | 0 .../securitybottomsheets/HashBottomSheet.py | 0 .../SignaturesBottomSheet.py | 0 .../chat/securitybottomsheets/__init__.py | 0 .../integrations/chatlist/BtnCAB.py | 0 .../integrations/chatlist/BtnPluginsMenu.py | 0 .../integrations/chatlist/BuildNotCorrect.py | 0 .../integrations/chatlist/Button.py | 0 .../integrations/chatlist/ChatDialogButton.py | 0 .../integrations/chatlist/PackitUpdateSheet.py | 0 .../integrations/chatlist/PillWidget.py | 0 .../integrations/chatlist/UpdatesWidget.py | 0 .../integrations/chatlist/__init__.py | 0 .../integrations/decorations/Badges.py | 0 .../integrations/decorations/ChatBadge.py | 0 .../integrations/decorations/ChatTitleIcon.py | 0 .../integrations/decorations/Everyone.py | 0 .../integrations/decorations/IsBeta.py | 0 .../decorations/ProfileTitleIcon.py | 0 .../integrations/decorations/Text.py | 0 .../integrations/decorations/__init__.py | 0 .../integrations/hooks/AddIconsFab.py | 0 .../integrations/hooks/AddPluginFab.py | 0 .../integrations/hooks/InstallDismissHook.py | 0 .../integrations/hooks/SettingsActivityHook.py | 0 .../integrations/hooks/UniversalFragmentFix.py | 0 .../{ => python}/integrations/hooks/__init__.py | 0 packit/src/{ => python}/network/Storage.py | 0 packit/src/{ => python}/network/__init__.py | 0 packit/src/{ => python}/scl/Doc.py | 0 packit/src/{ => python}/scl/Errors.py | 0 packit/src/{ => python}/scl/Native.py | 0 packit/src/{ => python}/scl/Opts.py | 0 packit/src/{ => python}/scl/Scl.py | 0 packit/src/{ => python}/scl/Value.py | 0 packit/src/{ => python}/scl/__init__.py | 0 packit/src/{ => python}/ui/MainActivity.py | 0 packit/src/{ => python}/ui/__init__.py | 0 .../{ => python}/ui/achievements/Fragment.py | 0 .../{ => python}/ui/achievements/__init__.py | 0 .../achievements/service/AchivementsEngine.py | 0 .../ui/achievements/service/__init__.py | 0 .../{ => python}/ui/components/ContextMenu.py | 0 .../{ => python}/ui/components/FontManager.py | 0 .../src/{ => python}/ui/components/Md3Slider.py | 0 .../src/{ => python}/ui/components/ViewUtils.py | 0 .../src/{ => python}/ui/components/__init__.py | 0 .../{ => python}/ui/contributors/Fragment.py | 0 .../{ => python}/ui/contributors/__init__.py | 0 .../ui/dialogs/DeeplinkBottomSheets.py | 0 .../ui/dialogs/ExportBottomSheet.py | 0 .../ui/dialogs/FontPickerBottomSheet.py | 0 .../{ => python}/ui/dialogs/NoInternetBanner.py | 0 .../src/{ => python}/ui/dialogs/ReportDialog.py | 0 .../{ => python}/ui/dialogs/RestartDialog.py | 0 packit/src/{ => python}/ui/dialogs/__init__.py | 0 packit/src/{ => python}/ui/files/Fragment.py | 0 packit/src/{ => python}/ui/files/InfoDialog.py | 0 .../{ => python}/ui/files/OpenFileFragment.py | 0 packit/src/{ => python}/ui/files/Packlight.py | 0 packit/src/{ => python}/ui/files/__init__.py | 0 packit/src/{ => python}/ui/icons/Fragment.py | 0 .../{ => python}/ui/icons/RepoBottomSheet.py | 0 .../{ => python}/ui/icons/SortBottomSheet.py | 0 packit/src/{ => python}/ui/icons/__init__.py | 0 packit/src/{ => python}/ui/plugin/Fragment.py | 0 .../src/{ => python}/ui/plugin/VersionPicker.py | 0 packit/src/{ => python}/ui/plugin/__init__.py | 0 packit/src/{ => python}/ui/plugins/Card.py | 0 packit/src/{ => python}/ui/plugins/Fragment.py | 0 packit/src/{ => python}/ui/plugins/ListView.py | 0 packit/src/{ => python}/ui/plugins/__init__.py | 0 .../ui/plugins/filter/FilterDrawer.py | 0 .../ui/plugins/filter/FilterEngine.py | 0 .../ui/plugins/filter/TagLayoutListener.py | 0 .../{ => python}/ui/plugins/filter/__init__.py | 0 .../ui/plugins/helpers/PluginActions.py | 0 .../ui/plugins/helpers/ReportService.py | 0 .../ui/plugins/helpers/UiHelpers.py | 0 .../{ => python}/ui/plugins/helpers/Utils.py | 0 .../{ => python}/ui/plugins/helpers/__init__.py | 0 .../ui/plugins/sheets/AISearchSheet.py | 0 .../{ => python}/ui/plugins/sheets/DepsSheet.py | 0 .../ui/plugins/sheets/RepoBottomSheet.py | 0 .../ui/plugins/sheets/SortBottomSheet.py | 0 .../ui/plugins/sheets/TgChannelSheet.py | 0 .../{ => python}/ui/plugins/sheets/__init__.py | 0 packit/src/{ => python}/ui/repos/Actions.py | 0 packit/src/{ => python}/ui/repos/AddSheet.py | 0 packit/src/{ => python}/ui/repos/Card.py | 0 packit/src/{ => python}/ui/repos/Fragment.py | 0 packit/src/{ => python}/ui/repos/RepoIcon.py | 0 packit/src/{ => python}/ui/repos/RepoSheet.py | 0 packit/src/{ => python}/ui/repos/__init__.py | 0 .../src/{ => python}/ui/settings/DebugItems.py | 0 .../src/{ => python}/ui/settings/Deeplinks.py | 0 packit/src/{ => python}/ui/settings/Docs.py | 0 packit/src/{ => python}/ui/settings/Profile.py | 0 packit/src/{ => python}/ui/settings/Settings.py | 0 .../src/{ => python}/ui/settings/Utilities.py | 0 packit/src/{ => python}/ui/settings/__init__.py | 0 .../ui/settings/service/AddKeyDialog.py | 0 .../ui/settings/service/FastExpandableHook.py | 0 .../ui/settings/service/PluginsExport.py | 0 .../ui/settings/service/__init__.py | 0 .../ui/settings/subsettings/Apikeys.py | 0 .../ui/settings/subsettings/Comps.py | 0 .../ui/settings/subsettings/Debug.py | 0 .../ui/settings/subsettings/FileSettings.py | 0 .../ui/settings/subsettings/Hotkeys.py | 0 .../ui/settings/subsettings/Inline.py | 0 .../ui/settings/subsettings/Interface.py | 0 .../ui/settings/subsettings/Misc.py | 0 .../ui/settings/subsettings/PluginCardEditor.py | 0 .../ui/settings/subsettings/PluginProfile.py | 0 .../{ => python}/ui/settings/subsettings/Sfx.py | 0 .../ui/settings/subsettings/Updplugins.py | 0 .../ui/settings/subsettings/__init__.py | 0 packit/src/{ => python}/ui/suggest/Fragment.py | 0 packit/src/{ => python}/ui/suggest/__init__.py | 0 .../ui/updates/ClearIgnoreListDialog.py | 0 packit/src/{ => python}/ui/updates/Fragment.py | 0 .../{ => python}/ui/updates/HideAllDialog.py | 0 .../src/{ => python}/ui/updates/HideDialog.py | 0 .../src/{ => python}/ui/updates/StartupSheet.py | 0 packit/src/{ => python}/ui/updates/__init__.py | 0 packit/src/{ => python}/utils/AppVersion.py | 0 packit/src/{ => python}/utils/BuildInfo.py | 0 packit/src/{ => python}/utils/Bulletins.py | 0 packit/src/{ => python}/utils/CachedRepos.py | 0 packit/src/{ => python}/utils/Copy.py | 0 packit/src/{ => python}/utils/Drawable.py | 0 packit/src/{ => python}/utils/GlobalState.py | 0 packit/src/{ => python}/utils/HashUtil.py | 0 packit/src/{ => python}/utils/ImagePool.py | 0 packit/src/{ => python}/utils/ImportFailed.py | 0 packit/src/{ => python}/utils/InstallIndex.py | 0 packit/src/{ => python}/utils/Jsonx.py | 0 packit/src/{ => python}/utils/LocalConfig.py | 0 packit/src/{ => python}/utils/Markdown.py | 0 packit/src/{ => python}/utils/Media.py | 0 packit/src/{ => python}/utils/NetQueue.py | 0 packit/src/{ => python}/utils/Paths.py | 0 packit/src/{ => python}/utils/RepoStats.py | 0 packit/src/{ => python}/utils/Ripple.py | 0 packit/src/{ => python}/utils/Search.py | 0 packit/src/{ => python}/utils/Share.py | 0 packit/src/{ => python}/utils/Stickers.py | 0 packit/src/{ => python}/utils/Translation.py | 0 packit/src/{ => python}/utils/__init__.py | 0 refmap.yml | 2 +- 199 files changed, 15 insertions(+), 10 deletions(-) rename packit/src/{ => python}/BasePlugin.py (100%) rename packit/src/{ => python}/Main.py (100%) rename packit/src/{ => python}/__init__.py (100%) rename packit/src/{ => python}/core/Core.py (100%) rename packit/src/{ => python}/core/DexLoader.py (100%) rename packit/src/{ => python}/core/NativeLoader.py (100%) rename packit/src/{ => python}/core/RepositoryManager.py (100%) rename packit/src/{ => python}/core/__init__.py (100%) rename packit/src/{ => python}/deeplinks/Contributors.py (100%) rename packit/src/{ => python}/deeplinks/DeepHandler.py (100%) rename packit/src/{ => python}/deeplinks/DeeplinkMenu.py (100%) rename packit/src/{ => python}/deeplinks/Docs.py (100%) rename packit/src/{ => python}/deeplinks/Forum.py (100%) rename packit/src/{ => python}/deeplinks/Install.py (100%) rename packit/src/{ => python}/deeplinks/MainMenu.py (100%) rename packit/src/{ => python}/deeplinks/Other.py (100%) rename packit/src/{ => python}/deeplinks/Pkill.py (100%) rename packit/src/{ => python}/deeplinks/Plugin.py (100%) rename packit/src/{ => python}/deeplinks/Problems.py (100%) rename packit/src/{ => python}/deeplinks/Repo.py (100%) rename packit/src/{ => python}/deeplinks/Settings.py (100%) rename packit/src/{ => python}/deeplinks/Suggestion.py (100%) rename packit/src/{ => python}/deeplinks/Update.py (100%) rename packit/src/{ => python}/deeplinks/__init__.py (100%) rename packit/src/{ => python}/deeplinks/secret/Aytist.py (100%) rename packit/src/{ => python}/deeplinks/secret/Premium.py (100%) rename packit/src/{ => python}/deeplinks/secret/Terraria.py (100%) rename packit/src/{ => python}/deeplinks/secret/__init__.py (100%) rename packit/src/{ => python}/integrations/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/AfpFile.py (100%) rename packit/src/{ => python}/integrations/chat/ConfirmImportBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/ImportBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/export/DecryptorBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/export/ImportBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/export/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/export/bin/Reader.py (100%) rename packit/src/{ => python}/integrations/chat/export/bin/Writer.py (100%) rename packit/src/{ => python}/integrations/chat/export/bin/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/inline/EnterView.py (100%) rename packit/src/{ => python}/integrations/chat/inline/InlineBtns.py (100%) rename packit/src/{ => python}/integrations/chat/inline/InlineState.py (100%) rename packit/src/{ => python}/integrations/chat/inline/MessageBuilder.py (100%) rename packit/src/{ => python}/integrations/chat/inline/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/linksicons/LinksBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/linksicons/__init__.py (100%) rename packit/src/{ => python}/integrations/chat/securitybottomsheets/HashBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py (100%) rename packit/src/{ => python}/integrations/chat/securitybottomsheets/__init__.py (100%) rename packit/src/{ => python}/integrations/chatlist/BtnCAB.py (100%) rename packit/src/{ => python}/integrations/chatlist/BtnPluginsMenu.py (100%) rename packit/src/{ => python}/integrations/chatlist/BuildNotCorrect.py (100%) rename packit/src/{ => python}/integrations/chatlist/Button.py (100%) rename packit/src/{ => python}/integrations/chatlist/ChatDialogButton.py (100%) rename packit/src/{ => python}/integrations/chatlist/PackitUpdateSheet.py (100%) rename packit/src/{ => python}/integrations/chatlist/PillWidget.py (100%) rename packit/src/{ => python}/integrations/chatlist/UpdatesWidget.py (100%) rename packit/src/{ => python}/integrations/chatlist/__init__.py (100%) rename packit/src/{ => python}/integrations/decorations/Badges.py (100%) rename packit/src/{ => python}/integrations/decorations/ChatBadge.py (100%) rename packit/src/{ => python}/integrations/decorations/ChatTitleIcon.py (100%) rename packit/src/{ => python}/integrations/decorations/Everyone.py (100%) rename packit/src/{ => python}/integrations/decorations/IsBeta.py (100%) rename packit/src/{ => python}/integrations/decorations/ProfileTitleIcon.py (100%) rename packit/src/{ => python}/integrations/decorations/Text.py (100%) rename packit/src/{ => python}/integrations/decorations/__init__.py (100%) rename packit/src/{ => python}/integrations/hooks/AddIconsFab.py (100%) rename packit/src/{ => python}/integrations/hooks/AddPluginFab.py (100%) rename packit/src/{ => python}/integrations/hooks/InstallDismissHook.py (100%) rename packit/src/{ => python}/integrations/hooks/SettingsActivityHook.py (100%) rename packit/src/{ => python}/integrations/hooks/UniversalFragmentFix.py (100%) rename packit/src/{ => python}/integrations/hooks/__init__.py (100%) rename packit/src/{ => python}/network/Storage.py (100%) rename packit/src/{ => python}/network/__init__.py (100%) rename packit/src/{ => python}/scl/Doc.py (100%) rename packit/src/{ => python}/scl/Errors.py (100%) rename packit/src/{ => python}/scl/Native.py (100%) rename packit/src/{ => python}/scl/Opts.py (100%) rename packit/src/{ => python}/scl/Scl.py (100%) rename packit/src/{ => python}/scl/Value.py (100%) rename packit/src/{ => python}/scl/__init__.py (100%) rename packit/src/{ => python}/ui/MainActivity.py (100%) rename packit/src/{ => python}/ui/__init__.py (100%) rename packit/src/{ => python}/ui/achievements/Fragment.py (100%) rename packit/src/{ => python}/ui/achievements/__init__.py (100%) rename packit/src/{ => python}/ui/achievements/service/AchivementsEngine.py (100%) rename packit/src/{ => python}/ui/achievements/service/__init__.py (100%) rename packit/src/{ => python}/ui/components/ContextMenu.py (100%) rename packit/src/{ => python}/ui/components/FontManager.py (100%) rename packit/src/{ => python}/ui/components/Md3Slider.py (100%) rename packit/src/{ => python}/ui/components/ViewUtils.py (100%) rename packit/src/{ => python}/ui/components/__init__.py (100%) rename packit/src/{ => python}/ui/contributors/Fragment.py (100%) rename packit/src/{ => python}/ui/contributors/__init__.py (100%) rename packit/src/{ => python}/ui/dialogs/DeeplinkBottomSheets.py (100%) rename packit/src/{ => python}/ui/dialogs/ExportBottomSheet.py (100%) rename packit/src/{ => python}/ui/dialogs/FontPickerBottomSheet.py (100%) rename packit/src/{ => python}/ui/dialogs/NoInternetBanner.py (100%) rename packit/src/{ => python}/ui/dialogs/ReportDialog.py (100%) rename packit/src/{ => python}/ui/dialogs/RestartDialog.py (100%) rename packit/src/{ => python}/ui/dialogs/__init__.py (100%) rename packit/src/{ => python}/ui/files/Fragment.py (100%) rename packit/src/{ => python}/ui/files/InfoDialog.py (100%) rename packit/src/{ => python}/ui/files/OpenFileFragment.py (100%) rename packit/src/{ => python}/ui/files/Packlight.py (100%) rename packit/src/{ => python}/ui/files/__init__.py (100%) rename packit/src/{ => python}/ui/icons/Fragment.py (100%) rename packit/src/{ => python}/ui/icons/RepoBottomSheet.py (100%) rename packit/src/{ => python}/ui/icons/SortBottomSheet.py (100%) rename packit/src/{ => python}/ui/icons/__init__.py (100%) rename packit/src/{ => python}/ui/plugin/Fragment.py (100%) rename packit/src/{ => python}/ui/plugin/VersionPicker.py (100%) rename packit/src/{ => python}/ui/plugin/__init__.py (100%) rename packit/src/{ => python}/ui/plugins/Card.py (100%) rename packit/src/{ => python}/ui/plugins/Fragment.py (100%) rename packit/src/{ => python}/ui/plugins/ListView.py (100%) rename packit/src/{ => python}/ui/plugins/__init__.py (100%) rename packit/src/{ => python}/ui/plugins/filter/FilterDrawer.py (100%) rename packit/src/{ => python}/ui/plugins/filter/FilterEngine.py (100%) rename packit/src/{ => python}/ui/plugins/filter/TagLayoutListener.py (100%) rename packit/src/{ => python}/ui/plugins/filter/__init__.py (100%) rename packit/src/{ => python}/ui/plugins/helpers/PluginActions.py (100%) rename packit/src/{ => python}/ui/plugins/helpers/ReportService.py (100%) rename packit/src/{ => python}/ui/plugins/helpers/UiHelpers.py (100%) rename packit/src/{ => python}/ui/plugins/helpers/Utils.py (100%) rename packit/src/{ => python}/ui/plugins/helpers/__init__.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/AISearchSheet.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/DepsSheet.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/RepoBottomSheet.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/SortBottomSheet.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/TgChannelSheet.py (100%) rename packit/src/{ => python}/ui/plugins/sheets/__init__.py (100%) rename packit/src/{ => python}/ui/repos/Actions.py (100%) rename packit/src/{ => python}/ui/repos/AddSheet.py (100%) rename packit/src/{ => python}/ui/repos/Card.py (100%) rename packit/src/{ => python}/ui/repos/Fragment.py (100%) rename packit/src/{ => python}/ui/repos/RepoIcon.py (100%) rename packit/src/{ => python}/ui/repos/RepoSheet.py (100%) rename packit/src/{ => python}/ui/repos/__init__.py (100%) rename packit/src/{ => python}/ui/settings/DebugItems.py (100%) rename packit/src/{ => python}/ui/settings/Deeplinks.py (100%) rename packit/src/{ => python}/ui/settings/Docs.py (100%) rename packit/src/{ => python}/ui/settings/Profile.py (100%) rename packit/src/{ => python}/ui/settings/Settings.py (100%) rename packit/src/{ => python}/ui/settings/Utilities.py (100%) rename packit/src/{ => python}/ui/settings/__init__.py (100%) rename packit/src/{ => python}/ui/settings/service/AddKeyDialog.py (100%) rename packit/src/{ => python}/ui/settings/service/FastExpandableHook.py (100%) rename packit/src/{ => python}/ui/settings/service/PluginsExport.py (100%) rename packit/src/{ => python}/ui/settings/service/__init__.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Apikeys.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Comps.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Debug.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/FileSettings.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Hotkeys.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Inline.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Interface.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Misc.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/PluginCardEditor.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/PluginProfile.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Sfx.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/Updplugins.py (100%) rename packit/src/{ => python}/ui/settings/subsettings/__init__.py (100%) rename packit/src/{ => python}/ui/suggest/Fragment.py (100%) rename packit/src/{ => python}/ui/suggest/__init__.py (100%) rename packit/src/{ => python}/ui/updates/ClearIgnoreListDialog.py (100%) rename packit/src/{ => python}/ui/updates/Fragment.py (100%) rename packit/src/{ => python}/ui/updates/HideAllDialog.py (100%) rename packit/src/{ => python}/ui/updates/HideDialog.py (100%) rename packit/src/{ => python}/ui/updates/StartupSheet.py (100%) rename packit/src/{ => python}/ui/updates/__init__.py (100%) rename packit/src/{ => python}/utils/AppVersion.py (100%) rename packit/src/{ => python}/utils/BuildInfo.py (100%) rename packit/src/{ => python}/utils/Bulletins.py (100%) rename packit/src/{ => python}/utils/CachedRepos.py (100%) rename packit/src/{ => python}/utils/Copy.py (100%) rename packit/src/{ => python}/utils/Drawable.py (100%) rename packit/src/{ => python}/utils/GlobalState.py (100%) rename packit/src/{ => python}/utils/HashUtil.py (100%) rename packit/src/{ => python}/utils/ImagePool.py (100%) rename packit/src/{ => python}/utils/ImportFailed.py (100%) rename packit/src/{ => python}/utils/InstallIndex.py (100%) rename packit/src/{ => python}/utils/Jsonx.py (100%) rename packit/src/{ => python}/utils/LocalConfig.py (100%) rename packit/src/{ => python}/utils/Markdown.py (100%) rename packit/src/{ => python}/utils/Media.py (100%) rename packit/src/{ => python}/utils/NetQueue.py (100%) rename packit/src/{ => python}/utils/Paths.py (100%) rename packit/src/{ => python}/utils/RepoStats.py (100%) rename packit/src/{ => python}/utils/Ripple.py (100%) rename packit/src/{ => python}/utils/Search.py (100%) rename packit/src/{ => python}/utils/Share.py (100%) rename packit/src/{ => python}/utils/Stickers.py (100%) rename packit/src/{ => python}/utils/Translation.py (100%) rename packit/src/{ => python}/utils/__init__.py (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9cd66ac..b18bce8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -9,17 +9,22 @@ packit/ res/ fonts, sounds, drawables shipped with the plugin dex/ compiled Kotlin, built from kotlin/ native/ .so files per ABI - src/ everything below + src/ + python/ everything below — the plugin itself kotlin/ Kotlin sources for the dexes in packit/dex scripts/ one-off tooling; yours goes in scripts/{username}/ ``` -`packit/src` is laid out by what a module *is*, not by which client screen it -happens to touch: +`src/` holds one folder per language the plugin is written in, and `src/python` +is the package root — the path `refmap.yml` and the builder's `source:` both +point at. Move it and those two have to move with it. + +It is laid out by what a module *is*, not by which client screen it happens to +touch: ``` -src/ +src/python/ BasePlugin.py the entry point — the class the loader looks for Main.py startup, hooks, lifecycle @@ -79,8 +84,8 @@ two things. Split it before inventing a folder for it. ### Imports -All imports inside `packit/src` are relative — `from ..utils import Paths`, -never `from packit.src.utils import Paths`. When you move a file, remember that +All imports inside `src/python` are relative — `from ..utils import Paths`, +never an absolute path from the package root. When you move a file, remember that the number of dots changes with its depth. Two modules are the only way to reach a repository, and which one you want is diff --git a/packit/.elyxbuilder/config.yml b/packit/.elyxbuilder/config.yml index 4d57b27..f566a80 100644 --- a/packit/.elyxbuilder/config.yml +++ b/packit/.elyxbuilder/config.yml @@ -1,12 +1,12 @@ zipFormat: eaf -source: packit/src +source: packit/src/python buildNameUncompiled: '{name}-{version}' buildNameCompiled: '{name}-{version}-3.11' ignoreAll: - packit/.elyxbuilder/cache/* - packit/docs/ compilationIgnore: -- packit/src/BasePlugin.py +- packit/src/python/BasePlugin.py obfuscationConfig: stripDocstrings: true removeLogs: false diff --git a/packit/meta.yml b/packit/meta.yml index 57345a1..00b3591 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.1.2-dev.33" +version: "0.1.2-dev.34" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/packit/src/BasePlugin.py b/packit/src/python/BasePlugin.py similarity index 100% rename from packit/src/BasePlugin.py rename to packit/src/python/BasePlugin.py diff --git a/packit/src/Main.py b/packit/src/python/Main.py similarity index 100% rename from packit/src/Main.py rename to packit/src/python/Main.py diff --git a/packit/src/__init__.py b/packit/src/python/__init__.py similarity index 100% rename from packit/src/__init__.py rename to packit/src/python/__init__.py diff --git a/packit/src/core/Core.py b/packit/src/python/core/Core.py similarity index 100% rename from packit/src/core/Core.py rename to packit/src/python/core/Core.py diff --git a/packit/src/core/DexLoader.py b/packit/src/python/core/DexLoader.py similarity index 100% rename from packit/src/core/DexLoader.py rename to packit/src/python/core/DexLoader.py diff --git a/packit/src/core/NativeLoader.py b/packit/src/python/core/NativeLoader.py similarity index 100% rename from packit/src/core/NativeLoader.py rename to packit/src/python/core/NativeLoader.py diff --git a/packit/src/core/RepositoryManager.py b/packit/src/python/core/RepositoryManager.py similarity index 100% rename from packit/src/core/RepositoryManager.py rename to packit/src/python/core/RepositoryManager.py diff --git a/packit/src/core/__init__.py b/packit/src/python/core/__init__.py similarity index 100% rename from packit/src/core/__init__.py rename to packit/src/python/core/__init__.py diff --git a/packit/src/deeplinks/Contributors.py b/packit/src/python/deeplinks/Contributors.py similarity index 100% rename from packit/src/deeplinks/Contributors.py rename to packit/src/python/deeplinks/Contributors.py diff --git a/packit/src/deeplinks/DeepHandler.py b/packit/src/python/deeplinks/DeepHandler.py similarity index 100% rename from packit/src/deeplinks/DeepHandler.py rename to packit/src/python/deeplinks/DeepHandler.py diff --git a/packit/src/deeplinks/DeeplinkMenu.py b/packit/src/python/deeplinks/DeeplinkMenu.py similarity index 100% rename from packit/src/deeplinks/DeeplinkMenu.py rename to packit/src/python/deeplinks/DeeplinkMenu.py diff --git a/packit/src/deeplinks/Docs.py b/packit/src/python/deeplinks/Docs.py similarity index 100% rename from packit/src/deeplinks/Docs.py rename to packit/src/python/deeplinks/Docs.py diff --git a/packit/src/deeplinks/Forum.py b/packit/src/python/deeplinks/Forum.py similarity index 100% rename from packit/src/deeplinks/Forum.py rename to packit/src/python/deeplinks/Forum.py diff --git a/packit/src/deeplinks/Install.py b/packit/src/python/deeplinks/Install.py similarity index 100% rename from packit/src/deeplinks/Install.py rename to packit/src/python/deeplinks/Install.py diff --git a/packit/src/deeplinks/MainMenu.py b/packit/src/python/deeplinks/MainMenu.py similarity index 100% rename from packit/src/deeplinks/MainMenu.py rename to packit/src/python/deeplinks/MainMenu.py diff --git a/packit/src/deeplinks/Other.py b/packit/src/python/deeplinks/Other.py similarity index 100% rename from packit/src/deeplinks/Other.py rename to packit/src/python/deeplinks/Other.py diff --git a/packit/src/deeplinks/Pkill.py b/packit/src/python/deeplinks/Pkill.py similarity index 100% rename from packit/src/deeplinks/Pkill.py rename to packit/src/python/deeplinks/Pkill.py diff --git a/packit/src/deeplinks/Plugin.py b/packit/src/python/deeplinks/Plugin.py similarity index 100% rename from packit/src/deeplinks/Plugin.py rename to packit/src/python/deeplinks/Plugin.py diff --git a/packit/src/deeplinks/Problems.py b/packit/src/python/deeplinks/Problems.py similarity index 100% rename from packit/src/deeplinks/Problems.py rename to packit/src/python/deeplinks/Problems.py diff --git a/packit/src/deeplinks/Repo.py b/packit/src/python/deeplinks/Repo.py similarity index 100% rename from packit/src/deeplinks/Repo.py rename to packit/src/python/deeplinks/Repo.py diff --git a/packit/src/deeplinks/Settings.py b/packit/src/python/deeplinks/Settings.py similarity index 100% rename from packit/src/deeplinks/Settings.py rename to packit/src/python/deeplinks/Settings.py diff --git a/packit/src/deeplinks/Suggestion.py b/packit/src/python/deeplinks/Suggestion.py similarity index 100% rename from packit/src/deeplinks/Suggestion.py rename to packit/src/python/deeplinks/Suggestion.py diff --git a/packit/src/deeplinks/Update.py b/packit/src/python/deeplinks/Update.py similarity index 100% rename from packit/src/deeplinks/Update.py rename to packit/src/python/deeplinks/Update.py diff --git a/packit/src/deeplinks/__init__.py b/packit/src/python/deeplinks/__init__.py similarity index 100% rename from packit/src/deeplinks/__init__.py rename to packit/src/python/deeplinks/__init__.py diff --git a/packit/src/deeplinks/secret/Aytist.py b/packit/src/python/deeplinks/secret/Aytist.py similarity index 100% rename from packit/src/deeplinks/secret/Aytist.py rename to packit/src/python/deeplinks/secret/Aytist.py diff --git a/packit/src/deeplinks/secret/Premium.py b/packit/src/python/deeplinks/secret/Premium.py similarity index 100% rename from packit/src/deeplinks/secret/Premium.py rename to packit/src/python/deeplinks/secret/Premium.py diff --git a/packit/src/deeplinks/secret/Terraria.py b/packit/src/python/deeplinks/secret/Terraria.py similarity index 100% rename from packit/src/deeplinks/secret/Terraria.py rename to packit/src/python/deeplinks/secret/Terraria.py diff --git a/packit/src/deeplinks/secret/__init__.py b/packit/src/python/deeplinks/secret/__init__.py similarity index 100% rename from packit/src/deeplinks/secret/__init__.py rename to packit/src/python/deeplinks/secret/__init__.py diff --git a/packit/src/integrations/__init__.py b/packit/src/python/integrations/__init__.py similarity index 100% rename from packit/src/integrations/__init__.py rename to packit/src/python/integrations/__init__.py diff --git a/packit/src/integrations/chat/AfpFile.py b/packit/src/python/integrations/chat/AfpFile.py similarity index 100% rename from packit/src/integrations/chat/AfpFile.py rename to packit/src/python/integrations/chat/AfpFile.py diff --git a/packit/src/integrations/chat/ConfirmImportBottomSheet.py b/packit/src/python/integrations/chat/ConfirmImportBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/ConfirmImportBottomSheet.py rename to packit/src/python/integrations/chat/ConfirmImportBottomSheet.py diff --git a/packit/src/integrations/chat/ImportBottomSheet.py b/packit/src/python/integrations/chat/ImportBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/ImportBottomSheet.py rename to packit/src/python/integrations/chat/ImportBottomSheet.py diff --git a/packit/src/integrations/chat/__init__.py b/packit/src/python/integrations/chat/__init__.py similarity index 100% rename from packit/src/integrations/chat/__init__.py rename to packit/src/python/integrations/chat/__init__.py diff --git a/packit/src/integrations/chat/export/DecryptorBottomSheet.py b/packit/src/python/integrations/chat/export/DecryptorBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/export/DecryptorBottomSheet.py rename to packit/src/python/integrations/chat/export/DecryptorBottomSheet.py diff --git a/packit/src/integrations/chat/export/ImportBottomSheet.py b/packit/src/python/integrations/chat/export/ImportBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/export/ImportBottomSheet.py rename to packit/src/python/integrations/chat/export/ImportBottomSheet.py diff --git a/packit/src/integrations/chat/export/__init__.py b/packit/src/python/integrations/chat/export/__init__.py similarity index 100% rename from packit/src/integrations/chat/export/__init__.py rename to packit/src/python/integrations/chat/export/__init__.py diff --git a/packit/src/integrations/chat/export/bin/Reader.py b/packit/src/python/integrations/chat/export/bin/Reader.py similarity index 100% rename from packit/src/integrations/chat/export/bin/Reader.py rename to packit/src/python/integrations/chat/export/bin/Reader.py diff --git a/packit/src/integrations/chat/export/bin/Writer.py b/packit/src/python/integrations/chat/export/bin/Writer.py similarity index 100% rename from packit/src/integrations/chat/export/bin/Writer.py rename to packit/src/python/integrations/chat/export/bin/Writer.py diff --git a/packit/src/integrations/chat/export/bin/__init__.py b/packit/src/python/integrations/chat/export/bin/__init__.py similarity index 100% rename from packit/src/integrations/chat/export/bin/__init__.py rename to packit/src/python/integrations/chat/export/bin/__init__.py diff --git a/packit/src/integrations/chat/inline/EnterView.py b/packit/src/python/integrations/chat/inline/EnterView.py similarity index 100% rename from packit/src/integrations/chat/inline/EnterView.py rename to packit/src/python/integrations/chat/inline/EnterView.py diff --git a/packit/src/integrations/chat/inline/InlineBtns.py b/packit/src/python/integrations/chat/inline/InlineBtns.py similarity index 100% rename from packit/src/integrations/chat/inline/InlineBtns.py rename to packit/src/python/integrations/chat/inline/InlineBtns.py diff --git a/packit/src/integrations/chat/inline/InlineState.py b/packit/src/python/integrations/chat/inline/InlineState.py similarity index 100% rename from packit/src/integrations/chat/inline/InlineState.py rename to packit/src/python/integrations/chat/inline/InlineState.py diff --git a/packit/src/integrations/chat/inline/MessageBuilder.py b/packit/src/python/integrations/chat/inline/MessageBuilder.py similarity index 100% rename from packit/src/integrations/chat/inline/MessageBuilder.py rename to packit/src/python/integrations/chat/inline/MessageBuilder.py diff --git a/packit/src/integrations/chat/inline/__init__.py b/packit/src/python/integrations/chat/inline/__init__.py similarity index 100% rename from packit/src/integrations/chat/inline/__init__.py rename to packit/src/python/integrations/chat/inline/__init__.py diff --git a/packit/src/integrations/chat/linksicons/LinksBottomSheet.py b/packit/src/python/integrations/chat/linksicons/LinksBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/linksicons/LinksBottomSheet.py rename to packit/src/python/integrations/chat/linksicons/LinksBottomSheet.py diff --git a/packit/src/integrations/chat/linksicons/__init__.py b/packit/src/python/integrations/chat/linksicons/__init__.py similarity index 100% rename from packit/src/integrations/chat/linksicons/__init__.py rename to packit/src/python/integrations/chat/linksicons/__init__.py diff --git a/packit/src/integrations/chat/securitybottomsheets/HashBottomSheet.py b/packit/src/python/integrations/chat/securitybottomsheets/HashBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/securitybottomsheets/HashBottomSheet.py rename to packit/src/python/integrations/chat/securitybottomsheets/HashBottomSheet.py diff --git a/packit/src/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py b/packit/src/python/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py similarity index 100% rename from packit/src/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py rename to packit/src/python/integrations/chat/securitybottomsheets/SignaturesBottomSheet.py diff --git a/packit/src/integrations/chat/securitybottomsheets/__init__.py b/packit/src/python/integrations/chat/securitybottomsheets/__init__.py similarity index 100% rename from packit/src/integrations/chat/securitybottomsheets/__init__.py rename to packit/src/python/integrations/chat/securitybottomsheets/__init__.py diff --git a/packit/src/integrations/chatlist/BtnCAB.py b/packit/src/python/integrations/chatlist/BtnCAB.py similarity index 100% rename from packit/src/integrations/chatlist/BtnCAB.py rename to packit/src/python/integrations/chatlist/BtnCAB.py diff --git a/packit/src/integrations/chatlist/BtnPluginsMenu.py b/packit/src/python/integrations/chatlist/BtnPluginsMenu.py similarity index 100% rename from packit/src/integrations/chatlist/BtnPluginsMenu.py rename to packit/src/python/integrations/chatlist/BtnPluginsMenu.py diff --git a/packit/src/integrations/chatlist/BuildNotCorrect.py b/packit/src/python/integrations/chatlist/BuildNotCorrect.py similarity index 100% rename from packit/src/integrations/chatlist/BuildNotCorrect.py rename to packit/src/python/integrations/chatlist/BuildNotCorrect.py diff --git a/packit/src/integrations/chatlist/Button.py b/packit/src/python/integrations/chatlist/Button.py similarity index 100% rename from packit/src/integrations/chatlist/Button.py rename to packit/src/python/integrations/chatlist/Button.py diff --git a/packit/src/integrations/chatlist/ChatDialogButton.py b/packit/src/python/integrations/chatlist/ChatDialogButton.py similarity index 100% rename from packit/src/integrations/chatlist/ChatDialogButton.py rename to packit/src/python/integrations/chatlist/ChatDialogButton.py diff --git a/packit/src/integrations/chatlist/PackitUpdateSheet.py b/packit/src/python/integrations/chatlist/PackitUpdateSheet.py similarity index 100% rename from packit/src/integrations/chatlist/PackitUpdateSheet.py rename to packit/src/python/integrations/chatlist/PackitUpdateSheet.py diff --git a/packit/src/integrations/chatlist/PillWidget.py b/packit/src/python/integrations/chatlist/PillWidget.py similarity index 100% rename from packit/src/integrations/chatlist/PillWidget.py rename to packit/src/python/integrations/chatlist/PillWidget.py diff --git a/packit/src/integrations/chatlist/UpdatesWidget.py b/packit/src/python/integrations/chatlist/UpdatesWidget.py similarity index 100% rename from packit/src/integrations/chatlist/UpdatesWidget.py rename to packit/src/python/integrations/chatlist/UpdatesWidget.py diff --git a/packit/src/integrations/chatlist/__init__.py b/packit/src/python/integrations/chatlist/__init__.py similarity index 100% rename from packit/src/integrations/chatlist/__init__.py rename to packit/src/python/integrations/chatlist/__init__.py diff --git a/packit/src/integrations/decorations/Badges.py b/packit/src/python/integrations/decorations/Badges.py similarity index 100% rename from packit/src/integrations/decorations/Badges.py rename to packit/src/python/integrations/decorations/Badges.py diff --git a/packit/src/integrations/decorations/ChatBadge.py b/packit/src/python/integrations/decorations/ChatBadge.py similarity index 100% rename from packit/src/integrations/decorations/ChatBadge.py rename to packit/src/python/integrations/decorations/ChatBadge.py diff --git a/packit/src/integrations/decorations/ChatTitleIcon.py b/packit/src/python/integrations/decorations/ChatTitleIcon.py similarity index 100% rename from packit/src/integrations/decorations/ChatTitleIcon.py rename to packit/src/python/integrations/decorations/ChatTitleIcon.py diff --git a/packit/src/integrations/decorations/Everyone.py b/packit/src/python/integrations/decorations/Everyone.py similarity index 100% rename from packit/src/integrations/decorations/Everyone.py rename to packit/src/python/integrations/decorations/Everyone.py diff --git a/packit/src/integrations/decorations/IsBeta.py b/packit/src/python/integrations/decorations/IsBeta.py similarity index 100% rename from packit/src/integrations/decorations/IsBeta.py rename to packit/src/python/integrations/decorations/IsBeta.py diff --git a/packit/src/integrations/decorations/ProfileTitleIcon.py b/packit/src/python/integrations/decorations/ProfileTitleIcon.py similarity index 100% rename from packit/src/integrations/decorations/ProfileTitleIcon.py rename to packit/src/python/integrations/decorations/ProfileTitleIcon.py diff --git a/packit/src/integrations/decorations/Text.py b/packit/src/python/integrations/decorations/Text.py similarity index 100% rename from packit/src/integrations/decorations/Text.py rename to packit/src/python/integrations/decorations/Text.py diff --git a/packit/src/integrations/decorations/__init__.py b/packit/src/python/integrations/decorations/__init__.py similarity index 100% rename from packit/src/integrations/decorations/__init__.py rename to packit/src/python/integrations/decorations/__init__.py diff --git a/packit/src/integrations/hooks/AddIconsFab.py b/packit/src/python/integrations/hooks/AddIconsFab.py similarity index 100% rename from packit/src/integrations/hooks/AddIconsFab.py rename to packit/src/python/integrations/hooks/AddIconsFab.py diff --git a/packit/src/integrations/hooks/AddPluginFab.py b/packit/src/python/integrations/hooks/AddPluginFab.py similarity index 100% rename from packit/src/integrations/hooks/AddPluginFab.py rename to packit/src/python/integrations/hooks/AddPluginFab.py diff --git a/packit/src/integrations/hooks/InstallDismissHook.py b/packit/src/python/integrations/hooks/InstallDismissHook.py similarity index 100% rename from packit/src/integrations/hooks/InstallDismissHook.py rename to packit/src/python/integrations/hooks/InstallDismissHook.py diff --git a/packit/src/integrations/hooks/SettingsActivityHook.py b/packit/src/python/integrations/hooks/SettingsActivityHook.py similarity index 100% rename from packit/src/integrations/hooks/SettingsActivityHook.py rename to packit/src/python/integrations/hooks/SettingsActivityHook.py diff --git a/packit/src/integrations/hooks/UniversalFragmentFix.py b/packit/src/python/integrations/hooks/UniversalFragmentFix.py similarity index 100% rename from packit/src/integrations/hooks/UniversalFragmentFix.py rename to packit/src/python/integrations/hooks/UniversalFragmentFix.py diff --git a/packit/src/integrations/hooks/__init__.py b/packit/src/python/integrations/hooks/__init__.py similarity index 100% rename from packit/src/integrations/hooks/__init__.py rename to packit/src/python/integrations/hooks/__init__.py diff --git a/packit/src/network/Storage.py b/packit/src/python/network/Storage.py similarity index 100% rename from packit/src/network/Storage.py rename to packit/src/python/network/Storage.py diff --git a/packit/src/network/__init__.py b/packit/src/python/network/__init__.py similarity index 100% rename from packit/src/network/__init__.py rename to packit/src/python/network/__init__.py diff --git a/packit/src/scl/Doc.py b/packit/src/python/scl/Doc.py similarity index 100% rename from packit/src/scl/Doc.py rename to packit/src/python/scl/Doc.py diff --git a/packit/src/scl/Errors.py b/packit/src/python/scl/Errors.py similarity index 100% rename from packit/src/scl/Errors.py rename to packit/src/python/scl/Errors.py diff --git a/packit/src/scl/Native.py b/packit/src/python/scl/Native.py similarity index 100% rename from packit/src/scl/Native.py rename to packit/src/python/scl/Native.py diff --git a/packit/src/scl/Opts.py b/packit/src/python/scl/Opts.py similarity index 100% rename from packit/src/scl/Opts.py rename to packit/src/python/scl/Opts.py diff --git a/packit/src/scl/Scl.py b/packit/src/python/scl/Scl.py similarity index 100% rename from packit/src/scl/Scl.py rename to packit/src/python/scl/Scl.py diff --git a/packit/src/scl/Value.py b/packit/src/python/scl/Value.py similarity index 100% rename from packit/src/scl/Value.py rename to packit/src/python/scl/Value.py diff --git a/packit/src/scl/__init__.py b/packit/src/python/scl/__init__.py similarity index 100% rename from packit/src/scl/__init__.py rename to packit/src/python/scl/__init__.py diff --git a/packit/src/ui/MainActivity.py b/packit/src/python/ui/MainActivity.py similarity index 100% rename from packit/src/ui/MainActivity.py rename to packit/src/python/ui/MainActivity.py diff --git a/packit/src/ui/__init__.py b/packit/src/python/ui/__init__.py similarity index 100% rename from packit/src/ui/__init__.py rename to packit/src/python/ui/__init__.py diff --git a/packit/src/ui/achievements/Fragment.py b/packit/src/python/ui/achievements/Fragment.py similarity index 100% rename from packit/src/ui/achievements/Fragment.py rename to packit/src/python/ui/achievements/Fragment.py diff --git a/packit/src/ui/achievements/__init__.py b/packit/src/python/ui/achievements/__init__.py similarity index 100% rename from packit/src/ui/achievements/__init__.py rename to packit/src/python/ui/achievements/__init__.py diff --git a/packit/src/ui/achievements/service/AchivementsEngine.py b/packit/src/python/ui/achievements/service/AchivementsEngine.py similarity index 100% rename from packit/src/ui/achievements/service/AchivementsEngine.py rename to packit/src/python/ui/achievements/service/AchivementsEngine.py diff --git a/packit/src/ui/achievements/service/__init__.py b/packit/src/python/ui/achievements/service/__init__.py similarity index 100% rename from packit/src/ui/achievements/service/__init__.py rename to packit/src/python/ui/achievements/service/__init__.py diff --git a/packit/src/ui/components/ContextMenu.py b/packit/src/python/ui/components/ContextMenu.py similarity index 100% rename from packit/src/ui/components/ContextMenu.py rename to packit/src/python/ui/components/ContextMenu.py diff --git a/packit/src/ui/components/FontManager.py b/packit/src/python/ui/components/FontManager.py similarity index 100% rename from packit/src/ui/components/FontManager.py rename to packit/src/python/ui/components/FontManager.py diff --git a/packit/src/ui/components/Md3Slider.py b/packit/src/python/ui/components/Md3Slider.py similarity index 100% rename from packit/src/ui/components/Md3Slider.py rename to packit/src/python/ui/components/Md3Slider.py diff --git a/packit/src/ui/components/ViewUtils.py b/packit/src/python/ui/components/ViewUtils.py similarity index 100% rename from packit/src/ui/components/ViewUtils.py rename to packit/src/python/ui/components/ViewUtils.py diff --git a/packit/src/ui/components/__init__.py b/packit/src/python/ui/components/__init__.py similarity index 100% rename from packit/src/ui/components/__init__.py rename to packit/src/python/ui/components/__init__.py diff --git a/packit/src/ui/contributors/Fragment.py b/packit/src/python/ui/contributors/Fragment.py similarity index 100% rename from packit/src/ui/contributors/Fragment.py rename to packit/src/python/ui/contributors/Fragment.py diff --git a/packit/src/ui/contributors/__init__.py b/packit/src/python/ui/contributors/__init__.py similarity index 100% rename from packit/src/ui/contributors/__init__.py rename to packit/src/python/ui/contributors/__init__.py diff --git a/packit/src/ui/dialogs/DeeplinkBottomSheets.py b/packit/src/python/ui/dialogs/DeeplinkBottomSheets.py similarity index 100% rename from packit/src/ui/dialogs/DeeplinkBottomSheets.py rename to packit/src/python/ui/dialogs/DeeplinkBottomSheets.py diff --git a/packit/src/ui/dialogs/ExportBottomSheet.py b/packit/src/python/ui/dialogs/ExportBottomSheet.py similarity index 100% rename from packit/src/ui/dialogs/ExportBottomSheet.py rename to packit/src/python/ui/dialogs/ExportBottomSheet.py diff --git a/packit/src/ui/dialogs/FontPickerBottomSheet.py b/packit/src/python/ui/dialogs/FontPickerBottomSheet.py similarity index 100% rename from packit/src/ui/dialogs/FontPickerBottomSheet.py rename to packit/src/python/ui/dialogs/FontPickerBottomSheet.py diff --git a/packit/src/ui/dialogs/NoInternetBanner.py b/packit/src/python/ui/dialogs/NoInternetBanner.py similarity index 100% rename from packit/src/ui/dialogs/NoInternetBanner.py rename to packit/src/python/ui/dialogs/NoInternetBanner.py diff --git a/packit/src/ui/dialogs/ReportDialog.py b/packit/src/python/ui/dialogs/ReportDialog.py similarity index 100% rename from packit/src/ui/dialogs/ReportDialog.py rename to packit/src/python/ui/dialogs/ReportDialog.py diff --git a/packit/src/ui/dialogs/RestartDialog.py b/packit/src/python/ui/dialogs/RestartDialog.py similarity index 100% rename from packit/src/ui/dialogs/RestartDialog.py rename to packit/src/python/ui/dialogs/RestartDialog.py diff --git a/packit/src/ui/dialogs/__init__.py b/packit/src/python/ui/dialogs/__init__.py similarity index 100% rename from packit/src/ui/dialogs/__init__.py rename to packit/src/python/ui/dialogs/__init__.py diff --git a/packit/src/ui/files/Fragment.py b/packit/src/python/ui/files/Fragment.py similarity index 100% rename from packit/src/ui/files/Fragment.py rename to packit/src/python/ui/files/Fragment.py diff --git a/packit/src/ui/files/InfoDialog.py b/packit/src/python/ui/files/InfoDialog.py similarity index 100% rename from packit/src/ui/files/InfoDialog.py rename to packit/src/python/ui/files/InfoDialog.py diff --git a/packit/src/ui/files/OpenFileFragment.py b/packit/src/python/ui/files/OpenFileFragment.py similarity index 100% rename from packit/src/ui/files/OpenFileFragment.py rename to packit/src/python/ui/files/OpenFileFragment.py diff --git a/packit/src/ui/files/Packlight.py b/packit/src/python/ui/files/Packlight.py similarity index 100% rename from packit/src/ui/files/Packlight.py rename to packit/src/python/ui/files/Packlight.py diff --git a/packit/src/ui/files/__init__.py b/packit/src/python/ui/files/__init__.py similarity index 100% rename from packit/src/ui/files/__init__.py rename to packit/src/python/ui/files/__init__.py diff --git a/packit/src/ui/icons/Fragment.py b/packit/src/python/ui/icons/Fragment.py similarity index 100% rename from packit/src/ui/icons/Fragment.py rename to packit/src/python/ui/icons/Fragment.py diff --git a/packit/src/ui/icons/RepoBottomSheet.py b/packit/src/python/ui/icons/RepoBottomSheet.py similarity index 100% rename from packit/src/ui/icons/RepoBottomSheet.py rename to packit/src/python/ui/icons/RepoBottomSheet.py diff --git a/packit/src/ui/icons/SortBottomSheet.py b/packit/src/python/ui/icons/SortBottomSheet.py similarity index 100% rename from packit/src/ui/icons/SortBottomSheet.py rename to packit/src/python/ui/icons/SortBottomSheet.py diff --git a/packit/src/ui/icons/__init__.py b/packit/src/python/ui/icons/__init__.py similarity index 100% rename from packit/src/ui/icons/__init__.py rename to packit/src/python/ui/icons/__init__.py diff --git a/packit/src/ui/plugin/Fragment.py b/packit/src/python/ui/plugin/Fragment.py similarity index 100% rename from packit/src/ui/plugin/Fragment.py rename to packit/src/python/ui/plugin/Fragment.py diff --git a/packit/src/ui/plugin/VersionPicker.py b/packit/src/python/ui/plugin/VersionPicker.py similarity index 100% rename from packit/src/ui/plugin/VersionPicker.py rename to packit/src/python/ui/plugin/VersionPicker.py diff --git a/packit/src/ui/plugin/__init__.py b/packit/src/python/ui/plugin/__init__.py similarity index 100% rename from packit/src/ui/plugin/__init__.py rename to packit/src/python/ui/plugin/__init__.py diff --git a/packit/src/ui/plugins/Card.py b/packit/src/python/ui/plugins/Card.py similarity index 100% rename from packit/src/ui/plugins/Card.py rename to packit/src/python/ui/plugins/Card.py diff --git a/packit/src/ui/plugins/Fragment.py b/packit/src/python/ui/plugins/Fragment.py similarity index 100% rename from packit/src/ui/plugins/Fragment.py rename to packit/src/python/ui/plugins/Fragment.py diff --git a/packit/src/ui/plugins/ListView.py b/packit/src/python/ui/plugins/ListView.py similarity index 100% rename from packit/src/ui/plugins/ListView.py rename to packit/src/python/ui/plugins/ListView.py diff --git a/packit/src/ui/plugins/__init__.py b/packit/src/python/ui/plugins/__init__.py similarity index 100% rename from packit/src/ui/plugins/__init__.py rename to packit/src/python/ui/plugins/__init__.py diff --git a/packit/src/ui/plugins/filter/FilterDrawer.py b/packit/src/python/ui/plugins/filter/FilterDrawer.py similarity index 100% rename from packit/src/ui/plugins/filter/FilterDrawer.py rename to packit/src/python/ui/plugins/filter/FilterDrawer.py diff --git a/packit/src/ui/plugins/filter/FilterEngine.py b/packit/src/python/ui/plugins/filter/FilterEngine.py similarity index 100% rename from packit/src/ui/plugins/filter/FilterEngine.py rename to packit/src/python/ui/plugins/filter/FilterEngine.py diff --git a/packit/src/ui/plugins/filter/TagLayoutListener.py b/packit/src/python/ui/plugins/filter/TagLayoutListener.py similarity index 100% rename from packit/src/ui/plugins/filter/TagLayoutListener.py rename to packit/src/python/ui/plugins/filter/TagLayoutListener.py diff --git a/packit/src/ui/plugins/filter/__init__.py b/packit/src/python/ui/plugins/filter/__init__.py similarity index 100% rename from packit/src/ui/plugins/filter/__init__.py rename to packit/src/python/ui/plugins/filter/__init__.py diff --git a/packit/src/ui/plugins/helpers/PluginActions.py b/packit/src/python/ui/plugins/helpers/PluginActions.py similarity index 100% rename from packit/src/ui/plugins/helpers/PluginActions.py rename to packit/src/python/ui/plugins/helpers/PluginActions.py diff --git a/packit/src/ui/plugins/helpers/ReportService.py b/packit/src/python/ui/plugins/helpers/ReportService.py similarity index 100% rename from packit/src/ui/plugins/helpers/ReportService.py rename to packit/src/python/ui/plugins/helpers/ReportService.py diff --git a/packit/src/ui/plugins/helpers/UiHelpers.py b/packit/src/python/ui/plugins/helpers/UiHelpers.py similarity index 100% rename from packit/src/ui/plugins/helpers/UiHelpers.py rename to packit/src/python/ui/plugins/helpers/UiHelpers.py diff --git a/packit/src/ui/plugins/helpers/Utils.py b/packit/src/python/ui/plugins/helpers/Utils.py similarity index 100% rename from packit/src/ui/plugins/helpers/Utils.py rename to packit/src/python/ui/plugins/helpers/Utils.py diff --git a/packit/src/ui/plugins/helpers/__init__.py b/packit/src/python/ui/plugins/helpers/__init__.py similarity index 100% rename from packit/src/ui/plugins/helpers/__init__.py rename to packit/src/python/ui/plugins/helpers/__init__.py diff --git a/packit/src/ui/plugins/sheets/AISearchSheet.py b/packit/src/python/ui/plugins/sheets/AISearchSheet.py similarity index 100% rename from packit/src/ui/plugins/sheets/AISearchSheet.py rename to packit/src/python/ui/plugins/sheets/AISearchSheet.py diff --git a/packit/src/ui/plugins/sheets/DepsSheet.py b/packit/src/python/ui/plugins/sheets/DepsSheet.py similarity index 100% rename from packit/src/ui/plugins/sheets/DepsSheet.py rename to packit/src/python/ui/plugins/sheets/DepsSheet.py diff --git a/packit/src/ui/plugins/sheets/RepoBottomSheet.py b/packit/src/python/ui/plugins/sheets/RepoBottomSheet.py similarity index 100% rename from packit/src/ui/plugins/sheets/RepoBottomSheet.py rename to packit/src/python/ui/plugins/sheets/RepoBottomSheet.py diff --git a/packit/src/ui/plugins/sheets/SortBottomSheet.py b/packit/src/python/ui/plugins/sheets/SortBottomSheet.py similarity index 100% rename from packit/src/ui/plugins/sheets/SortBottomSheet.py rename to packit/src/python/ui/plugins/sheets/SortBottomSheet.py diff --git a/packit/src/ui/plugins/sheets/TgChannelSheet.py b/packit/src/python/ui/plugins/sheets/TgChannelSheet.py similarity index 100% rename from packit/src/ui/plugins/sheets/TgChannelSheet.py rename to packit/src/python/ui/plugins/sheets/TgChannelSheet.py diff --git a/packit/src/ui/plugins/sheets/__init__.py b/packit/src/python/ui/plugins/sheets/__init__.py similarity index 100% rename from packit/src/ui/plugins/sheets/__init__.py rename to packit/src/python/ui/plugins/sheets/__init__.py diff --git a/packit/src/ui/repos/Actions.py b/packit/src/python/ui/repos/Actions.py similarity index 100% rename from packit/src/ui/repos/Actions.py rename to packit/src/python/ui/repos/Actions.py diff --git a/packit/src/ui/repos/AddSheet.py b/packit/src/python/ui/repos/AddSheet.py similarity index 100% rename from packit/src/ui/repos/AddSheet.py rename to packit/src/python/ui/repos/AddSheet.py diff --git a/packit/src/ui/repos/Card.py b/packit/src/python/ui/repos/Card.py similarity index 100% rename from packit/src/ui/repos/Card.py rename to packit/src/python/ui/repos/Card.py diff --git a/packit/src/ui/repos/Fragment.py b/packit/src/python/ui/repos/Fragment.py similarity index 100% rename from packit/src/ui/repos/Fragment.py rename to packit/src/python/ui/repos/Fragment.py diff --git a/packit/src/ui/repos/RepoIcon.py b/packit/src/python/ui/repos/RepoIcon.py similarity index 100% rename from packit/src/ui/repos/RepoIcon.py rename to packit/src/python/ui/repos/RepoIcon.py diff --git a/packit/src/ui/repos/RepoSheet.py b/packit/src/python/ui/repos/RepoSheet.py similarity index 100% rename from packit/src/ui/repos/RepoSheet.py rename to packit/src/python/ui/repos/RepoSheet.py diff --git a/packit/src/ui/repos/__init__.py b/packit/src/python/ui/repos/__init__.py similarity index 100% rename from packit/src/ui/repos/__init__.py rename to packit/src/python/ui/repos/__init__.py diff --git a/packit/src/ui/settings/DebugItems.py b/packit/src/python/ui/settings/DebugItems.py similarity index 100% rename from packit/src/ui/settings/DebugItems.py rename to packit/src/python/ui/settings/DebugItems.py diff --git a/packit/src/ui/settings/Deeplinks.py b/packit/src/python/ui/settings/Deeplinks.py similarity index 100% rename from packit/src/ui/settings/Deeplinks.py rename to packit/src/python/ui/settings/Deeplinks.py diff --git a/packit/src/ui/settings/Docs.py b/packit/src/python/ui/settings/Docs.py similarity index 100% rename from packit/src/ui/settings/Docs.py rename to packit/src/python/ui/settings/Docs.py diff --git a/packit/src/ui/settings/Profile.py b/packit/src/python/ui/settings/Profile.py similarity index 100% rename from packit/src/ui/settings/Profile.py rename to packit/src/python/ui/settings/Profile.py diff --git a/packit/src/ui/settings/Settings.py b/packit/src/python/ui/settings/Settings.py similarity index 100% rename from packit/src/ui/settings/Settings.py rename to packit/src/python/ui/settings/Settings.py diff --git a/packit/src/ui/settings/Utilities.py b/packit/src/python/ui/settings/Utilities.py similarity index 100% rename from packit/src/ui/settings/Utilities.py rename to packit/src/python/ui/settings/Utilities.py diff --git a/packit/src/ui/settings/__init__.py b/packit/src/python/ui/settings/__init__.py similarity index 100% rename from packit/src/ui/settings/__init__.py rename to packit/src/python/ui/settings/__init__.py diff --git a/packit/src/ui/settings/service/AddKeyDialog.py b/packit/src/python/ui/settings/service/AddKeyDialog.py similarity index 100% rename from packit/src/ui/settings/service/AddKeyDialog.py rename to packit/src/python/ui/settings/service/AddKeyDialog.py diff --git a/packit/src/ui/settings/service/FastExpandableHook.py b/packit/src/python/ui/settings/service/FastExpandableHook.py similarity index 100% rename from packit/src/ui/settings/service/FastExpandableHook.py rename to packit/src/python/ui/settings/service/FastExpandableHook.py diff --git a/packit/src/ui/settings/service/PluginsExport.py b/packit/src/python/ui/settings/service/PluginsExport.py similarity index 100% rename from packit/src/ui/settings/service/PluginsExport.py rename to packit/src/python/ui/settings/service/PluginsExport.py diff --git a/packit/src/ui/settings/service/__init__.py b/packit/src/python/ui/settings/service/__init__.py similarity index 100% rename from packit/src/ui/settings/service/__init__.py rename to packit/src/python/ui/settings/service/__init__.py diff --git a/packit/src/ui/settings/subsettings/Apikeys.py b/packit/src/python/ui/settings/subsettings/Apikeys.py similarity index 100% rename from packit/src/ui/settings/subsettings/Apikeys.py rename to packit/src/python/ui/settings/subsettings/Apikeys.py diff --git a/packit/src/ui/settings/subsettings/Comps.py b/packit/src/python/ui/settings/subsettings/Comps.py similarity index 100% rename from packit/src/ui/settings/subsettings/Comps.py rename to packit/src/python/ui/settings/subsettings/Comps.py diff --git a/packit/src/ui/settings/subsettings/Debug.py b/packit/src/python/ui/settings/subsettings/Debug.py similarity index 100% rename from packit/src/ui/settings/subsettings/Debug.py rename to packit/src/python/ui/settings/subsettings/Debug.py diff --git a/packit/src/ui/settings/subsettings/FileSettings.py b/packit/src/python/ui/settings/subsettings/FileSettings.py similarity index 100% rename from packit/src/ui/settings/subsettings/FileSettings.py rename to packit/src/python/ui/settings/subsettings/FileSettings.py diff --git a/packit/src/ui/settings/subsettings/Hotkeys.py b/packit/src/python/ui/settings/subsettings/Hotkeys.py similarity index 100% rename from packit/src/ui/settings/subsettings/Hotkeys.py rename to packit/src/python/ui/settings/subsettings/Hotkeys.py diff --git a/packit/src/ui/settings/subsettings/Inline.py b/packit/src/python/ui/settings/subsettings/Inline.py similarity index 100% rename from packit/src/ui/settings/subsettings/Inline.py rename to packit/src/python/ui/settings/subsettings/Inline.py diff --git a/packit/src/ui/settings/subsettings/Interface.py b/packit/src/python/ui/settings/subsettings/Interface.py similarity index 100% rename from packit/src/ui/settings/subsettings/Interface.py rename to packit/src/python/ui/settings/subsettings/Interface.py diff --git a/packit/src/ui/settings/subsettings/Misc.py b/packit/src/python/ui/settings/subsettings/Misc.py similarity index 100% rename from packit/src/ui/settings/subsettings/Misc.py rename to packit/src/python/ui/settings/subsettings/Misc.py diff --git a/packit/src/ui/settings/subsettings/PluginCardEditor.py b/packit/src/python/ui/settings/subsettings/PluginCardEditor.py similarity index 100% rename from packit/src/ui/settings/subsettings/PluginCardEditor.py rename to packit/src/python/ui/settings/subsettings/PluginCardEditor.py diff --git a/packit/src/ui/settings/subsettings/PluginProfile.py b/packit/src/python/ui/settings/subsettings/PluginProfile.py similarity index 100% rename from packit/src/ui/settings/subsettings/PluginProfile.py rename to packit/src/python/ui/settings/subsettings/PluginProfile.py diff --git a/packit/src/ui/settings/subsettings/Sfx.py b/packit/src/python/ui/settings/subsettings/Sfx.py similarity index 100% rename from packit/src/ui/settings/subsettings/Sfx.py rename to packit/src/python/ui/settings/subsettings/Sfx.py diff --git a/packit/src/ui/settings/subsettings/Updplugins.py b/packit/src/python/ui/settings/subsettings/Updplugins.py similarity index 100% rename from packit/src/ui/settings/subsettings/Updplugins.py rename to packit/src/python/ui/settings/subsettings/Updplugins.py diff --git a/packit/src/ui/settings/subsettings/__init__.py b/packit/src/python/ui/settings/subsettings/__init__.py similarity index 100% rename from packit/src/ui/settings/subsettings/__init__.py rename to packit/src/python/ui/settings/subsettings/__init__.py diff --git a/packit/src/ui/suggest/Fragment.py b/packit/src/python/ui/suggest/Fragment.py similarity index 100% rename from packit/src/ui/suggest/Fragment.py rename to packit/src/python/ui/suggest/Fragment.py diff --git a/packit/src/ui/suggest/__init__.py b/packit/src/python/ui/suggest/__init__.py similarity index 100% rename from packit/src/ui/suggest/__init__.py rename to packit/src/python/ui/suggest/__init__.py diff --git a/packit/src/ui/updates/ClearIgnoreListDialog.py b/packit/src/python/ui/updates/ClearIgnoreListDialog.py similarity index 100% rename from packit/src/ui/updates/ClearIgnoreListDialog.py rename to packit/src/python/ui/updates/ClearIgnoreListDialog.py diff --git a/packit/src/ui/updates/Fragment.py b/packit/src/python/ui/updates/Fragment.py similarity index 100% rename from packit/src/ui/updates/Fragment.py rename to packit/src/python/ui/updates/Fragment.py diff --git a/packit/src/ui/updates/HideAllDialog.py b/packit/src/python/ui/updates/HideAllDialog.py similarity index 100% rename from packit/src/ui/updates/HideAllDialog.py rename to packit/src/python/ui/updates/HideAllDialog.py diff --git a/packit/src/ui/updates/HideDialog.py b/packit/src/python/ui/updates/HideDialog.py similarity index 100% rename from packit/src/ui/updates/HideDialog.py rename to packit/src/python/ui/updates/HideDialog.py diff --git a/packit/src/ui/updates/StartupSheet.py b/packit/src/python/ui/updates/StartupSheet.py similarity index 100% rename from packit/src/ui/updates/StartupSheet.py rename to packit/src/python/ui/updates/StartupSheet.py diff --git a/packit/src/ui/updates/__init__.py b/packit/src/python/ui/updates/__init__.py similarity index 100% rename from packit/src/ui/updates/__init__.py rename to packit/src/python/ui/updates/__init__.py diff --git a/packit/src/utils/AppVersion.py b/packit/src/python/utils/AppVersion.py similarity index 100% rename from packit/src/utils/AppVersion.py rename to packit/src/python/utils/AppVersion.py diff --git a/packit/src/utils/BuildInfo.py b/packit/src/python/utils/BuildInfo.py similarity index 100% rename from packit/src/utils/BuildInfo.py rename to packit/src/python/utils/BuildInfo.py diff --git a/packit/src/utils/Bulletins.py b/packit/src/python/utils/Bulletins.py similarity index 100% rename from packit/src/utils/Bulletins.py rename to packit/src/python/utils/Bulletins.py diff --git a/packit/src/utils/CachedRepos.py b/packit/src/python/utils/CachedRepos.py similarity index 100% rename from packit/src/utils/CachedRepos.py rename to packit/src/python/utils/CachedRepos.py diff --git a/packit/src/utils/Copy.py b/packit/src/python/utils/Copy.py similarity index 100% rename from packit/src/utils/Copy.py rename to packit/src/python/utils/Copy.py diff --git a/packit/src/utils/Drawable.py b/packit/src/python/utils/Drawable.py similarity index 100% rename from packit/src/utils/Drawable.py rename to packit/src/python/utils/Drawable.py diff --git a/packit/src/utils/GlobalState.py b/packit/src/python/utils/GlobalState.py similarity index 100% rename from packit/src/utils/GlobalState.py rename to packit/src/python/utils/GlobalState.py diff --git a/packit/src/utils/HashUtil.py b/packit/src/python/utils/HashUtil.py similarity index 100% rename from packit/src/utils/HashUtil.py rename to packit/src/python/utils/HashUtil.py diff --git a/packit/src/utils/ImagePool.py b/packit/src/python/utils/ImagePool.py similarity index 100% rename from packit/src/utils/ImagePool.py rename to packit/src/python/utils/ImagePool.py diff --git a/packit/src/utils/ImportFailed.py b/packit/src/python/utils/ImportFailed.py similarity index 100% rename from packit/src/utils/ImportFailed.py rename to packit/src/python/utils/ImportFailed.py diff --git a/packit/src/utils/InstallIndex.py b/packit/src/python/utils/InstallIndex.py similarity index 100% rename from packit/src/utils/InstallIndex.py rename to packit/src/python/utils/InstallIndex.py diff --git a/packit/src/utils/Jsonx.py b/packit/src/python/utils/Jsonx.py similarity index 100% rename from packit/src/utils/Jsonx.py rename to packit/src/python/utils/Jsonx.py diff --git a/packit/src/utils/LocalConfig.py b/packit/src/python/utils/LocalConfig.py similarity index 100% rename from packit/src/utils/LocalConfig.py rename to packit/src/python/utils/LocalConfig.py diff --git a/packit/src/utils/Markdown.py b/packit/src/python/utils/Markdown.py similarity index 100% rename from packit/src/utils/Markdown.py rename to packit/src/python/utils/Markdown.py diff --git a/packit/src/utils/Media.py b/packit/src/python/utils/Media.py similarity index 100% rename from packit/src/utils/Media.py rename to packit/src/python/utils/Media.py diff --git a/packit/src/utils/NetQueue.py b/packit/src/python/utils/NetQueue.py similarity index 100% rename from packit/src/utils/NetQueue.py rename to packit/src/python/utils/NetQueue.py diff --git a/packit/src/utils/Paths.py b/packit/src/python/utils/Paths.py similarity index 100% rename from packit/src/utils/Paths.py rename to packit/src/python/utils/Paths.py diff --git a/packit/src/utils/RepoStats.py b/packit/src/python/utils/RepoStats.py similarity index 100% rename from packit/src/utils/RepoStats.py rename to packit/src/python/utils/RepoStats.py diff --git a/packit/src/utils/Ripple.py b/packit/src/python/utils/Ripple.py similarity index 100% rename from packit/src/utils/Ripple.py rename to packit/src/python/utils/Ripple.py diff --git a/packit/src/utils/Search.py b/packit/src/python/utils/Search.py similarity index 100% rename from packit/src/utils/Search.py rename to packit/src/python/utils/Search.py diff --git a/packit/src/utils/Share.py b/packit/src/python/utils/Share.py similarity index 100% rename from packit/src/utils/Share.py rename to packit/src/python/utils/Share.py diff --git a/packit/src/utils/Stickers.py b/packit/src/python/utils/Stickers.py similarity index 100% rename from packit/src/utils/Stickers.py rename to packit/src/python/utils/Stickers.py diff --git a/packit/src/utils/Translation.py b/packit/src/python/utils/Translation.py similarity index 100% rename from packit/src/utils/Translation.py rename to packit/src/python/utils/Translation.py diff --git a/packit/src/utils/__init__.py b/packit/src/python/utils/__init__.py similarity index 100% rename from packit/src/utils/__init__.py rename to packit/src/python/utils/__init__.py diff --git a/refmap.yml b/refmap.yml index afac9a2..ecd1ca8 100644 --- a/refmap.yml +++ b/refmap.yml @@ -1,5 +1,5 @@ metainfo: packit/meta.yml -main: packit/src/BasePlugin.py +main: packit/src/python/BasePlugin.py strings: packit/locales assets: packit/res elyxbuilder: packit/.elyxbuilder From 7768b2ab80dbae8c646b7a0e3fb53e3c84e992d7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 11:34:02 +0000 Subject: [PATCH 46/46] Move kotlin under src, and ship one dex instead of four MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kotlin/ joins the python tree at packit/src/kotlin, so src/ holds a folder per language rather than one of them sitting at the repo root. The four dexes were one dex. R8 emits a single classes.dex from all of the sources, and kotlin-build.sh copied that same file out four times under four names — four byte-identical 55K blobs shipped, and DexLoader built a separate InMemoryDexClassLoader over each, so the same bytecode was resident four times over. There is now one packit/dex/packit.dex holding all of kawaii.packetik, one class loader for it, and classes are resolved out of it by name. packit.dex is badges.dex renamed: the bytes are unchanged, which their matching sha256 says plainly. kotlin-build.sh needed its paths fixed for more than the move: its REPO_ROOT was one directory short, resolving to scripts/, so every path it derived pointed at scripts/kotlin/src and the script could not have run as written. It also now refuses to continue if R8 splits the output across several dex files, since only the first would be loaded. Two things in the builder config, both about what ships. The kotlin sources have no business in the artifact now that they live under packit/ — and the pattern that was supposed to keep packit/docs out never worked either: ignoreAll is fnmatch against each file's full path, so a bare directory matches nothing. Both entries take a trailing /* now, and docs stops shipping too, which the line always intended. The artifact loses 80K. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0171ySGWMBZa2pESv9uTT44H --- CONTRIBUTING.md | 14 ++- packit/.elyxbuilder/config.yml | 6 +- packit/dex/catalog.dex | Bin 55720 -> 0 bytes packit/dex/openfile.dex | Bin 55720 -> 0 bytes packit/dex/{badges.dex => packit.dex} | Bin packit/dex/sfx.dex | Bin 55720 -> 0 bytes packit/meta.yml | 2 +- .../kawaii/packetik/badges/BadgesNative.kt | 0 .../packetik/catalog/CatalogChromeNative.kt | 0 .../packetik/openfile/OpenFileNative.kt | 0 .../src/kawaii/packetik/sfx/SfxNative.kt | 0 .../de/robv/android/xposed/XC_MethodHook.java | 0 .../de/robv/android/xposed/XposedBridge.java | 0 packit/src/python/core/DexLoader.py | 100 ++++++++++-------- .../python/integrations/decorations/Badges.py | 2 +- scripts/linux/kotlin-build.sh | 45 ++++---- 16 files changed, 97 insertions(+), 72 deletions(-) delete mode 100644 packit/dex/catalog.dex delete mode 100644 packit/dex/openfile.dex rename packit/dex/{badges.dex => packit.dex} (100%) delete mode 100644 packit/dex/sfx.dex rename {kotlin => packit/src/kotlin}/src/kawaii/packetik/badges/BadgesNative.kt (100%) rename {kotlin => packit/src/kotlin}/src/kawaii/packetik/catalog/CatalogChromeNative.kt (100%) rename {kotlin => packit/src/kotlin}/src/kawaii/packetik/openfile/OpenFileNative.kt (100%) rename {kotlin => packit/src/kotlin}/src/kawaii/packetik/sfx/SfxNative.kt (100%) rename {kotlin => packit/src/kotlin}/stubs/de/robv/android/xposed/XC_MethodHook.java (100%) rename {kotlin => packit/src/kotlin}/stubs/de/robv/android/xposed/XposedBridge.java (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b18bce8..3c43168 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,19 +7,27 @@ packit/ meta.yml plugin name, id, version, minimum client and SDK locales/ strings_{en,ru,de,be}.json — the only place UI text belongs res/ fonts, sounds, drawables shipped with the plugin - dex/ compiled Kotlin, built from kotlin/ + dex/ packit.dex — all of kawaii.packetik, built from src/kotlin native/ .so files per ABI src/ - python/ everything below — the plugin itself + python/ the plugin itself, everything below + kotlin/ src/ and the compile-only Xposed stubs -kotlin/ Kotlin sources for the dexes in packit/dex scripts/ one-off tooling; yours goes in scripts/{username}/ + linux/kotlin-build.sh Kotlin -> packit/dex/packit.dex + linux/build-native.sh C -> packit/native// ``` `src/` holds one folder per language the plugin is written in, and `src/python` is the package root — the path `refmap.yml` and the builder's `source:` both point at. Move it and those two have to move with it. +There is one dex, not one per Kotlin package. R8 emits a single `classes.dex` +from all of the sources, so splitting it by name only ever shipped the same +bytes several times over. Add a class under `kawaii.packetik.*`, rerun +`kotlin-build.sh`, and reach it from `core/DexLoader.py` by its fully qualified +name — nothing else needs to change. + It is laid out by what a module *is*, not by which client screen it happens to touch: diff --git a/packit/.elyxbuilder/config.yml b/packit/.elyxbuilder/config.yml index f566a80..2e49dd5 100644 --- a/packit/.elyxbuilder/config.yml +++ b/packit/.elyxbuilder/config.yml @@ -2,9 +2,13 @@ zipFormat: eaf source: packit/src/python buildNameUncompiled: '{name}-{version}' buildNameCompiled: '{name}-{version}-3.11' +# fnmatch against each file's full path in the archive, so a bare directory +# never matches anything — the trailing /* is what does the work ignoreAll: - packit/.elyxbuilder/cache/* -- packit/docs/ +- packit/docs/* +# the kotlin sources build into packit/dex/packit.dex; only the dex ships +- packit/src/kotlin/* compilationIgnore: - packit/src/python/BasePlugin.py obfuscationConfig: diff --git a/packit/dex/catalog.dex b/packit/dex/catalog.dex deleted file mode 100644 index 27c77d65be3c2c11ddaa75e30709ae4e983a714f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55720 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 W-$p&aS*2ipz+ diff --git a/packit/dex/openfile.dex b/packit/dex/openfile.dex deleted file mode 100644 index 27c77d65be3c2c11ddaa75e30709ae4e983a714f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55720 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 W-$p&aS*2ipz+ diff --git a/packit/dex/badges.dex b/packit/dex/packit.dex similarity index 100% rename from packit/dex/badges.dex rename to packit/dex/packit.dex diff --git a/packit/dex/sfx.dex b/packit/dex/sfx.dex deleted file mode 100644 index 27c77d65be3c2c11ddaa75e30709ae4e983a714f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 55720 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 W-$p&aS*2ipz+ diff --git a/packit/meta.yml b/packit/meta.yml index 00b3591..629de02 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.1.2-dev.34" +version: "0.1.2-dev.37" author: "@packitX" app_version: ">=12.9.0" sdk_version: ">=1.4.5.0" diff --git a/kotlin/src/kawaii/packetik/badges/BadgesNative.kt b/packit/src/kotlin/src/kawaii/packetik/badges/BadgesNative.kt similarity index 100% rename from kotlin/src/kawaii/packetik/badges/BadgesNative.kt rename to packit/src/kotlin/src/kawaii/packetik/badges/BadgesNative.kt diff --git a/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt b/packit/src/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt similarity index 100% rename from kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt rename to packit/src/kotlin/src/kawaii/packetik/catalog/CatalogChromeNative.kt diff --git a/kotlin/src/kawaii/packetik/openfile/OpenFileNative.kt b/packit/src/kotlin/src/kawaii/packetik/openfile/OpenFileNative.kt similarity index 100% rename from kotlin/src/kawaii/packetik/openfile/OpenFileNative.kt rename to packit/src/kotlin/src/kawaii/packetik/openfile/OpenFileNative.kt diff --git a/kotlin/src/kawaii/packetik/sfx/SfxNative.kt b/packit/src/kotlin/src/kawaii/packetik/sfx/SfxNative.kt similarity index 100% rename from kotlin/src/kawaii/packetik/sfx/SfxNative.kt rename to packit/src/kotlin/src/kawaii/packetik/sfx/SfxNative.kt diff --git a/kotlin/stubs/de/robv/android/xposed/XC_MethodHook.java b/packit/src/kotlin/stubs/de/robv/android/xposed/XC_MethodHook.java similarity index 100% rename from kotlin/stubs/de/robv/android/xposed/XC_MethodHook.java rename to packit/src/kotlin/stubs/de/robv/android/xposed/XC_MethodHook.java diff --git a/kotlin/stubs/de/robv/android/xposed/XposedBridge.java b/packit/src/kotlin/stubs/de/robv/android/xposed/XposedBridge.java similarity index 100% rename from kotlin/stubs/de/robv/android/xposed/XposedBridge.java rename to packit/src/kotlin/stubs/de/robv/android/xposed/XposedBridge.java diff --git a/packit/src/python/core/DexLoader.py b/packit/src/python/core/DexLoader.py index a370f45..b458cb9 100644 --- a/packit/src/python/core/DexLoader.py +++ b/packit/src/python/core/DexLoader.py @@ -1,52 +1,73 @@ # pyright: reportMissingImports=false # SPDX-License-Identifier: GPL-3.0-or-later -# Loads precompiled Kotlin dexes shipped in packit/dex/ and calls their -# entrypoints. Currently used for the badge system (kawaii.packetik.badges), -# ported from packit/src/other/{badges,chatBadge,chatTitleIcon,profileTitleIcon}.py. +# Loads the precompiled Kotlin shipped in packit/dex/packit.dex and calls its +# entrypoints — badges, the code viewer, the catalogue chrome, the sfx cells. # -# Dex is arch-independent Dalvik bytecode, so a single copy is shipped for all -# ABIs (unlike packit/native/, which is per-ABI). +# One dex for all of kawaii.packetik. It used to be four, named after the +# package each was for, but R8 emits one classes.dex from all of the sources +# and the build script copied that same file out under four names: four +# identical 55K blobs in the artifact, and four class loaders here, each with +# its own copy of the same bytecode resident. +# +# Dex is arch-independent Dalvik bytecode, so one copy serves all ABIs (unlike +# packit/native/, which is per-ABI). from packutil import logx import os -_DEX_BASE = "/plugins/ElyxPlugins/shareui_packit/packit/dex" +_DEX_PATH = "/plugins/ElyxPlugins/shareui_packit/packit/dex/packit.dex" + _BADGES_CLASS = "kawaii.packetik.badges.BadgesNative" +_OPENFILE_CLASS = "kawaii.packetik.openfile.OpenFileNative" +_CATALOG_CLASS = "kawaii.packetik.catalog.CatalogChromeNative" +_SFX_CLASS = "kawaii.packetik.sfx.SfxNative" -# loaded entrypoint Class objects, keyed by dex name (kept for later calls) -_loaded = {} +_loader = None # the one InMemoryDexClassLoader over packit.dex +_classes = {} # entrypoint Class objects by name, kept for later calls -def _dexPath(name: str) -> str: +def _dexPath() -> str: from ..utils.Paths import _filesDir - return _filesDir() + _DEX_BASE + "/" + name + ".dex" + return _filesDir() + _DEX_PATH -def _loadClass(dexName: str, className: str, context): - # loads className from packit/dex//.dex whose parent is the - # host app classloader (so host classes resolve). - # +def _dexLoader(context): # Uses InMemoryDexClassLoader (API 26+; PackIt requires Android 13+): the # plugin dir is writable by the app, and Android's W^X policy refuses to # load a writable dex file via DexClassLoader ("Writable dex file ... is not # allowed"). Loading from an in-memory ByteBuffer sidesteps that entirely. - cached = _loaded.get(dexName) - if cached is not None: - return cached - dex_path = _dexPath(dexName) + global _loader + if _loader is not None: + return _loader + dex_path = _dexPath() if not os.path.exists(dex_path): - logx(f"dexLoader: {dexName}.dex not found at {dex_path}", False) + logx(f"dexLoader: packit.dex not found at {dex_path}", False) return None with open(dex_path, "rb") as f: data = f.read() from java.nio import ByteBuffer from dalvik.system import InMemoryDexClassLoader - parent_cl = context.getClassLoader() - loader = InMemoryDexClassLoader(ByteBuffer.wrap(data), parent_cl) - cls = loader.loadClass(className) - _loaded[dexName] = cls - logx(f"dexLoader: loaded {className} from {dexName}.dex ({len(data)} bytes, in-memory)", True) + # parented to the host app classloader, so host classes resolve + _loader = InMemoryDexClassLoader(ByteBuffer.wrap(data), context.getClassLoader()) + logx(f"dexLoader: loaded packit.dex ({len(data)} bytes, in-memory)", True) + return _loader + + +def _loadClass(className: str, context): + cached = _classes.get(className) + if cached is not None: + return cached + loader = _dexLoader(context) + if loader is None: + return None + try: + cls = loader.loadClass(className) + except Exception as e: + logx(f"dexLoader: {className} not in packit.dex: {e}", False) + return None + _classes[className] = cls + logx(f"dexLoader: resolved {className}", True) return cls @@ -68,13 +89,13 @@ def _callStatic(cls, method: str, *args): def loadBadges(context, enabled: bool) -> bool: - # loads badges.dex and calls BadgesNative.init(classLoader, context, enabled). + # resolves BadgesNative and calls init(classLoader, context, enabled). # returns True on success; caller falls back to the Python impl on False. try: if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("badges", _BADGES_CLASS, context) + cls = _loadClass(_BADGES_CLASS, context) if cls is None: return False _callStatic(cls, "init", context.getClassLoader(), context, bool(enabled)) @@ -87,7 +108,7 @@ def loadBadges(context, enabled: bool) -> bool: def setBadgesEnabled(enabled: bool): try: - cls = _loaded.get("badges") + cls = _classes.get(_BADGES_CLASS) if cls is not None: _callStatic(cls, "setEnabled", bool(enabled)) except Exception as e: @@ -96,16 +117,13 @@ def setBadgesEnabled(enabled: bool): def unloadBadges(): try: - cls = _loaded.get("badges") + cls = _classes.get(_BADGES_CLASS) if cls is not None: _callStatic(cls, "deinit") except Exception as e: logx(f"dexLoader: unloadBadges error: {e}", False) -_OPENFILE_CLASS = "kawaii.packetik.openfile.OpenFileNative" - - def openFileCreate(context, path, text_size_px, pad_l, pad_t, pad_r, pad_b, bg_color, text_color, token_types, token_starts, token_ends, color_keys, color_vals): @@ -117,7 +135,7 @@ def openFileCreate(context, path, text_size_px, pad_l, pad_t, pad_r, pad_b, if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("openfile", _OPENFILE_CLASS, context) + cls = _loadClass(_OPENFILE_CLASS, context) if cls is None: return None from java import jint, jfloat, jarray @@ -138,10 +156,6 @@ def _ia(lst): return None -_CATALOG_CLASS = "kawaii.packetik.catalog.CatalogChromeNative" -_SFX_CLASS = "kawaii.packetik.sfx.SfxNative" - - 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, @@ -153,7 +167,7 @@ def catalogChromeCreate(context, main_bg, card_bg, card_pressed, text_color, if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("catalog", _CATALOG_CLASS, context) + cls = _loadClass(_CATALOG_CLASS, context) if cls is None: return None from java import jint @@ -183,7 +197,7 @@ def catalogIconsChromeCreate(context, main_bg, card_bg, card_pressed, text_color if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("catalog", _CATALOG_CLASS, context) + cls = _loadClass(_CATALOG_CLASS, context) if cls is None: return None from java import jint @@ -210,7 +224,7 @@ def sfxExpandableCreate(context, item_id, text, subtext, checked, collapsed, if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("sfx", _SFX_CLASS, context) + cls = _loadClass(_SFX_CLASS, context) if cls is None: return None from java import jint @@ -229,7 +243,7 @@ def sfxChildCreate(context, item_id, text, checked): if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("sfx", _SFX_CLASS, context) + cls = _loadClass(_SFX_CLASS, context) if cls is None: return None from java import jint @@ -248,7 +262,7 @@ def sfxVolumeSliderCreate(context, initial, title, off_label, maximum_label, if context is None: from org.telegram.messenger import ApplicationLoader context = ApplicationLoader.applicationContext - cls = _loadClass("sfx", _SFX_CLASS, context) + cls = _loadClass(_SFX_CLASS, context) if cls is None: return None from java import jint @@ -264,7 +278,7 @@ def sfxVolumeSliderCreate(context, initial, title, off_label, maximum_label, def openFileCancel(view): try: - cls = _loaded.get("openfile") + cls = _classes.get(_OPENFILE_CLASS) if cls is not None and view is not None: _callStatic(cls, "cancel", view) except Exception as e: @@ -273,7 +287,7 @@ def openFileCancel(view): def openFileGetText(view): try: - cls = _loaded.get("openfile") + cls = _classes.get(_OPENFILE_CLASS) if cls is not None and view is not None: return _callStatic(cls, "getText", view) except Exception as e: diff --git a/packit/src/python/integrations/decorations/Badges.py b/packit/src/python/integrations/decorations/Badges.py index dcbacb0..2a7988c 100644 --- a/packit/src/python/integrations/decorations/Badges.py +++ b/packit/src/python/integrations/decorations/Badges.py @@ -126,7 +126,7 @@ def setup_hooks(self): pass # primary path: precompiled Kotlin dex (config fetch + cache + hooks - # all live in packit/dex//badges.dex, source in /kotlin/) + # all live in packit/dex/packit.dex, source in packit/src/kotlin/) try: from ...core.DexLoader import loadBadges if loadBadges(self.context, enabled): diff --git a/scripts/linux/kotlin-build.sh b/scripts/linux/kotlin-build.sh index dc12d57..a09747d 100644 --- a/scripts/linux/kotlin-build.sh +++ b/scripts/linux/kotlin-build.sh @@ -1,10 +1,16 @@ #!/usr/bin/env bash # SPDX-License-Identifier: GPL-3.0-or-later # -# Builds the self-written Kotlin dexes (sources in /kotlin/) into -# packit/dex/.dex. Reflection + Xposed based, so it compiles against -# android.jar + the compile-only Xposed stubs in kotlin/stubs (never shipped), -# and R8 tree-shakes kotlin-stdlib so the dex stays tiny. +# Builds the self-written Kotlin (sources in packit/src/kotlin) into a single +# packit/dex/packit.dex holding all of kawaii.packetik. Reflection + Xposed +# based, so it compiles against android.jar + the compile-only Xposed stubs in +# packit/src/kotlin/stubs (never shipped), and R8 tree-shakes kotlin-stdlib so +# the dex stays small. +# +# One dex, not one per package: R8 emits a single classes.dex from all of the +# sources anyway, and this script used to copy that same file out four times +# under four names. Four identical 55K blobs shipped, and DexLoader built a +# separate class loader over each — the same bytecode resident four times. # # Toolchain discovery order: # 1. environment variables (ANDROID_HOME / ANDROID_SDK_ROOT, KOTLINC, ...) @@ -13,22 +19,14 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" -SRC_DIR="$REPO_ROOT/kotlin/src" -STUB_DIR="$REPO_ROOT/kotlin/stubs" +SRC_DIR="$REPO_ROOT/packit/src/kotlin/src" +STUB_DIR="$REPO_ROOT/packit/src/kotlin/stubs" OUT_DIR="$REPO_ROOT/kotlin-build" -DEX_OUT_BASE="$REPO_ROOT/packit/dex" +DEX_OUT="$REPO_ROOT/packit/dex/packit.dex" MIN_API=26 -# what to build: "=" -PACKAGES=( - "badges=kawaii.packetik.badges.BadgesNative" - "openfile=kawaii.packetik.openfile.OpenFileNative" - "catalog=kawaii.packetik.catalog.CatalogChromeNative" - "sfx=kawaii.packetik.sfx.SfxNative" -) - die() { echo "error: $*" >&2; exit 1; } info() { echo "[kotlin-build] $*"; } @@ -134,13 +132,14 @@ java -cp "$D8_JAR" com.android.tools.r8.R8 \ "$KOTLIN_STDLIB" [[ -f "$OUT_DIR/dex/classes.dex" ]] || die "R8 produced no classes.dex" +if [[ -f "$OUT_DIR/dex/classes2.dex" ]]; then + die "R8 split the output across several dex files. + packit.dex is loaded as one file, so the method count has to stay under the limit." +fi -# NOTE: .dex is arch-independent Dalvik bytecode; a single copy serves all ABIs. -for entry in "${PACKAGES[@]}"; do - name="${entry%%=*}" - mkdir -p "$DEX_OUT_BASE" - cp "$OUT_DIR/dex/classes.dex" "$DEX_OUT_BASE/$name.dex" - info "-> $DEX_OUT_BASE/$name.dex ($(wc -c < "$DEX_OUT_BASE/$name.dex") bytes)" -done +# NOTE: .dex is arch-independent Dalvik bytecode; one copy serves all ABIs. +mkdir -p "$(dirname "$DEX_OUT")" +cp "$OUT_DIR/dex/classes.dex" "$DEX_OUT" +info "-> $DEX_OUT ($(wc -c < "$DEX_OUT") bytes)" info "done."