diff --git a/AGENTS.md b/AGENTS.md
index 334e5e1..5b83301 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -98,6 +98,10 @@ branch names, credentials, signing material, or other secrets.
- After creating and verifying a release, update the corresponding F-Droid submission/build recipe
when required. The repository's F-Droid-related files are for reproducible verification, not an
excuse to maintain a duplicate unused build path.
+- MegaProxyIdentity SVGs are the sole source for owned artwork; screenshots and the local GitHub
+ download badge are exceptions. Regenerate committed PNG/XML with
+ `bundle exec fastlane android update_graphics [ref:FULL_SHA]`; keep source/output hashes in
+ `assets/branding/identity-lock.json`. Normal builds must remain offline with respect to artwork.
- Store F-Droid listing metadata and current store artwork under `fastlane/metadata/android`.
Obsolete artwork does not need an archive copy in the working tree because Git preserves history.
diff --git a/README.md b/README.md
index 607065f..5079ae6 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
[](https://developer.android.com/about/versions/oreo)
-
+
MegaProxy is an open-source Android VPN client for HTTPS, SOCKS5, MASQUE α (HTTP/3)
diff --git a/app/src/main/res/drawable-hdpi/ic_vpn_notification.png b/app/src/main/res/drawable-hdpi/ic_vpn_notification.png
new file mode 100644
index 0000000..c7fdbb9
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_vpn_notification.png differ
diff --git a/app/src/main/res/drawable-mdpi/ic_vpn_notification.png b/app/src/main/res/drawable-mdpi/ic_vpn_notification.png
new file mode 100644
index 0000000..6ad51ed
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_vpn_notification.png differ
diff --git a/app/src/main/res/drawable-xhdpi/ic_vpn_notification.png b/app/src/main/res/drawable-xhdpi/ic_vpn_notification.png
new file mode 100644
index 0000000..c0fd351
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_vpn_notification.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/ic_vpn_notification.png b/app/src/main/res/drawable-xxhdpi/ic_vpn_notification.png
new file mode 100644
index 0000000..788b021
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_vpn_notification.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/ic_vpn_notification.png b/app/src/main/res/drawable-xxxhdpi/ic_vpn_notification.png
new file mode 100644
index 0000000..8eb65db
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/ic_vpn_notification.png differ
diff --git a/app/src/main/res/drawable/ic_vpn_notification.xml b/app/src/main/res/drawable/ic_vpn_notification.xml
deleted file mode 100644
index 82e7e17..0000000
--- a/app/src/main/res/drawable/ic_vpn_notification.xml
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index be43858..f882cd8 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,5 +1,6 @@
+
-
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
index be43858..f882cd8 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -1,5 +1,6 @@
+
-
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 4e5afda..2cc06bc 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_background.png b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
new file mode 100644
index 0000000..6ad2f93
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_background.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
index f850e5e..81a9d77 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
index 4e5afda..be2c10c 100644
Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
index ef2d792..336214e 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_background.png b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
new file mode 100644
index 0000000..b7c609c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_background.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
index d1c5c40..1a1ccf4 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
index ef2d792..8a02ac4 100644
Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 17cc7ad..a10416d 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..c5d2f00
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
index fa349ec..049ae0e 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
index 17cc7ad..5dd4469 100644
Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index abb52ab..86ccc37 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..0148bbb
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
index 79bfcc7..945c2cc 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
index abb52ab..bd1dcc3 100644
Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 39e3331..3e05f27 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
new file mode 100644
index 0000000..df20bf6
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
index 2709ad9..296fb78 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
index 39e3331..9283b7b 100644
Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/ic_launcher_colors.xml b/app/src/main/res/values/ic_launcher_colors.xml
deleted file mode 100644
index 5d60d0a..0000000
--- a/app/src/main/res/values/ic_launcher_colors.xml
+++ /dev/null
@@ -1,4 +0,0 @@
-
-
- #2725D9
-
diff --git a/assets/branding/IDENTITY-LICENSE b/assets/branding/IDENTITY-LICENSE
new file mode 100644
index 0000000..0e259d4
--- /dev/null
+++ b/assets/branding/IDENTITY-LICENSE
@@ -0,0 +1,121 @@
+Creative Commons Legal Code
+
+CC0 1.0 Universal
+
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
+ HEREUNDER.
+
+Statement of Purpose
+
+The laws of most jurisdictions throughout the world automatically confer
+exclusive Copyright and Related Rights (defined below) upon the creator
+and subsequent owner(s) (each and all, an "owner") of an original work of
+authorship and/or a database (each, a "Work").
+
+Certain owners wish to permanently relinquish those rights to a Work for
+the purpose of contributing to a commons of creative, cultural and
+scientific works ("Commons") that the public can reliably and without fear
+of later claims of infringement build upon, modify, incorporate in other
+works, reuse and redistribute as freely as possible in any form whatsoever
+and for any purposes, including without limitation commercial purposes.
+These owners may contribute to the Commons to promote the ideal of a free
+culture and the further production of creative, cultural and scientific
+works, or to gain reputation or greater distribution for their Work in
+part through the use and efforts of others.
+
+For these and/or other purposes and motivations, and without any
+expectation of additional consideration or compensation, the person
+associating CC0 with a Work (the "Affirmer"), to the extent that he or she
+is an owner of Copyright and Related Rights in the Work, voluntarily
+elects to apply CC0 to the Work and publicly distribute the Work under its
+terms, with knowledge of his or her Copyright and Related Rights in the
+Work and the meaning and intended legal effect of CC0 on those rights.
+
+1. Copyright and Related Rights. A Work made available under CC0 may be
+protected by copyright and related or neighboring rights ("Copyright and
+Related Rights"). Copyright and Related Rights include, but are not
+limited to, the following:
+
+ i. the right to reproduce, adapt, distribute, perform, display,
+ communicate, and translate a Work;
+ ii. moral rights retained by the original author(s) and/or performer(s);
+iii. publicity and privacy rights pertaining to a person's image or
+ likeness depicted in a Work;
+ iv. rights protecting against unfair competition in regards to a Work,
+ subject to the limitations in paragraph 4(a), below;
+ v. rights protecting the extraction, dissemination, use and reuse of data
+ in a Work;
+ vi. database rights (such as those arising under Directive 96/9/EC of the
+ European Parliament and of the Council of 11 March 1996 on the legal
+ protection of databases, and under any national implementation
+ thereof, including any amended or successor version of such
+ directive); and
+vii. other similar, equivalent or corresponding rights throughout the
+ world based on applicable law or treaty, and any national
+ implementations thereof.
+
+2. Waiver. To the greatest extent permitted by, but not in contravention
+of, applicable law, Affirmer hereby overtly, fully, permanently,
+irrevocably and unconditionally waives, abandons, and surrenders all of
+Affirmer's Copyright and Related Rights and associated claims and causes
+of action, whether now known or unknown (including existing as well as
+future claims and causes of action), in the Work (i) in all territories
+worldwide, (ii) for the maximum duration provided by applicable law or
+treaty (including future time extensions), (iii) in any current or future
+medium and for any number of copies, and (iv) for any purpose whatsoever,
+including without limitation commercial, advertising or promotional
+purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
+member of the public at large and to the detriment of Affirmer's heirs and
+successors, fully intending that such Waiver shall not be subject to
+revocation, rescission, cancellation, termination, or any other legal or
+equitable action to disrupt the quiet enjoyment of the Work by the public
+as contemplated by Affirmer's express Statement of Purpose.
+
+3. Public License Fallback. Should any part of the Waiver for any reason
+be judged legally invalid or ineffective under applicable law, then the
+Waiver shall be preserved to the maximum extent permitted taking into
+account Affirmer's express Statement of Purpose. In addition, to the
+extent the Waiver is so judged Affirmer hereby grants to each affected
+person a royalty-free, non transferable, non sublicensable, non exclusive,
+irrevocable and unconditional license to exercise Affirmer's Copyright and
+Related Rights in the Work (i) in all territories worldwide, (ii) for the
+maximum duration provided by applicable law or treaty (including future
+time extensions), (iii) in any current or future medium and for any number
+of copies, and (iv) for any purpose whatsoever, including without
+limitation commercial, advertising or promotional purposes (the
+"License"). The License shall be deemed effective as of the date CC0 was
+applied by Affirmer to the Work. Should any part of the License for any
+reason be judged legally invalid or ineffective under applicable law, such
+partial invalidity or ineffectiveness shall not invalidate the remainder
+of the License, and in such case Affirmer hereby affirms that he or she
+will not (i) exercise any of his or her remaining Copyright and Related
+Rights in the Work or (ii) assert any associated claims and causes of
+action with respect to the Work, in either case contrary to Affirmer's
+express Statement of Purpose.
+
+4. Limitations and Disclaimers.
+
+ a. No trademark or patent rights held by Affirmer are waived, abandoned,
+ surrendered, licensed or otherwise affected by this document.
+ b. Affirmer offers the Work as-is and makes no representations or
+ warranties of any kind concerning the Work, express, implied,
+ statutory or otherwise, including without limitation warranties of
+ title, merchantability, fitness for a particular purpose, non
+ infringement, or the absence of latent or other defects, accuracy, or
+ the present or absence of errors, whether or not discoverable, all to
+ the greatest extent permissible under applicable law.
+ c. Affirmer disclaims responsibility for clearing rights of other persons
+ that may apply to the Work or any use thereof, including without
+ limitation any person's Copyright and Related Rights in the Work.
+ Further, Affirmer disclaims responsibility for obtaining any necessary
+ consents, permissions or other rights required for any use of the
+ Work.
+ d. Affirmer understands and acknowledges that Creative Commons is not a
+ party to this document and has no duty or obligation with respect to
+ this CC0 or use of the Work.
diff --git a/assets/branding/README.md b/assets/branding/README.md
index bb49b25..e89a4b4 100644
--- a/assets/branding/README.md
+++ b/assets/branding/README.md
@@ -1,17 +1,14 @@
-# MegaProxy branding assets
+# Generated MegaProxy artwork
-`megaproxy-launcher-master.png` is the current canonical raster artwork for the MegaProxy logo.
-Use it as the visual source when regenerating derived application, store, or documentation assets.
+[MegaProxyIdentity](https://github.com/andre487/MegaProxyIdentity) SVGs are the source of truth.
+The retired raster masters are archived there under `Generative/AndroidMegaProxy`.
-Derived assets live with the component that consumes them:
+Run `bundle exec fastlane android update_graphics` to regenerate committed Android, store
+and documentation artwork. `identity-lock.json` records the Identity revision, source/output
+SHA-256 hashes and renderer version. `IDENTITY-LICENSE` applies to Identity-derived artwork.
+Normal app builds use committed outputs and never download artwork.
-- Android launcher resources: `app/src/main/res/mipmap-*`
-- Store listing icon and graphics: `fastlane/metadata/android/*/images/`
-- Repository and documentation graphics: `docs/assets/`
+The GitHub badge remains a local exception: `docs/assets/get-it-on-github.svg` and its
+notices stay here; its PNG is generated by the same command. Screenshots are maintained separately.
-## GitHub download badge
-
-`docs/assets/get-it-on-github.svg`, its PNG and notices are copied from AndroidMessage487.
-The SVG contains outlined text; third-party attribution is in
-[get-it-on-github-NOTICES.txt](../../docs/assets/get-it-on-github-NOTICES.txt).
-README and installation guides use the PNG at a display height of 75 pixels.
+Instructions: [English](../../docs/en/graphics.md) · [Русский](../../docs/ru/graphics.md).
diff --git a/assets/branding/identity-lock.json b/assets/branding/identity-lock.json
new file mode 100644
index 0000000..6dc3060
--- /dev/null
+++ b/assets/branding/identity-lock.json
@@ -0,0 +1,52 @@
+{
+ "repository": "https://github.com/andre487/MegaProxyIdentity",
+ "commit": "fc236e43659602c3093726521d7c25bdd3536fe7",
+ "renderer": "rsvg-convert version 2.62.3\n\nlibraries used:\n cairo 1.18.6\n pango 1.58.2\n harfbuzz 14.6.0\n fontconfig 2.18.3",
+ "sources": {
+ "SVG/icon.svg": "193fe4e0848f934a17d095d2adc874430bb5a3b1311b25b4e614ff4918742299",
+ "SVG/icon-radius.svg": "7c276c805dbd641569d838831fcc355e671f9d17e577bcba1be22120978414e4",
+ "SVG/icon-round.svg": "21709d03ca94deb643f27c28765419a88fde0ff93564b347b61879e306e50d81",
+ "SVG/feature-graphic.svg": "cb76a52ba46da5e0ee63821325d5481731f17436059c9c026111d41c37a412a0",
+ "SVG/icon-taskbar.svg": "7ef9047c29cbdd67bf4553f2896f766b18e0c6a4d8b6bcc5c4f6fbbc32f27411",
+ "LICENSE": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499"
+ },
+ "local_sources": {
+ "docs/assets/get-it-on-github.svg": "a3463008b42db57aede368deb5cfb5d4b137c318f04d8fead00118bc64263693"
+ },
+ "outputs": {
+ "app/src/main/res/drawable-hdpi/ic_vpn_notification.png": "7b09c81a50e04a8b69bc67640f1362124b41bc0d7d887d06f46a7d15558e9969",
+ "app/src/main/res/drawable-mdpi/ic_vpn_notification.png": "45e99b714a2e87796fd6e6d791fa78a517587db31cd3439d68ae3716b595e2af",
+ "app/src/main/res/drawable-xhdpi/ic_vpn_notification.png": "c3cd48a947e5a8413ac2bc110a1096e7f8543b616e82525bd2e8e99c5d6d52d0",
+ "app/src/main/res/drawable-xxhdpi/ic_vpn_notification.png": "76e6a1583c280687a738910bea575ff2b32da5ceeb161ba33fd667b1ba9df2b5",
+ "app/src/main/res/drawable-xxxhdpi/ic_vpn_notification.png": "07f566dd7c14ea91b8850663ab625c73f442ab8dc28e5870664ba20dfb147583",
+ "app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml": "c4fe288de664c1d03457499c5e5c060695265f705ee0ab3222668efd7044bbc1",
+ "app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml": "c4fe288de664c1d03457499c5e5c060695265f705ee0ab3222668efd7044bbc1",
+ "app/src/main/res/mipmap-hdpi/ic_launcher.png": "07c3d4c38716577c9f42886e1d85fcc0af921cf56ee674fe4e44f15fa097fd86",
+ "app/src/main/res/mipmap-hdpi/ic_launcher_background.png": "b7bc17dbd5345a7b87c45ea6f1caae55a4dd7b5c283b420361b28b3ce604d2f9",
+ "app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png": "397b4ada2959790f22525dc2d3096e7981dcae39e3a9dfa0def9012cf8b3f74b",
+ "app/src/main/res/mipmap-hdpi/ic_launcher_round.png": "248599c82f2035734a16418794f73bc7399839cf2f5b9a1ffce7fa166128d416",
+ "app/src/main/res/mipmap-mdpi/ic_launcher.png": "a47be352b759e1251049b7cf947ac608c892a2606de5196ceabdbeca622679f4",
+ "app/src/main/res/mipmap-mdpi/ic_launcher_background.png": "06beddd6cb61455d1f8e3003a018141c83683266e9a189900c81ceae3d43784f",
+ "app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png": "d533928a889c31741a2688819c3644658bd953e31af09c7d19bb0dbaa90478c3",
+ "app/src/main/res/mipmap-mdpi/ic_launcher_round.png": "f7636b8f2be5dc929d3ad8f5cc7b347757dc0054612aea9ae5f391cf69f40d54",
+ "app/src/main/res/mipmap-xhdpi/ic_launcher.png": "e3f75e60d2fc048b252fea353468d3b6f9e609a8f68b17100728426619cff52b",
+ "app/src/main/res/mipmap-xhdpi/ic_launcher_background.png": "33e9efa0c5ec99b33e1ed09cfb352f0f88d4a744556176fd60d3dcd8203de2d5",
+ "app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png": "6c65f3a6c30062a33506f25fef78af602c34ac73979d5b803ecb58ccaefe8b1f",
+ "app/src/main/res/mipmap-xhdpi/ic_launcher_round.png": "c35ce610605451931a91f8daed0f29ea0717a1dd3c6516e4093362512c8e5601",
+ "app/src/main/res/mipmap-xxhdpi/ic_launcher.png": "ae5b1f6eb4155b6c0a57cf8d4ff5c1373d6c6cf7b2679ca188343b910136d052",
+ "app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png": "2087baf37a52a2bcb9eb7871f8e8cdd3f6c7ea5d0c6d960bdb177184628c4ebd",
+ "app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png": "a9d96a7e692414a55a954eb684d669eb927711189622f9faf7ee5eba6912cc22",
+ "app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png": "985db1ecd8cd245b3225b7eeda465ecc4dce2c2d736e3f2e11699f780c29574f",
+ "app/src/main/res/mipmap-xxxhdpi/ic_launcher.png": "34d31c5c2a82c08432fba53cd06b9acae09eb704db9759cca102664a56ca45df",
+ "app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png": "63885f095aa8a529a1d03e7219bcf5af6ff8b662e9ebf2bf8a8efdc03ff4eb54",
+ "app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png": "fb47c5ce427b8c8cc0c8435a2f3f5ab6ba6864404986416280f554ad7f225242",
+ "app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png": "ddfdbdd28730b37e127f6f20b46a505f8e63fc490298e1b5e8d2b592541c181a",
+ "assets/branding/IDENTITY-LICENSE": "a2010f343487d3f7618affe54f789f5487602331c0a8d03f49e9a7c547cf0499",
+ "docs/assets/get-it-on-github.png": "6f5cca8d2a224beff6ef2310c406dbda776f6f02ce6deb8791b30675df9e1da6",
+ "docs/assets/social-preview.png": "f435ead9047b3fbe29f856f10db3b1ba7a8cf8801242ba331534008c6aa36527",
+ "fastlane/metadata/android/en-US/images/featureGraphic.png": "6651973cc2b3432d319c131742645d427874944e2b72b5aa9db9c939426fe9b6",
+ "fastlane/metadata/android/en-US/images/icon.png": "0c4b997f0ee660e9ba677df0c30d89647afce787996ba42f156674adf25534bd",
+ "fastlane/metadata/android/ru-RU/images/featureGraphic.png": "6651973cc2b3432d319c131742645d427874944e2b72b5aa9db9c939426fe9b6",
+ "fastlane/metadata/android/ru-RU/images/icon.png": "0c4b997f0ee660e9ba677df0c30d89647afce787996ba42f156674adf25534bd"
+ }
+}
diff --git a/assets/branding/megaproxy-launcher-master.png b/assets/branding/megaproxy-launcher-master.png
deleted file mode 100644
index 8f5eb7c..0000000
Binary files a/assets/branding/megaproxy-launcher-master.png and /dev/null differ
diff --git a/docs/assets/social-preview.png b/docs/assets/social-preview.png
index 8109769..5e72f56 100644
Binary files a/docs/assets/social-preview.png and b/docs/assets/social-preview.png differ
diff --git a/docs/en/fastlane.md b/docs/en/fastlane.md
index 2f30e12..c9433a0 100644
--- a/docs/en/fastlane.md
+++ b/docs/en/fastlane.md
@@ -10,6 +10,7 @@ explicit `JAVA_HOME` fails early. `bundle exec fastlane lanes` lists the checked
| Command | Result |
| --- | --- |
| `bundle exec fastlane android renew_config_schema` | Downloads and pins the current MegaProxyConfig schemas and examples; use `ref:FULL_SHA` for a reviewed revision. |
+| `bundle exec fastlane android update_graphics` | Generates Android/store/docs artwork from MegaProxyIdentity SVGs; use `ref:FULL_SHA` to reproduce a pinned revision. |
| `bundle exec fastlane android python_format` | Formats Python scripts with pinned Black and isort. |
| `bundle exec fastlane android python_tests` | Runs Python unit tests. |
| `bundle exec fastlane android python_checks` | Checks Python formatting/import order and runs unit tests. |
diff --git a/docs/en/graphics.md b/docs/en/graphics.md
new file mode 100644
index 0000000..f5e3ecd
--- /dev/null
+++ b/docs/en/graphics.md
@@ -0,0 +1,45 @@
+# Updating artwork
+
+[Русский](../ru/graphics.md) · [Fastlane commands](fastlane.md)
+
+[MegaProxyIdentity](https://github.com/andre487/MegaProxyIdentity) is the source of truth for
+MegaProxy artwork. Edit its SVGs, then regenerate AndroidMegaProxy's committed PNG/XML outputs.
+The only local graphic source is the GitHub download badge SVG with its notices. Screenshots
+remain separate and are not overwritten. Retired originals, including the old social preview,
+are archived in Identity's `Generative/AndroidMegaProxy`; archived PNGs are not generation inputs.
+
+Install `gh` and authenticate it, install librsvg's `rsvg-convert` (`brew install librsvg` on
+macOS or `sudo apt-get install librsvg2-bin` on Debian/Ubuntu), and use the project's Bundler setup.
+Python uses only the standard library; generation does not require Pillow or ImageMagick.
+
+```sh
+bundle exec fastlane android update_graphics
+# Reproduce a reviewed Identity revision:
+bundle exec fastlane android update_graphics ref:FULL_SHA
+```
+
+The lane calls `scripts/update_graphics.py`. It resolves one Identity commit and downloads all
+inputs at that SHA before rendering. It replaces outputs only after every download and render
+succeeds. Review the images and commit them together with `assets/branding/identity-lock.json`.
+The lock records source/output SHA-256 hashes and the renderer version; use the same librsvg
+version for byte-for-byte reproduction. Normal app/F-Droid builds and CI use committed outputs,
+without downloading Identity or running a renderer.
+
+| Source | Generated outputs |
+| --- | --- |
+| `SVG/icon.svg` | 512×512 store icons and adaptive background/foreground layers |
+| `SVG/icon-radius.svg`, `SVG/icon-round.svg` | Legacy square/round launcher icons for all five densities |
+| `SVG/icon-taskbar.svg` | White alpha-only notification icons at 24dp for all five densities |
+| `SVG/feature-graphic.svg` | 1024×500 EN/RU store feature graphics and a centered 1280×640 social preview |
+| Local `docs/assets/get-it-on-github.svg` | 1292×500 GitHub download badge PNG; local notices are retained |
+
+Adaptive layers retain the SVG's background/rings and keep the wordmark inside Android's safe
+circle by scaling it to 75% of the 108dp foreground canvas. The extraction uses the Identity
+`icon-2` and `Background` layer IDs; a changed SVG structure requires reviewing the recipe.
+The social preview fills its canvas with a small centered crop, without stretching the SVG.
+Identity-derived artwork retains the upstream license in `assets/branding/IDENTITY-LICENSE`;
+the GitHub badge retains its separate notices.
+
+`bundle exec fastlane android python_checks` verifies committed hashes, output dimensions,
+source pinning, layer extraction and preservation of existing files when rendering fails.
+It also rejects untracked-by-manifest artwork outside screenshots and the GitHub badge source.
diff --git a/docs/ru/fastlane.md b/docs/ru/fastlane.md
index 1e1eaba..691bee3 100644
--- a/docs/ru/fastlane.md
+++ b/docs/ru/fastlane.md
@@ -10,6 +10,7 @@ Ruby 3.4.10 (`.ruby-version`), затем выполните `bundle install`.
| Команда | Результат |
| --- | --- |
| `bundle exec fastlane android renew_config_schema` | Загружает и фиксирует схемы и примеры MegaProxyConfig; `ref:FULL_SHA` выбирает проверенную ревизию. |
+| `bundle exec fastlane android update_graphics` | Генерирует графику Android/магазина/документации из SVG MegaProxyIdentity; `ref:FULL_SHA` воспроизводит закреплённую версию. |
| `bundle exec fastlane android python_format` | Форматирует Python зафиксированными Black и isort. |
| `bundle exec fastlane android python_tests` | Запускает Python unit-тесты. |
| `bundle exec fastlane android python_checks` | Проверяет форматирование, порядок импортов и Python-тесты. |
diff --git a/docs/ru/graphics.md b/docs/ru/graphics.md
new file mode 100644
index 0000000..d02b241
--- /dev/null
+++ b/docs/ru/graphics.md
@@ -0,0 +1,45 @@
+# Обновление графики
+
+[English](../en/graphics.md) · [Команды Fastlane](fastlane.md)
+
+[MegaProxyIdentity](https://github.com/andre487/MegaProxyIdentity) — источник истины для графики
+MegaProxy. Правьте SVG там, затем генерируйте PNG/XML, которые коммитятся в AndroidMegaProxy.
+Единственный локальный графический исходник — SVG кнопки GitHub с лицензиями. Скриншоты
+обновляются отдельно и не перезаписываются. Старые оригиналы, включая прежний social preview,
+сохранены в `Generative/AndroidMegaProxy` репозитория Identity; архивные PNG не участвуют в генерации.
+
+Установите `gh` и авторизуйтесь, установите `rsvg-convert` из librsvg (`brew install librsvg` на
+macOS или `sudo apt-get install librsvg2-bin` на Debian/Ubuntu) и настройте Bundler проекта.
+Python использует только стандартную библиотеку; Pillow и ImageMagick не нужны.
+
+```sh
+bundle exec fastlane android update_graphics
+# Воспроизвести проверенную версию Identity:
+bundle exec fastlane android update_graphics ref:FULL_SHA
+```
+
+Lane вызывает `scripts/update_graphics.py`. Скрипт определяет один коммит Identity и получает
+все исходники по этому SHA. Файлы заменяются только после успешного скачивания и рендеринга
+всех изображений. Проверьте результат и закоммитьте его вместе с
+`assets/branding/identity-lock.json`. Манифест содержит SHA-256 исходников и результатов, а также
+версию рендерера; для побайтового воспроизведения используйте ту же версию librsvg.
+Обычные сборки приложения/F-Droid и CI используют готовые файлы из Git, без скачивания Identity
+и без запуска рендерера.
+
+| Исходник | Результат |
+| --- | --- |
+| `SVG/icon.svg` | Иконки магазина 512×512 и слои фона/переднего плана adaptive icon |
+| `SVG/icon-radius.svg`, `SVG/icon-round.svg` | Обычные и круглые launcher-иконки для пяти плотностей |
+| `SVG/icon-taskbar.svg` | Белые значки уведомлений с прозрачностью размером 24dp для пяти плотностей |
+| `SVG/feature-graphic.svg` | Store feature graphic EN/RU 1024×500 и social preview 1280×640 |
+| Локальный `docs/assets/get-it-on-github.svg` | PNG кнопки GitHub 1292×500; локальные лицензии сохраняются |
+
+Adaptive icon сохраняет фон и кольца SVG, а логотип уменьшается до 75% холста 108dp, чтобы
+вписаться в безопасную окружность Android. Извлечение использует ID слоёв `icon-2` и `Background`;
+изменение структуры SVG требует проверки рецепта. Social preview заполняет холст с небольшой
+центральной обрезкой без растягивания SVG. Лицензия Identity сохраняется в
+`assets/branding/IDENTITY-LICENSE`; у кнопки GitHub остаются отдельные лицензии.
+
+`bundle exec fastlane android python_checks` проверяет хеши готовых файлов, размеры, закрепление
+исходников, разделение слоёв и сохранность старых файлов при ошибке рендеринга. Изображения,
+не перечисленные в манифесте, запрещены, кроме скриншотов и SVG кнопки GitHub.
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index af3bc16..1be3e04 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -12,6 +12,11 @@ platform :android do
sh(ENV.fetch("PYTHON", "python3"), File.join(project_root, "scripts/renew_config_schema.py"), "--ref", options.fetch(:ref, "main").to_s)
end
+ desc "Regenerate committed artwork from MegaProxyIdentity SVGs"
+ lane :update_graphics do |options|
+ sh(ENV.fetch("PYTHON", "python3"), File.join(project_root, "scripts/update_graphics.py"), "--ref", options.fetch(:ref, "main").to_s)
+ end
+
desc "Run the native Go tests with the race detector"
lane :native_tests do
report = File.join(project_root, "test-results/native/unit.xml")
diff --git a/fastlane/metadata/android/en-US/images/featureGraphic.png b/fastlane/metadata/android/en-US/images/featureGraphic.png
index be88ec0..f49cfa1 100644
Binary files a/fastlane/metadata/android/en-US/images/featureGraphic.png and b/fastlane/metadata/android/en-US/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/en-US/images/icon.png b/fastlane/metadata/android/en-US/images/icon.png
index e373bff..9e4c54e 100644
Binary files a/fastlane/metadata/android/en-US/images/icon.png and b/fastlane/metadata/android/en-US/images/icon.png differ
diff --git a/fastlane/metadata/android/ru-RU/images/featureGraphic.png b/fastlane/metadata/android/ru-RU/images/featureGraphic.png
index be88ec0..f49cfa1 100644
Binary files a/fastlane/metadata/android/ru-RU/images/featureGraphic.png and b/fastlane/metadata/android/ru-RU/images/featureGraphic.png differ
diff --git a/fastlane/metadata/android/ru-RU/images/icon.png b/fastlane/metadata/android/ru-RU/images/icon.png
index e373bff..9e4c54e 100644
Binary files a/fastlane/metadata/android/ru-RU/images/icon.png and b/fastlane/metadata/android/ru-RU/images/icon.png differ
diff --git a/scripts/tests/test_update_graphics.py b/scripts/tests/test_update_graphics.py
new file mode 100644
index 0000000..f460111
--- /dev/null
+++ b/scripts/tests/test_update_graphics.py
@@ -0,0 +1,131 @@
+import hashlib
+import json
+import struct
+import sys
+import tempfile
+import unittest
+from pathlib import Path
+from types import SimpleNamespace
+from unittest.mock import patch
+
+sys.path.insert(0, str(Path(__file__).parents[1]))
+import update_graphics as m
+
+ICON = b""""""
+
+
+class UpdateGraphicsTest(unittest.TestCase):
+ def github(self, path):
+ self.calls.append(path)
+ if path == "commits/main":
+ return json.dumps({"sha": "a" * 40}).encode()
+ return b"license\n" if "contents/LICENSE?" in path else ICON
+
+ def test_pins_all_sources_and_renders_before_replacing_files(self):
+ for fail in (False, True):
+ with self.subTest(fail=fail), tempfile.TemporaryDirectory() as temporary:
+ root = Path(temporary)
+ badge = root / m.BADGE
+ badge.parent.mkdir(parents=True)
+ badge.write_bytes(ICON)
+ target = root / "docs/assets/social-preview.png"
+ target.write_bytes(b"existing")
+ retired = root / "assets/branding/megaproxy-launcher-master.png"
+ retired.parent.mkdir(parents=True)
+ retired.write_bytes(b"original")
+ self.calls = []
+ sizes = []
+
+ def render(content, width, height, directory):
+ sizes.append((width, height))
+ if fail and len(sizes) == 3:
+ raise RuntimeError("render failed")
+ return b"PNG" + struct.pack(">II", width, height) + content
+
+ with (
+ patch.object(m, "github", self.github),
+ patch.object(m.shutil, "which", return_value="rsvg-convert"),
+ patch.object(
+ m.subprocess,
+ "run",
+ return_value=SimpleNamespace(stdout="rsvg-convert test\n"),
+ ),
+ patch.object(m, "render", render),
+ ):
+ if fail:
+ with self.assertRaises(RuntimeError):
+ m.update("main", root)
+ self.assertEqual(b"existing", target.read_bytes())
+ self.assertTrue(retired.exists())
+ self.assertFalse((root / m.LOCK).exists())
+ continue
+ m.update("main", root)
+ lock = json.loads((root / m.LOCK).read_text())
+ self.assertEqual("a" * 40, lock["commit"])
+ self.assertEqual("commits/main", self.calls[0])
+ self.assertTrue(
+ all(path.endswith("?ref=" + "a" * 40) for path in self.calls[1:])
+ )
+ self.assertEqual(len(m.SOURCES) + 1, len(self.calls))
+ self.assertIn((1280, 640), sizes)
+ self.assertIn((432, 432), sizes)
+ self.assertFalse(retired.exists())
+ for path, checksum in lock["outputs"].items():
+ self.assertEqual(checksum, m.digest((root / path).read_bytes()))
+
+ def test_adaptive_layers_preserve_center_and_reject_changed_structure(self):
+ background, foreground = map(m.svg, m.adaptive_layers(ICON))
+ self.assertIsNone(background.find(f".//*[@id='logo']"))
+ self.assertIsNone(foreground.find(f".//*[@id='Background']"))
+ self.assertIsNotNone(foreground.find(f".//*[@id='logo']"))
+ self.assertEqual("-20 -20 160 160", foreground.get("viewBox"))
+ with self.assertRaises(ValueError):
+ m.adaptive_layers(ICON.replace(b'id="Background"', b'id="renamed"'))
+ with self.assertRaises(ValueError):
+ m.svg(
+ b''
+ )
+
+ def test_committed_artwork_has_exact_recorded_bytes_and_no_extra_masters(self):
+ lock = json.loads((m.ROOT / m.LOCK).read_text())
+ self.assertRegex(lock["commit"], r"^[a-f0-9]{40}$")
+ for path, checksum in (lock["outputs"] | lock["local_sources"]).items():
+ self.assertEqual(
+ checksum, hashlib.sha256((m.ROOT / path).read_bytes()).hexdigest(), path
+ )
+ artwork = {
+ str(path.relative_to(m.ROOT))
+ for folder in (
+ "assets",
+ "docs/assets",
+ "fastlane/metadata",
+ "app/src/main/res",
+ )
+ for path in (m.ROOT / folder).rglob("*")
+ if path.suffix.lower() in (".png", ".svg", ".webp", ".jpg", ".jpeg", ".ico")
+ and "phoneScreenshots" not in path.parts
+ }
+ self.assertEqual(
+ artwork,
+ {path for path in lock["outputs"] if path.endswith(".png")}
+ | set(lock["local_sources"]),
+ )
+ for density, scale in m.DENSITIES.items():
+ for name, size in (
+ ("ic_launcher", 48),
+ ("ic_launcher_round", 48),
+ ("ic_launcher_foreground", 108),
+ ("ic_launcher_background", 108),
+ ):
+ data = (
+ m.ROOT / f"app/src/main/res/mipmap-{density}/{name}.png"
+ ).read_bytes()
+ self.assertEqual(
+ (int(size * scale),) * 2, struct.unpack(">II", data[16:24])
+ )
+ data = (
+ m.ROOT / f"app/src/main/res/drawable-{density}/ic_vpn_notification.png"
+ ).read_bytes()
+ self.assertEqual((int(24 * scale),) * 2, struct.unpack(">II", data[16:24]))
diff --git a/scripts/update_graphics.py b/scripts/update_graphics.py
new file mode 100644
index 0000000..aa93dc7
--- /dev/null
+++ b/scripts/update_graphics.py
@@ -0,0 +1,231 @@
+#!/usr/bin/env python3
+"""Generate committed Android/store/docs artwork from pinned MegaProxyIdentity SVGs."""
+
+import argparse
+import hashlib
+import json
+import re
+import shutil
+import struct
+import subprocess
+import tempfile
+import xml.etree.ElementTree as ET
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+REPOSITORY = "andre487/MegaProxyIdentity"
+LOCK = "assets/branding/identity-lock.json"
+BADGE = "docs/assets/get-it-on-github.svg"
+SOURCES = (
+ "SVG/icon.svg",
+ "SVG/icon-radius.svg",
+ "SVG/icon-round.svg",
+ "SVG/feature-graphic.svg",
+ "SVG/icon-taskbar.svg",
+ "LICENSE",
+)
+DENSITIES = {"mdpi": 1, "hdpi": 1.5, "xhdpi": 2, "xxhdpi": 3, "xxxhdpi": 4}
+SVG = "http://www.w3.org/2000/svg"
+ET.register_namespace("", SVG)
+ET.register_namespace("xlink", "http://www.w3.org/1999/xlink")
+
+
+def digest(content):
+ return hashlib.sha256(content).hexdigest()
+
+
+def github(path):
+ return subprocess.run(
+ [
+ "gh",
+ "api",
+ f"repos/{REPOSITORY}/{path}",
+ "-H",
+ "Accept: application/vnd.github.raw+json",
+ ],
+ check=True,
+ capture_output=True,
+ timeout=60,
+ ).stdout
+
+
+def svg(content):
+ root = ET.fromstring(content)
+ if root.tag != f"{{{SVG}}}svg":
+ raise ValueError("Expected an SVG document")
+ for node in root.iter():
+ if node.tag == f"{{{SVG}}}image":
+ raise ValueError("Embedded raster artwork is not a vector source")
+ for key, value in node.attrib.items():
+ if key.rsplit("}", 1)[-1] == "href" and not value.startswith("#"):
+ raise ValueError("SVG sources must be self-contained")
+ return root
+
+
+def adaptive_layers(content):
+ background, foreground = svg(content), svg(content)
+ for root, keep_background in ((background, True), (foreground, False)):
+ artwork = root.find(f"{{{SVG}}}g[@id='icon-2']")
+ if artwork is None or artwork.find(f"{{{SVG}}}g[@id='Background']") is None:
+ raise ValueError(
+ "Identity icon layers changed; review the adaptive icon recipe"
+ )
+ for child in list(artwork):
+ if (child.get("id") == "Background") != keep_background:
+ artwork.remove(child)
+ # Keep the complete wordmark inside Android's 66dp safe circle on the 108dp layer.
+ x, y, width, height = map(float, foreground.get("viewBox").split())
+ foreground.set(
+ "viewBox",
+ f"{x - width / 6:g} {y - height / 6:g} {width / .75:g} {height / .75:g}",
+ )
+ return ET.tostring(background), ET.tostring(foreground)
+
+
+def social_preview(content):
+ artwork = svg(content)
+ artwork.set("width", "1280")
+ artwork.set("height", "640")
+ artwork.set("preserveAspectRatio", "xMidYMid slice")
+ canvas = ET.Element(f"{{{SVG}}}svg", {"viewBox": "0 0 1280 640"})
+ canvas.append(artwork)
+ return ET.tostring(canvas)
+
+
+def render(content, width, height, directory):
+ source, output = directory / "source.svg", directory / "render.png"
+ source.write_bytes(content)
+ subprocess.run(
+ [
+ "rsvg-convert",
+ "--width",
+ str(width),
+ "--height",
+ str(height),
+ "--output",
+ str(output),
+ str(source),
+ ],
+ check=True,
+ capture_output=True,
+ timeout=60,
+ )
+ data = output.read_bytes()
+ if data[:8] != b"\x89PNG\r\n\x1a\n" or struct.unpack(">II", data[16:24]) != (
+ width,
+ height,
+ ):
+ raise ValueError("Renderer returned an unexpected PNG size")
+ return data
+
+
+def update(ref="main", root=ROOT):
+ if not re.fullmatch(r"[A-Za-z0-9._/-]+", ref):
+ raise ValueError("Expected an Identity commit or branch name")
+ if not shutil.which("rsvg-convert"):
+ raise RuntimeError("Install librsvg (rsvg-convert) before updating graphics")
+ commit = (
+ ref
+ if re.fullmatch(r"[a-f0-9]{40}", ref)
+ else json.loads(github(f"commits/{ref}"))["sha"]
+ )
+ if not re.fullmatch(r"[a-f0-9]{40}", commit):
+ raise ValueError("GitHub returned an invalid commit")
+ sources = {name: github(f"contents/{name}?ref={commit}") for name in SOURCES}
+ local_badge = (root / BADGE).read_bytes()
+ for name, content in sources.items():
+ if name.endswith(".svg"):
+ svg(content)
+ svg(local_badge)
+ background, foreground = adaptive_layers(sources["SVG/icon.svg"])
+ outputs = {"assets/branding/IDENTITY-LICENSE": sources["LICENSE"]}
+ with tempfile.TemporaryDirectory(prefix="megaproxy-graphics-") as temporary:
+ directory = Path(temporary)
+
+ def png(path, content, width, height):
+ outputs[path] = render(content, width, height, directory)
+
+ for density, scale in DENSITIES.items():
+ folder = f"app/src/main/res/mipmap-{density}"
+ for name, source in (
+ ("ic_launcher", "icon-radius"),
+ ("ic_launcher_round", "icon-round"),
+ ):
+ size = int(48 * scale)
+ png(f"{folder}/{name}.png", sources[f"SVG/{source}.svg"], size, size)
+ size = int(108 * scale)
+ png(f"{folder}/ic_launcher_background.png", background, size, size)
+ png(f"{folder}/ic_launcher_foreground.png", foreground, size, size)
+ size = int(24 * scale)
+ png(
+ f"app/src/main/res/drawable-{density}/ic_vpn_notification.png",
+ sources["SVG/icon-taskbar.svg"],
+ size,
+ size,
+ )
+ icon = render(sources["SVG/icon.svg"], 512, 512, directory)
+ feature = render(sources["SVG/feature-graphic.svg"], 1024, 500, directory)
+ for language in ("en-US", "ru-RU"):
+ outputs[f"fastlane/metadata/android/{language}/images/icon.png"] = icon
+ outputs[
+ f"fastlane/metadata/android/{language}/images/featureGraphic.png"
+ ] = feature
+ png(
+ "docs/assets/social-preview.png",
+ social_preview(sources["SVG/feature-graphic.svg"]),
+ 1280,
+ 640,
+ )
+ png("docs/assets/get-it-on-github.png", local_badge, 1292, 500)
+ adaptive = b"""
+
+
+
+
+
+"""
+ for name in ("ic_launcher", "ic_launcher_round"):
+ outputs[f"app/src/main/res/mipmap-anydpi-v26/{name}.xml"] = adaptive
+ version = subprocess.run(
+ ["rsvg-convert", "--version"],
+ check=True,
+ capture_output=True,
+ text=True,
+ timeout=10,
+ ).stdout.strip()
+ lock = {
+ "repository": f"https://github.com/{REPOSITORY}",
+ "commit": commit,
+ "renderer": version,
+ "sources": {name: digest(content) for name, content in sources.items()},
+ "local_sources": {BADGE: digest(local_badge)},
+ "outputs": {name: digest(content) for name, content in sorted(outputs.items())},
+ }
+ outputs[LOCK] = (json.dumps(lock, indent=2) + "\n").encode()
+ # Complete every download/render before replacing any committed asset.
+ for name, content in outputs.items():
+ target = root / name
+ target.parent.mkdir(parents=True, exist_ok=True)
+ temporary = target.with_name(target.name + ".tmp")
+ temporary.write_bytes(content)
+ temporary.replace(target)
+ for obsolete in (
+ "assets/branding/megaproxy-launcher-master.png",
+ "app/src/main/res/drawable/ic_vpn_notification.xml",
+ "app/src/main/res/values/ic_launcher_colors.xml",
+ ):
+ (root / obsolete).unlink(missing_ok=True)
+ print(
+ f"Generated {len(outputs) - 1} assets from MegaProxyIdentity {commit}. Review and commit the outputs and {LOCK}."
+ )
+
+
+def main():
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--ref", default="main")
+ args = parser.parse_args()
+ update(args.ref)
+
+
+if __name__ == "__main__":
+ main()