diff --git a/.github/workflows/build_all.yml b/.github/workflows/build_all.yml
index 1557f6bb56..50a68253b5 100644
--- a/.github/workflows/build_all.yml
+++ b/.github/workflows/build_all.yml
@@ -66,7 +66,7 @@ jobs:
- name: Export vars
run: |
- python -u resources/scripts/resolve_paths.py -p >> $GITHUB_ENV
+ python -u resources/scripts/shared.py -p >> $GITHUB_ENV
due_on=$(TZ=UTC date --iso-8601=seconds -d "$(date +%Y-%m-01) +1 month") >> $GITHUB_ENV
- name: Install Python deps
@@ -95,7 +95,7 @@ jobs:
- name: Add icons
run: |
- python -u ${{ env.sd }}/add_icons_wrapper.py
+ python -u ${{ env.sd }}/process_icons.py
- name: Set icons count
run: |
@@ -112,14 +112,6 @@ jobs:
echo 'No release tag or changed files found, skip optimizing'
fi
- - name: Sort XMLs
- run: |
- cd ${{ env.sd }}
- python sort_appfilter.py -o
- python sort_drawable.py -o
- cp -fv ${{ env.a1 }} ${{ env.a2 }}
- cp -fv ${{ env.d1 }} ${{ env.d2 }}
-
- name: Create changelog
run: |
mkdir -v changelog
@@ -188,10 +180,7 @@ jobs:
EOF
- python ${{ env.sd }}/sort_appfilter.py -o
- python ${{ env.sd }}/sort_drawable.py -o
- cp -fv ${{ env.a1 }} ${{ env.a2 }}
- cp -fv ${{ env.d1 }} ${{ env.d2 }}
+ python -u ${{ env.sd }}/process_icons.py -s
bash gradlew assembleFossdc
git restore ${{ env.d1 }} ${{ env.d2 }}
diff --git a/.github/workflows/build_foss.yml b/.github/workflows/build_foss.yml
index 985b990f9d..56f2f91fcc 100644
--- a/.github/workflows/build_foss.yml
+++ b/.github/workflows/build_foss.yml
@@ -47,7 +47,7 @@ jobs:
- name: Export vars
run: |
- python -u resources/scripts/resolve_paths.py -p >> $GITHUB_ENV
+ python -u resources/scripts/shared.py -p >> $GITHUB_ENV
- name: Install Python deps
run: |
@@ -55,7 +55,7 @@ jobs:
- name: Add icons
run: |
- python -u ${{ env.sd }}/add_icons_wrapper.py
+ python -u ${{ env.sd }}/process_icons.py
- name: Set version
run: |
diff --git a/.github/workflows/check_conflicts.yml b/.github/workflows/check_conflicts.yml
index ec916e4556..f19366a9ec 100644
--- a/.github/workflows/check_conflicts.yml
+++ b/.github/workflows/check_conflicts.yml
@@ -5,19 +5,24 @@ on:
workflow_dispatch:
pull_request:
paths:
- - contribs/**
+ - contribs/icons/**
+ - contribs/icons.yml
+
+env:
+ FORCE_COLOR: 1
+ PYTHONUNBUFFERED: 1
jobs:
build:
name: Check for Conflicts
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
defaults:
run:
working-directory: resources/scripts
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
- name: Setup Python
uses: actions/setup-python@v6
@@ -29,6 +34,6 @@ jobs:
run: |
pip install -r requirements.txt
- - name: Add icons
+ - name: Process icons in dry run
run: |
- python -u add_icons_wrapper.py
+ python process_icons.py -d 2>&1
diff --git a/.github/workflows/update_requests.yml b/.github/workflows/update_requests.yml
index 86dce31116..a50c002297 100644
--- a/.github/workflows/update_requests.yml
+++ b/.github/workflows/update_requests.yml
@@ -10,17 +10,20 @@ permissions:
on:
workflow_dispatch:
+env:
+ PYTHONUNBUFFERED: 1
+
jobs:
update:
name: Update Requests
- runs-on: ubuntu-22.04
+ runs-on: ubuntu-24.04
defaults:
run:
working-directory: resources/scripts
steps:
- name: Checkout
- uses: actions/checkout@v5
+ uses: actions/checkout@v6
with:
token: ${{ secrets.PA_TOKEN }}
@@ -34,20 +37,12 @@ jobs:
run: |
pip install -r requirements.txt
- - name: Dump emails
- run: |
- python -u email_dumper.py \
- -u '${{ secrets.EMAIL_ADDRESS }}' \
- -p '${{ secrets.EMAIL_PASSWORD }}' \
- -r '${{ secrets.EMAIL_FOLDER }}'
-
- - name: Parse emails
- run: |
- python -u email_parser.py
-
- - name: Parse requests
+ - name: Process emails
run: |
- python -u requests_parser.py -r
+ python process_emails.py -p -d --unread \
+ --user '${{ secrets.EMAIL_ADDRESS }}' \
+ --pass '${{ secrets.EMAIL_PASSWORD }}' \
+ --remote '${{ secrets.EMAIL_FOLDER }}' \
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v7
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7723aaa194..96aad33786 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,344 +1,448 @@
-# Introduction
+# ℹ️ Introduction
-## Requirements
+> Feel free to ask for help on [the Discord server](https://discord.gg/F9RFqHN) if anything is unclear
-In case you wanna contribute to Delta you need:
+Hi! I see you've decided to contribute. That's great and there are a few ways to do so:
-- basic knowledge of [`git`](https://git-scm.com/)
-- a fork of this repo
-- an SVG icon
-- an 192x192px PNG icon
-- [ComponentInfo](#gathering-componentinfo)(s) of the target app
+If you know how to make cool icons but don't know how to work with [git](https://git-scm.com/), [GitHub](https://github.com), and other tech stuff, just read [Design Guidelines](#-design-guidelines) carefully, make some icons and submit them on our Discord in the [icon review channel](https://discord.com/channels/743783969216135198/743784395856412782).
-## Info
+If you don't know how to make icons but you are tech-savvy, you can help improve our CI/CD, add missing ComponentInfos, and do other things. Read [Contributing](#-contributing) for more info.
-We have two methods of adding icons:
- - [Auto method](#auto-method) is a new method of adding icons. You only need to add PNG and SVG to a specific folder and append the icon name with ComponentInfo(s) to a specific YAML file. These files will be automatically handled by our CI/CD every release.
- - [Manual method](#manual-method) implies editing four XML files and adding icons to two specific folders.
+If you can handle tech and design stuff, then please read this guide completely.
-We also have such a thing as alternative icons — we mainly use it to move an existing icon to an alternative one after rebranding, but there's nothing stopping you to make alternative icons for any app in different shapes as you wish. You can select an alternative icon for the target app via your icon launcher if it supports that feature.
+If you don't know any of that but want to try to make icons, we can help with some advice on our Discord. There are many tools, instructions, and guides for them that it’s impossible to describe everything, so the only thing you can do is search for information on the Internet and try it out.
-Description of categories and what they are for:
+In short, the suggested skills are:
-- `New`: for new icons obviously (new icons always must be duplicated there)
-- `Alts`: for alternative icons
-- `Calendar`: for calendar icons
-- `Folders`: for folder icons
-- `Google`: for Google apps (Chrome, YouTube, etc.)
-- `System`: for system icons (Camera, Settings, etc.)
-- `#`: icons whose name starts with a number (or, to be more clear, with an underscore followed by a number, e.g. `_2048`)
-- `A-Z`: icons which don't fit in previous categories must be placed in a category based on the first letter of its name
+- Basic knowledge of [git](https://git-scm.com/) and/or [GitHub](https://github.com).
+- Some experience with vector editors.
+- A little bit of a design sense.
-## Rules
+We'll be happy to see you participate!
-- Keep `LF` line endings in files (`CLRF` line endings make GitHub think the entire file has been modified, and they may break our CI/CD)
-- SVG, PNG and drawable names must be the same
-- Keep filenames in alphanumeric lowercase with underscores
-- If the icon name starts with a number, it must have a leading underscore (e.g. `_9gag`) and be placed in `#` category
-- Keep the next naming format of alternative icons: `new_icon_alt_x`, where `x` is a number of current alternative version (yes, we have multiple of them, e.g. `old_icon_alt_1`, `old_icon_alt_2`, etc.
+# 📝 Design Guidelines
-# Contributing
+## General Tips
-> _`new_icon` will be used as the icon name_
-> _`new_icon_alt_1` will be used as the alternative icon for `new_icon`_
-> _`com.example/com.example.MainActivity` will be used as the 1st ComponentInfo for `new_icon`_
-> _`com.example/com.example.StartActivity` will be used as the 2nd ComponentInfo for `new_icon`_
+- Keep it simple: fewer details and small elements, because the end result will only be a small element on the users screens.
+- Your icon doesn't have to be a 1:1 copy of the original; improve and simplify where possible but at the same time try to maintain a recognizable appearance.
+- Avoid outlining, otherwise the icon will stand out from the general style.
+- Make the icon free-form if you can — it helps keep Delta a little more diverse.
+- You can search for app logos online (they're often found on official websites; avoid icons with non-free licenses) and adapt them. If the original icon is too complex, you can use another recognizable element (an item, a faction icon, etc.) — this applies to any complex icon, not just games.
+- Be sure to double-check that icons are centered and aligned, sized and exported correctly.
-Don't forget to give yourself an entry at the bottom of [`app/src/main/res/xml/contributors.xml`](./app/src/main/res/xml/contributors.xml) if this is your first contribution!
+## Icon Template
-## Auto Method
+### Rules
-> **This method is only for adding new icons or linking ComponentInfo(s) with existing icons!**
+- Canvas size must be 192x192px. The template from [Resources](#resources) is correctly configured, just download it and you should be good to go.
+- The icon size does not exceed template dimensions. Check out [the visual explanation](./resources/templates/template_tutorial.svg).
+- If the original logo is simple and doesn't fill most of the template as a shape (circle, square, etc.), keep the logo size between 73–80px.
+- The rounded corners of squares and rectangles have a corner radius of 10px.
+- The template must be properly centered on the canvas.
-1. Add `new_icon.svg` and `new_icon.png` to [`contribs/icons`](./contribs/icons) directory
+### Tips
-2. Append the icon name with ComponentInfo(s) to [`contribs/icons.yml`](./contribs/icons.yml) with any of the next formats:
+- If you're having trouble deciding whether to use geometric or optical centering, you can discuss it on Discord. Mostly it depends on the icon, but optical centering is usually your choice.
+- If you have any doubts about the design of your icon, you can also discuss it on Discord.
- 2.1. The new icon with the ComponentInfo.
+### Resources
- > Can be used for linking the ComponentInfo with the existing icon
+> You can also check out the [Figma icon template](https://www.figma.com/design/02aiFRSLkikcw8mpBAnoDA/Delta-Icon-Template?m=auto&t=qyLH05AMDzZwAI2s-1), if you're using Figma.
- ```yaml
- # lines omitted for example
+
- new_icon:
- - com.example/com.example.MainActivity
- ```
+## Colors
- 2.2. The new icon with multiple ComponentInfos:
- > Can be used for linking ComponentInfos with the existing icon
- ```yaml
- # lines omitted for example
+### Rules
- new_icon:
- - com.example/com.example.MainActivity
- - com.example/com.example.StartActivity
- ```
+- Use $\textcolor{#56595B}{\textsf{⬤}}$ #56595B Davy's grey as default black.
- 2.3. The new icon with a custom category:
- > For example, if your icon named as `pixel_buds` but you want it to go in `Google` category.
-
- > Note that you don't need to specify `New` category, it will be automatically copied to it, and you don't need to specify category if this is a named icon, _e.g._ `N` category is redundant for `new_icon`, the script will place it to `N` category automatically based on the first letter of drawable name.
-
- ```yaml
- new_icon:
- category: 'Google'
- compinfos:
- - com.example/com.example.MainActivity
- - com.example/com.example.StartActivity
- ```
+- Use $\textcolor{#FF837D}{\textsf{⬤}}$ #FF837D Coral pink as default red and $\textcolor{#BA6561}{\textsf{⬤}}$ #BA6561 Fuzzy Wuzzy as default dark red. Shades of red are specifically for shading purposes and complex arrangements (if we're honest it's mostly complex anime / game icons overusing pink).
- 2.4. The icon without the ComponentInfo (the alternative icon):
+- Transparencies can be used as an overlay for additional shading. Please keep the use of them to a minimum. Try to get by with basic colors and greys as much as possible. If you do use transparency, it should be an overlay, never a background (the overall shape of the icon should not contain any semi-transparent areas).
- ```yaml
- # lines omitted for example
+- Gradients are more acceptable than transparencies, but the usage should still be kept to a sensible minimum (as noted above).
- new_icon_alt_1: {}
- ```
+### Palette
-And we're done! Repeat the process for adding new icons.
+
+
+
+ Greys
+ Basic
+ Reds
+ Skintones
+
+
+
+
+
+ $\textcolor{#FFFFFF}{\textsf{⬤}}$ #FFFFFF White
+ $\textcolor{#ECECEC}{\textsf{⬤}}$ #ECECEC Isabelline
+ $\textcolor{#D8D8D8}{\textsf{⬤}}$ #D8D8D8 Timberwolf
+ $\textcolor{#D2D2D2}{\textsf{⬤}}$ #D2D2D2 Light gray
+ $\textcolor{#CCCCCC}{\textsf{⬤}}$ #CCCCCC Pastel gray
+ $\textcolor{#B1B5BD}{\textsf{⬤}}$ #B1B5BD Ash grey
+ $\textcolor{#A0A5AF}{\textsf{⬤}}$ #A0A5AF Dark gray
+ $\textcolor{#979797}{\textsf{⬤}}$ #979797 Manatee
+ $\textcolor{#83868C}{\textsf{⬤}}$ #83868C Taupe gray
+ $\textcolor{#56595B}{\textsf{⬤}}$ #56595B Davy's grey
+ $\textcolor{#4A4A4A}{\textsf{⬤}}$ #4A4A4A Quartz
+ $\textcolor{#000000}{\textsf{⬤}}$ #000000 Black
+
+
+ $\textcolor{#FFD6D4}{\textsf{⬤}}$ #FFD6D4 Pastel pink
+ $\textcolor{#FF837D}{\textsf{⬤}}$ #FF837D Coral pink
+ $\textcolor{#BA6561}{\textsf{⬤}}$ #BA6561 Fuzzy Wuzzy
+ $\textcolor{#D3B69A}{\textsf{⬤}}$ #D3B69A Tan
+ $\textcolor{#8E6F60}{\textsf{⬤}}$ #8E6F60 Shadow
+ $\textcolor{#FCECDC}{\textsf{⬤}}$ #FCECDC Antique white
+ $\textcolor{#F8C18C}{\textsf{⬤}}$ #F8C18C Pale gold
+ $\textcolor{#FDF5D9}{\textsf{⬤}}$ #FDF5D9 Cornsilk
+ $\textcolor{#F9DE81}{\textsf{⬤}}$ #F9DE81 Jasmine
+ $\textcolor{#C39A54}{\textsf{⬤}}$ #C39A54 Camel
+ $\textcolor{#E0F4E0}{\textsf{⬤}}$ #E0F4E0 Platinum
+ $\textcolor{#98DC9A}{\textsf{⬤}}$ #98DC9A Granny Smith Apple
+ $\textcolor{#71A372}{\textsf{⬤}}$ #71A372 Asparagus
+ $\textcolor{#96DFD3}{\textsf{⬤}}$ #96DFD3 Pale robin egg blue
+ $\textcolor{#73ADA4}{\textsf{⬤}}$ #73ADA4 Cadet blue
+ $\textcolor{#9ABEFF}{\textsf{⬤}}$ #9ABEFF Baby blue eyes
+ $\textcolor{#728DBE}{\textsf{⬤}}$ #728DBE Dark pastel blue
+ $\textcolor{#54688C}{\textsf{⬤}}$ #54688C UCLA Blue
+ $\textcolor{#ABABFF}{\textsf{⬤}}$ #ABABFF Baby blue eyes
+ $\textcolor{#BD9AFF}{\textsf{⬤}}$ #BD9AFF Bright lavender
+ $\textcolor{#8C72BD}{\textsf{⬤}}$ #8C72BD Ube
+
+
+ $\textcolor{#FFB0AC}{\textsf{⬤}}$ #FFB0AC Melon
+ $\textcolor{#F58F8A}{\textsf{⬤}}$ #F58F8A Light coral
+ $\textcolor{#F4806D}{\textsf{⬤}}$ #F4806D Coral pink
+ $\textcolor{#E85E5C}{\textsf{⬤}}$ #E85E5C Terra cotta
+ $\textcolor{#DC505E}{\textsf{⬤}}$ #DC505E Dark terra cotta
+ $\textcolor{#B02A3C}{\textsf{⬤}}$ #B02A3C Deep carmine
+ $\textcolor{#7A1B1C}{\textsf{⬤}}$ #7A1B1C Falu red
+ $\textcolor{#511119}{\textsf{⬤}}$ #511119 Dark scarlet
+
+
+ $\textcolor{#F1E9E0}{\textsf{⬤}}$ #F1E9E0 Eggshell
+ $\textcolor{#D6C8BA}{\textsf{⬤}}$ #D6C8BA Pastel gray
+ $\textcolor{#D4C6B8}{\textsf{⬤}}$ #D4C6B8 Pale silver
+ $\textcolor{#D7D0B8}{\textsf{⬤}}$ #D7D0B8 Pastel gray
+ $\textcolor{#E2C9B0}{\textsf{⬤}}$ #E2C9B0 Desert sand
+ $\textcolor{#D4B79A}{\textsf{⬤}}$ #D4B79A Tan
+ $\textcolor{#BF9E73}{\textsf{⬤}}$ #BF9E73 Camel
+
+
+
+
+
+### Resources
+
+#### Vector Palettes
+
+-
+ Simplified Palette
+
+
+
-## Manual Method
+-
+ Full Palette
+
+
+
-1. Add `new_icon.svg` to [`resources/vectors`](./resources/vectors) directory
+#### Vector Editors
-2. Add `new_icon.png` to [`app/src/main/res/drawable-nodpi`](./app/src/main/res/drawable-nodpi)
+- [Adobe Swatch Exchange Palette](./resources/palettes/palette.ase) (Illustrator)
+- [GPL Palette](./resources/palettes/palette.gpl) (Inkscape, Karbon)
-3. Append the line ` ` in `New` and named categories (the named category is based on the first letter of the icon name; `N` in our case) to [`app/src/main/assets/drawable.xml`](./app/src/main/assets/drawable.xml) and [`app/src/main/res/xml/drawable.xml`](./app/src/main/res/xml/drawable.xml). How it should look:
+## Font
- ```xml
-
-
-
- -
-
-
-
-
+### Tips
-
-
- -
-
-
-
-
- ```
- > You can edit one file and overwrite another with it to keep them identical.
+- If the original icon consists of just one or two letters, you may trace that letter instead of using these fonts.
+- Fonts can be a little tricky to align/center in different vector editors, which you can mitigate by either converting them to paths or in the case of Figma: the text box trim.
+- You can use a custom font if it matches the font from the original icon very closely, for the rest use fonts from [Resources](#resources-2).
-4. Append the line ` ` to [`app/src/main/assets/appfilter.xml`](./app/src/main/assets/appfilter.xml) and [`app/src/main/res/xml/appfilter.xml`](./app/src/main/res/xml/appfilter.xml). How it should look:
+### Resources
- ```xml
-
-
-
-
- ```
- > You can edit one file and overwrite another with it to keep them identical.
+- [Now](https://www.1001fonts.com/now-font.html?text=Delta%20Icons) — main Sans-serif font; Now Alt from the same family can be used for an alternate lowercase 'a' letter.
+- [Aleo](https://www.1001fonts.com/aleo-font.html?text=Delta%20Icons) — when Serifs are needed.
-The end. More complicated than Auto method, but it's a base method, you can modify/fix current icons by this method.
+# 📥 Contributing
-## Other Cases
+## Overview
-### Alternative Icons
+### Key Terms
-If the existing icon rebranded, don't overwrite it with a new one, do the following:
+- **Icon images** — your exported PNG/SVG icons.
+- **ComponentInfo** — an app identifier (e.g. `com.example/com.example.MainActivity`) that launchers use to match an installed app to its icon in the icon pack. You can use these tools to get ComponentInfos from your installed apps:
-> `old_icon` will be used as an existing icon name
+ - [Icon Pusher](https://iconpusher.com/) by [V01D](https://v01d.uk)
+ - [Icon Request](https://github.com/Kaiserdragon2/IconRequest/releases) by [Kaiserdragon2](https://github.com/Kaiserdragon2)
-> `old_icon_alt_1` will be used as an alternative icon name for the existing icon name
+- **Drawable name** — an internal name of an icon (e.g. `app_name`). It's used to include an icon image to the icon pack, and in combo with ComponentInfo(s) it links the icon with the target app. The drawable name must be in alphanumeric lowercase with underscores only and icon image names must match the drawable name exactly (e.g. `new_icon.png` and `new_icon.svg`).
+- **Standalone icon** — an icon that isn't linked to any app. They can be non-app icons like `adobe`, or folder icons and be used for web shortcuts, app folders, etc. Users can select them via their launcher if it supports that feature.
+- **Alternative icon** — an alternative version of an app icon. Mainly used when the app rebrands: the old icon becomes an alternative (e.g. `app_name_alt_1`), and a new icon takes its place. However, you can create alternative icons for any app without linking them to any ComponentInfo, and they will be standalone. Users can select them via their launcher if it supports that feature.
+- **Categories** — categories within [`app/src/main/assets/drawable.xml`](./app/src/main/assets/drawable.xml) to organize icons. Description of categories:
-1. Determine if alternative icons exist for the target app by checking `Alts` category in [`app/src/main/res/xml/drawable.xml`](./app/src/main/res/xml/drawable.xml). If no alternative icons then start numbering from `1` (e.g. `old_icon_alt_1`), otherwise continue numbering based on latest alternative icon number (e.g. `old_icon_alt_2`)
+ - `New` — new icons for the current release. If [manually](#manual) adding icons, you must also add the entry to this category.
+ - `Alts` — alternative icons.
+ - `Calendar` — calendar icons.
+ - `Folders` — folder icons.
+ - `Google` — Google apps (Chrome, YouTube, etc.).
+ - `System` — system icons (Camera, Contacts, Settings, etc.).
+ - `#` — icons whose name starts with a number (e.g. `_2048`). If [manually](#manual) adding icons, drawable names that begin with a number must have a leading underscore and be placed in this category.
+ - `A–Z` — everything else, sorted by the first letter of the drawable name.
-2. Rename `old_icon.svg` to `old_icon_alt_1.svg` in [`resources/vectors`](./resources/vectors) directory (if SVG not found there just skip this step)
+### Rules
-3. Rename `old_icon.png` to `old_icon_alt_1.png` in [`app/src/main/res/drawable-nodpi`](./app/src/main/res/drawable-nodpi) directory
+- Keep [LF](https://en.wikipedia.org/wiki/Newline) line endings in edited files. Git has a setting for this.
-4. Add `old_icon_alt_1` to `Alts` category and `old_icon` to `New` category in [`app/src/main/assets/drawable.xml`](./app/src/main/assets/drawable.xml) and [`app/src/main/res/xml/drawable.xml`](./app/src/main/res/xml/drawable.xml)
+### Notes for Contributors
-5. If the ComponentInfo also changed after rebranding, replace `old_icon` with `old_icon_alt_1` in [`app/src/main/assets/appfilter.xml`](./app/src/main/assets/appfilter.xml) and [`app/src/main/res/xml/appfilter.xml`](./app/src/main/res/xml/appfilter.xml) (the alternative icon will be linked with the old ComponentInfos for back compability)
+Want to help close user requests? Check [`contribs/requests.yml`](./contribs/requests.yml) — it contains all pending icon requests and is updated periodically.
-# Resources
+If you wish, you can add yourself to [`app/src/main/res/xml/contributors.xml`](./app/src/main/res/xml/contributors.xml) to shine in the app's contributors section!
-## Font
+## Adding / Managing Icons
-> If the original icon consists of just one or two letters, you may trace that letter instead of using these fonts
+Your new app icon will have two important identifiers:
-- [Now](https://www.1001fonts.com/now-font.html?text=Delta%20Icons) (Sans-serif) — main font ; use Now Alt from the same family for alternate lowercase 'a' letter
-- [Aleo](https://www.1001fonts.com/aleo-font.html?text=Delta%20Icons) (Serif) — use it only when Serif is needed
+- `icon_name` (and derivatives of it) will be used as a drawable name
+- `com.example/com.example.MainActivity` (and derivatives of it) will be used as a ComponentInfo
-## Requests
+
+ Example
+ You have an app called "Delta Icon Delivery". A sensible name for your drawable would be delta_icon_delivery. The component info would be dictated by the app itself and be akin to something like com.delta.delivery/com.delta.delivery.Actvities.MainActivity.
+
-If you wanna help close icon requests from users, you can take a look at [`contribs/requests.yml`](./contribs/requests.yml) where all requests are stored. The file updates periodically.
+So, you made an icon then exported it as `new_icon.png` and `new_icon.svg`. Now you needto select which way to manage icons. Here are two ways:
+
+- [**Automatic**](#automatic) — an automatic and declarative way of managing icons via the file [`contribs/icons.yml`](./contribs/icons.yml) in the repo, processed by scripts and GitHub Actions. This is the recommended approach.
+- [**Manual**](#manual) — this is how icons were managed before [**Automatic**](#automatic) was implemented. Directly editing XMLs and placing icon images into the appropriate directories. More control, but also more prone to errors. Try to avoid it unless [**Automatic**](#automatic) can't handle what you need.
+
+### Automatic
+
+This is an automatic and declarative method of managing icons via [`contribs/icons.yml`](./contribs/icons.yml), driven by scripts and GitHub Actions.
+
+Place your icon images (the `new_icon.png` and `new_icon.svg` files both) in the [`contribs/icons`](./contribs/icons) directory and add an entry in the following format to [`contribs/icons.yml`](./contribs/icons.yml):
+
+```yaml
+new_icon: com.example/com.example.MainActivity
+```
+
+And that's all. This is the easiest and most common method for adding a new icon and linking it to an app. [`contribs/icons.yml`](./contribs/icons.yml) will be processed by scripts and cleared automatically every release.
-## Gathering ComponentInfo
+That entry can be extended with more options, for example:
-ComponentInfo is what your launcher uses to know which apps get which icons in our icon pack.
+```yaml
+new_icon:
+ action: rewrite
+ category: google
+ compinfo:
+ - com.example/com.example.MainActivity
+ - com.example/com.example.SplashActivity
+```
-You may use these tools to find each app's СomponentInfo(s):
-- [Icon Pusher](https://iconpusher.com/) by [V01D](https://v01d.uk)
-- [Icon Request](https://github.com/Kaiserdragon2/IconRequest/releases) by [Kaiserdragon2](https://github.com/Kaiserdragon2)
+Check [Options](#options) below to get an explanation of each option and [Examples](#examples) for more examples.
-## Icon Template
+#### Options
-### Rules
+- `action` — describes what to do with the icon. It can take one of the following values:
-- Canvas size must be 192x192px, the icon size according to the template below
-- If the original logo doesn't contain small details or doesn't make up most of the background layer (circle/square/etc.) as designed, keep the logo size between 73-80px
-- The rounded corners of squares and rectangles have a corner radius of 10px
+ - `add` — add a new icon. This is the default action if the option is not explicitly set.
+ - `rewrite` — overwrite icon images of an existing icon with new ones from [`contribs/icons`](./contribs/icons).
+ - `rebrand` — move an existing icon to `alt_x` (`x` will be automatically calculated), add a new icon and use it as the main one. If you pass any ComponentInfo, existing ComponentInfos will be attached to `alt_x` (for backward compatibility with older versions of the app), otherwise `alt_x` will be a standalone alternative icon.
+ - `remove` — remove an existing icon.
+ - `rename > name` — rename an existing icon (where `name` is a new name of the existing icon).
+ - `move > category` — move an existing icon to a different category (where `category` is the category name).
-| | |
-|---|---|
+- `category` — overrides the automatic category assignment, e.g. if you want to assign a Google app icon to the Google category. If not set, the category is assigned based on the following logic:
-Or you can check [Figma icon template](https://www.figma.com/design/02aiFRSLkikcw8mpBAnoDA/Delta-Icon-Template?m=auto&t=qyLH05AMDzZwAI2s-1).
+ - if the drawable name starts with `_[0-9]` (e.g. `_2048`), the category will be `#`
+ - if the drawable name ends with `_alt_[0-9]+` (e.g. `telegram_alt_23`), the category will be `Alts`
+ - else the category will be `A–Z` based on the first letter of the drawable name
-## Colors
+- `compinfo` — a list of ComponentInfos to link to the current icon. It can be a string or a list (see more in [Examples](#examples)).
-### Rules
+#### Examples
+```yaml
+# a new icon with a single ComponentInfo
+# without adding icon images it will only link a ComponentInfo with new_icon
+new_icon: com.example/com.example.MainActivity
+# or with multiple ComponentInfos
+new_icon:
+ - com.example/com.example.MainActivity
+ - com.example/com.example.SplashActivity
-- [ $\textcolor{#56595B}{\textsf{⬤}}$ #56595B Davy's grey ] as default Black
+# new alternative icons
+# will be automatically assigned to Alts category
+new_icon_alt_1: com.example/com.example.MainActivity
+new_icon_alt_2: com.example/com.example.SplashActivity
+# or new standalone alternative icons
+new_icon_alt_1: {}
+new_icon_alt_2: {}
+# if you're not sure if there's no alts with these names, you can use this:
+# _alt_x will be automatically resolved to the next available alt number, e.g.:
+# new_icon_alt_x1 > new_icon_alt_4 (if _alt_1, _alt_2, _alt_3 exist)
+# use different numbers after x to add multiple alts in one file
+new_icon_alt_x1: com.example/com.example.MainActivity
+new_icon_alt_x2: com.example/com.example.SplashActivity
+new_icon_alt_x3: {}
+new_icon_alt_x4: {}
-- [ $\textcolor{#FF837D}{\textsf{⬤}}$ #FF837D Coral pink ] as default Red and [ $\textcolor{#BA6561}{\textsf{⬤}}$ #BA6561 Fuzzy Wuzzy ] as default Dark Red. Shades of Red are specifically for shading purposes
+# a new standalone icon
+new_icon: {}
-- Transparencies — White (25%, 50%, 70%) and Black (15%, 25%) can be used as overlay for additional shading
+# add a new Google app icon to Google category
+google_app:
+ category: google
-### Palette
+# rewrite the icons of an existing icons without touching XMLs
+# existing_icons.png and existing_icon.svg must be in contribs/icons
+existing_icon:
+ rebrand: rewrite
-> Palette variants are hidden under spoilers below
+# rebrand an existing icon and make the previous icon a standalone alternative icon \
+# as existing_icon_alt_x (x will be automatically calculated)
+existing_icon:
+ rebrand: rebrand
-
- Full
-
-
-
+# rebrand an existing icon with attaching previous ComponentInfos \
+# to a existing_icon_alt_x (x will be automatically calculated)
+existing_icon:
+ rebrand: rebrand
+ compinfos:
+ - com.example/com.example.MainActivity
-
- Simple
-
-
-
+# rename an existing icon
+# will be automatically moved to the appropriate category
+existing_icon:
+ action: rename > new_icon
-
-HTML
-
-
-
-
- Greys
- Basic
- Reds
- Skintones
-
-
-
-
-
- $\textcolor{#FFFFFF}{\textsf{⬤}}$ #FFFFFF White
- $\textcolor{#ECECEC}{\textsf{⬤}}$ #ECECEC Isabelline
- $\textcolor{#D8D8D8}{\textsf{⬤}}$ #D8D8D8 Timberwolf
- $\textcolor{#D2D2D2}{\textsf{⬤}}$ #D2D2D2 Light gray
- $\textcolor{#CCCCCC}{\textsf{⬤}}$ #CCCCCC Pastel gray
- $\textcolor{#B1B5BD}{\textsf{⬤}}$ #B1B5BD Ash grey
- $\textcolor{#A0A5AF}{\textsf{⬤}}$ #A0A5AF Dark gray
- $\textcolor{#979797}{\textsf{⬤}}$ #979797 Manatee
- $\textcolor{#83868C}{\textsf{⬤}}$ #83868C Taupe gray
- $\textcolor{#56595B}{\textsf{⬤}}$ #56595B Davy's grey
- $\textcolor{#4A4A4A}{\textsf{⬤}}$ #4A4A4A Quartz
- $\textcolor{#000000}{\textsf{⬤}}$ #000000 Black
-
-
- $\textcolor{#FFD6D4}{\textsf{⬤}}$ #FFD6D4 Pastel pink
- $\textcolor{#FF837D}{\textsf{⬤}}$ #FF837D Coral pink
- $\textcolor{#BA6561}{\textsf{⬤}}$ #BA6561 Fuzzy Wuzzy
- $\textcolor{#D3B69A}{\textsf{⬤}}$ #D3B69A Tan
- $\textcolor{#8E6F60}{\textsf{⬤}}$ #8E6F60 Shadow
- $\textcolor{#FCECDC}{\textsf{⬤}}$ #FCECDC Antique white
- $\textcolor{#F8C18C}{\textsf{⬤}}$ #F8C18C Pale gold
- $\textcolor{#FDF5D9}{\textsf{⬤}}$ #FDF5D9 Cornsilk
- $\textcolor{#F9DE81}{\textsf{⬤}}$ #F9DE81 Jasmine
- $\textcolor{#C39A54}{\textsf{⬤}}$ #C39A54 Camel
- $\textcolor{#E0F4E0}{\textsf{⬤}}$ #E0F4E0 Platinum
- $\textcolor{#98DC9A}{\textsf{⬤}}$ #98DC9A Granny Smith Apple
- $\textcolor{#71A372}{\textsf{⬤}}$ #71A372 Asparagus
- $\textcolor{#96DFD3}{\textsf{⬤}}$ #96DFD3 Pale robin egg blue
- $\textcolor{#73ADA4}{\textsf{⬤}}$ #73ADA4 Cadet blue
- $\textcolor{#9ABEFF}{\textsf{⬤}}$ #9ABEFF Baby blue eyes
- $\textcolor{#728DBE}{\textsf{⬤}}$ #728DBE Dark pastel blue
- $\textcolor{#54688C}{\textsf{⬤}}$ #54688C UCLA Blue
- $\textcolor{#ABABFF}{\textsf{⬤}}$ #ABABFF Baby blue eyes
- $\textcolor{#BD9AFF}{\textsf{⬤}}$ #BD9AFF Bright lavender
- $\textcolor{#8C72BD}{\textsf{⬤}}$ #8C72BD Ube
-
-
- $\textcolor{#FFB0AC}{\textsf{⬤}}$ #FFB0AC Melon
- $\textcolor{#F58F8A}{\textsf{⬤}}$ #F58F8A Light coral
- $\textcolor{#F4806D}{\textsf{⬤}}$ #F4806D Coral pink
- $\textcolor{#E85E5C}{\textsf{⬤}}$ #E85E5C Terra cotta
- $\textcolor{#DC505E}{\textsf{⬤}}$ #DC505E Dark terra cotta
- $\textcolor{#B02A3C}{\textsf{⬤}}$ #B02A3C Deep carmine
- $\textcolor{#7A1B1C}{\textsf{⬤}}$ #7A1B1C Falu red
- $\textcolor{#511119}{\textsf{⬤}}$ #511119 Dark scarlet
-
-
- $\textcolor{#F1E9E0}{\textsf{⬤}}$ #F1E9E0 Eggshell
- $\textcolor{#D6C8BA}{\textsf{⬤}}$ #D6C8BA Pastel gray
- $\textcolor{#D4C6B8}{\textsf{⬤}}$ #D4C6B8 Pale silver
- $\textcolor{#D7D0B8}{\textsf{⬤}}$ #D7D0B8 Pastel gray
- $\textcolor{#E2C9B0}{\textsf{⬤}}$ #E2C9B0 Desert sand
- $\textcolor{#D4B79A}{\textsf{⬤}}$ #D4B79A Tan
- $\textcolor{#BF9E73}{\textsf{⬤}}$ #BF9E73 Camel
-
-
-
-
-
+# move an existing icon to a different category, e.g. google
+existing_icon:
+ action: move > google
-#### Graphic Editors
+# remove an existing icon from XMLs and image directories
+existing_icon:
+ action: remove
-- [Adobe Swatch Exchange Palette](./resources/palettes/palette.ase) (Illustrator, Photoshop)
+```
-- [GPL Pallete](./resources/palettes/palette.gpl) (Inkscape, Karbon)
+### Manual
-# Building via GitHub Actions
+This is how icons were managed before [**Automatic**](#automatic) was implemented. Avoid it unless [**Automatic**](#automatic) can't handle what you need.
-> Everything described here must be done in your fork
+There are two `drawable.xml` and two `appfilter.xml` files to edit (stored in [`app/src/main/assets`](./app/src/main/assets) and [`app/src/main/res/xml`](./app/src/main/res/xml)). It's better to edit the XMLs in [`app/src/main/assets`](./app/src/main/assets), then copy them to [`app/src/main/res/xml`](./app/src/main/res/xml) to keep all files identical. You can do it however you want (e.g. editing all files at the same time), just keep them identical.
+
+#### Adding a new icon
-## [OPTIONAL] Creating Secrets
+1. Add `new_icon.svg` to [`resources/vectors`](./resources/vectors) directory.
-Go to `Settings → Secrets and Variables → Actions` and create the following repository secrets:
+2. Add `new_icon.png` to [`app/src/main/res/drawable-nodpi`](./app/src/main/res/drawable-nodpi) directory.
- - `KEYSTORE_BASE64`
+3. Append the line ` ` to both the `New` and the appropriate letter category in [`app/src/main/assets/drawable.xml`](./app/src/main/assets/drawable.xml). Here's how it should look:
+ ```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
```
- 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
- ```
- - `KEYSTORE_PASSWORD`
+4. Append the line ` ` to [`app/src/main/assets/appfilter.xml`](./app/src/main/assets/appfilter.xml). Here's how it should look:
+
+ ```xml
+
+
+
+
+
```
- android
- ```
+5. Copy edited XMLs from [`app/src/main/assets`](./app/src/main/assets) to [`app/src/main/res/xml`](./app/src/main/res/xml).
- - `KEYSTORE_KEY_ALIAS`
+6. Repeat the process for more icons.
- ```
- android
- ```
+#### Alternative Icons
- - `KEYSTORE_KEY_PASSWORD`
+If an existing icon has been rebranded, don't overwrite it with a new one — do the following:
- ```
- android
- ```
+> `old_icon` will be used as an existing drawable name.
+> `old_icon_alt_1` will be used as an alternative icon name for the existing drawable name.
+
+1. Determine if alternative icons exist for the target app by checking `Alts` category in [`app/src/main/res/xml/drawable.xml`](./app/src/main/res/xml/drawable.xml). If no alternative icons exist, start numbering from `1` (e.g. `old_icon_alt_1`), otherwise continue numbering based on the latest alternative icon number (e.g. `old_icon_alt_2`).
+
+2. Rename `old_icon.svg` to `old_icon_alt_1.svg` in [`resources/vectors`](./resources/vectors) directory (if SVG is not found there, just skip this step).
+
+3. Rename `old_icon.png` to `old_icon_alt_1.png` in [`app/src/main/res/drawable-nodpi`](./app/src/main/res/drawable-nodpi) directory.
+
+4. Add `old_icon_alt_1` to `Alts` category and `old_icon` to `New` category in [`app/src/main/assets/drawable.xml`](./app/src/main/assets/drawable.xml).
+
+5. If the ComponentInfo also changed after rebranding, replace `old_icon` with `old_icon_alt_1` in [`app/src/main/assets/appfilter.xml`](./app/src/main/assets/appfilter.xml) (the alternative icon will be linked with the old ComponentInfos for backward compatibility).
+
+6. Copy edited XMLs from [`app/src/main/assets`](./app/src/main/assets) to [`app/src/main/res/xml`](./app/src/main/res/xml).
+
+#### Other Manipulations
+
+The rest of the things are more or less obvious like moving drawable names between categories, renaming, etc. Just ask for help in Discord if something isn't clear.
+
+# 🏗️ Build
+
+## GitHub Actions
+
+> Everything described here must be done in your fork.
+
+### Run Workflow
+
+1. Go to [Actions → Build FOSS](../../actions/workflows/build_foss.yml)
+2. Click on **Run workflow**, optionally mark preferred checkboxes, then click on **Run workflow**
+3. Wait for the build, it takes approximately 5-10 minutes. The zipped APK will be attached to the workflow run. Go to [Actions](../../actions), click on the latest workflow run and download it from **Artifacts** down below
+
+### Creating Secrets
+
+> This is optional since the workflow contains hardcoded values, but you can do this to use your own keystore. The following values of variables and options match the hardcoded workflow values.
+
+1. Generate a personal keystore with `keytool -genkeypair -alias android -keypass android -keystore android.keystore -storepass android -keyalg RSA -dname "CN=Android,O=Android,C=US" -validity 9999`
+
+2. Encode the keystore with `cat android.keystore | base64 | tr -d '\n' > android.keystore.base64` or do it with any online tool.
+
+3. Go to `Settings → Secrets and Variables → Actions` and create the following repository secrets (key-value pairs):
+
+ - `KEYSTORE_BASE64` (contents of `android.keystore.base64`)
+
+ ```
+ MIIKRgIBAzCCCfAGCSqGSIb3DQEHAaCCCeEEggndMIIJ2TCCBbAGCSqGSIb3DQEHAaCCBaEEggWdMIIFmTCCBZUGCyqGSIb3DQEMCgECoIIFQDCCBTwwZgYJKoZIhvcNAQUNMFkwOAYJKoZIhvcNAQUMMCsEFALf2o/enYgJaO2D4otoTSpxWhWtAgInEAIBIDAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQMpyd3LX1rnoCfCGv+LAQ1wSCBNDoQdq5T9uFBEf2nKKgH1WR1/F7s9AIk9Gs+VVu03Y8ntd7QNDf55HytKZbRFE5cN7Vod5LPm4uiUP5zPVkGgqmX6nfZPRppR1k17X2pYG/lm7n2WUItt35HeIxr6Tbnqr7eLRuCwCZ7kfpJYhmOVZ/MIsylejqjbTqX1ajkVUFeb4J0KVZlq4OXhqMCmHHxaZe41yV/WjfPtbXyP7MCjp47XY4LpTlJ+ad1COwlktMv1oud5UUQfVnQwkcOQZQGoZuuL41cEAeHjR6GpEVnyhR33t9kOPdAPLFVyp22+8TLFt3RlRvJy4Sn+430kxGxhrfW8KTfz0CiGljTeElTq55OscEi+eOLJo/gwVgZ7zas+7lV/4MAhcQLsArhCn5v1l1QVWeXE+9udME+0OZfc3A/TDeP1k40/1KVkFpmKLyH1DZlCLy5SeuANFtKpP+Uj3tioVI7CBHzuTkf2A4itoaVHOFELmK7O5ypfz8jL+qmwQjvJiPJoVdCNZPUr9zF6uym65BvtRwBWhBKiBNYYCoeXJkX46SGSgZ4nSIlBGq3DwGbTqG6JfJkzbIys5a5nCIQWwCalveIRDeYQlEorNWXGY37cF1TOeCWcS6NeTSpAP+Php27kUpAwkYYTVJcqWnOyXcDysxiD1AWWt8Jtpg00OBnHVD1ANgoa8Zfe12pBEXIaLh/3PoBTkcHii0WRhV88z0ewGKTWKTYKFTJAY/pkP4MfPePYuJPvt3FZJ2NnslocTi8JgWZcveBsPNFSjTpR1aapg+ukgYRwAYO69gH2tw4SBkozrRTwh86xmedLA8ah1Jii7itdUg+odmF+JUjm2X50BJiLCpUKJxnJ4zkkcB7DP7XlRNHz/KBg5WLbNyBPxB6LYQbtMUDQ6Du0Idl5vQ/HLgbs1wHUMFQA/uc9Czz43Ansh1g+ZGI7pw+RVUGKe3YglXjrbGe8RWlr3RxjxBnWExeMkg9Z3SDVRYkFOQ8aI5HB/37JFAG5tk/z7UxiM1GlnEA3ZCZ/OJJMaYYfFidIsNb8FVjWddOPfDmrJlguSilkqJx2VsGAxslSpcicCHRij/Rjm5E6wWkj7GjgJb9kf4kXbOi+THK09/40LqZci89qvUJ1a0a0Ts+IVOhaIXXAk/1Jd2zzFTU/yRSPjm5UvLkajhfmr7sR/XCjZN53kq8aR6F5YIyH1f+Su3ahzl4CGG7Dceypd5KX0NfpO2i/9IoYSDTm/eWCNfQ18k7kpqdI/tyhD1YTum2dzW8o578qReph37SG5CsqX9AVeuKBLihAbY+fZ4tKaWigiigCgnGBKjKcBNRTjnDlfL/lkmR0uB6Ye618dnRVUIOsfG9rsM0pLlNc2rUIBwEkFXj7Zdsao9y3T+SCIBNyM0mWEleQLHcEs8E8g7C88gtvFvxXGANT3z1tr0C05Og9OJSV7Sz4Di7JoI+c1kmBS7Gn8KqxYNv+lCdS0f+mKIypOHwgRcPeY7rk0vpkfBHIaMR8Vnvd0aiOCgbmiJWXTcmfl+cgKUvcfzMUbR8aYJPnP0wEUR64EBuEJHUnkwpFUprXDYvIPcI39EALVlnVqY5ZSXzeqX2vVyiuK4IcR6R7vH0ZlD26r0/c/Pj3Ci6mQS6RNGuzrcsf78/bvdzTFCMB0GCSqGSIb3DQEJFDEQHg4AYQBuAGQAcgBvAGkAZDAhBgkqhkiG9w0BCRUxFAQSVGltZSAxNjgzNTMxNDQ1NzI0MIIEIQYJKoZIhvcNAQcGoIIEEjCCBA4CAQAwggQHBgkqhkiG9w0BBwEwZgYJKoZIhvcNAQUNMFkwOAYJKoZIhvcNAQUMMCsEFAMBu3VzOPYst5nuc5pukUGrNpb1AgInEAIBIDAMBggqhkiG9w0CCQUAMB0GCWCGSAFlAwQBKgQQjKOpjsq0gFHwTwH9VV53BoCCA5DBfuD14myPSgcezH6Z4V2Fph94upgzY4ijij5zOZdgzj4D7yYbNh9iSSvb3nEB5m/FbnuHBYuGEzeGOiHugMqPwr+2M4dfqcC+17myjtv+2DCseUHZIMAA++HBWsl1yFF8OF7Ofxj8f17gBiJ+Cexd1oniNj8HyT5aWeJ/+pIsMSirX/fQ2sKyA7YTrmFVAqsJ29rTv923XDXi1CcW0tGsxFHT+FsbvwzxS5S2t8hKgmbQz2tO6i/NP6kencEc93YdsVRVlO+pu8bT+LXSvINT1wdrsedWlUBIjjmEfuz6cckDIpphsaEQcMegTJ0eb5IldyrCD7iVTWYBE6ZhUM9v7UbAAEx3MsdMOfsdNqpfFeJswIYOxQjBJ0GFv7zVfVT6LA2SXqwTaecFiAl5pC3QOFOsSSe/rndBqeT62zGn9daL4Zr1qgmhtvFcgOYKAVGgxiaa2XDN6Z8OsIgYqONWOhwX8IwjbWgpiVzJjr9HqNSrUl+3Fk8nOyzRlf1gBdQmIblDqZ9C6PPHSJQiVZCS8hd68np9oiz96ltxSnroEZ7YkoBQSfDMw3nFoDJ6W46/H65HjUmALxikw1wsOkDvT5Z6VGvaAHFc7Ng/38UBx1yNhF+W+IGFnXIhtwaxfKmdtdFjHzS54Q5qPk/HCKVBTlZOZtfEJvQNiE1pthDMPwdYZ8a6PR7gTEiRT9LChHuGh1TZIhk0rkGiUJScj5ix69iGHTi4yKmeHgqonDXeCCdyjf6S9Ox8wQ7x9Kvu53pz8u/hadbR/+Iuc9v1YFES44QmApizYYEUufVCYqlsCD+pBSm41WSpvLYZvBJpO8lQgMPNh+IKU5mbTaMOdF+NMRMdu1tdjBbcjn/HpqCIztNxZqUbcRe4ndNMs7qmDdIDqmkPBxmLnmuJERHNdu2BiCsj+UlVDgVx0H7yNFFAD7RPheekIHMILhb63ngr1uKXYD/zpJj3fNqbOlveN47JydA1pEMPRKmehudmgm5k9oNxgKKDof3J9RMsynUSNUlvG/UWA/9+aeL8vImOMSeYAnQ3idwc8t4y9zzHWmVzdtw9vALo8O5H1IddwSlii4U9kq/3NniWR7JaPEva910vOYDlkcSIoZyLuEx3e+QgYVlI/9u0/0cE0PzwY8BAJK0ze38Rz5pRfErenYRQ/xXZ8uKM4gJZ5C8bYj3RN8yFFs5UL6gbeacaWVrjVPuW+zswTTAxMA0GCWCGSAFlAwQCAQUABCCoXbCueJPh7HqJ7mXzLBbkWP2C3n/PcJd94KJX2rufDQQUn9KR4oRYNugnRaGiJGcSzwEvq7oCAicQ
+ ```
+ - `KEYSTORE_PASSWORD` (the value of `-storepass` option)
-You can generate a new keystore with `keytool -genkeypair -alias android -keypass android -keystore android.keystore -storepass android -keyalg RSA -dname "CN=Android,O=Android,C=US" -validity 9999` and fill secrets with your values.
+ ```
+ android
+ ```
-To encode the keystore, use `cat android.keystore | base64 | tr -d '\n' > android.keystore.base64` or do it with any online tool.
+ - `KEYSTORE_KEY_ALIAS` (the value of `-alias` option)
-## Run Workflow
+ ```
+ android
+ ```
-1. Go to **Actions**
-2. Select **Build FOSS**
-3. Select **Run workflow** then click to green **Run workflow** button
-4. Wait for the build, it takes approximately 5 minutes
+ - `KEYSTORE_KEY_PASSWORD` (the value of `-keypass` option)
-The zipped APK will be attached to the workflow run. Go to **Actions**, click on the latest workflow run and download it from **Artifacts** down below
+ ```
+ android
+ ```
diff --git a/README.md b/README.md
index 5a31bd9ac4..e98d48c397 100644
--- a/README.md
+++ b/README.md
@@ -48,9 +48,9 @@
### Flavors
-- `gplay` [`website.leifs.delta`] — Google Play version
-- `foss` [`website.leifs.delta.foss`] — FOSS version (Google-free)
-- `fossdc` [`website.leifs.delta.fossdc`] — FOSS version with a dynamic clock
+- `gplay` — Google Play version
+- `foss` — FOSS version (Google-free)
+- `fossdc` — FOSS version with a dynamic clock
> `fossdc` is only available via [Releases](./releases/latest) section on GitHub. It's the same as `foss` flavor but the static clock is replaced with a dynamic one, and it has a different app ID suffix. This is a separate app because dynamic clocks require a higher level of Android API and are not widely supported in launchers. We don't wanna sacriface users with old phones but wanna support cool features, that's why there's such a compromise. This may change in the future. At the moment, you can use this version by installing it via Obtanium .
diff --git a/app/src/main/assets/drawable.xml b/app/src/main/assets/drawable.xml
index 1e1b1d2b6e..3de1064071 100644
--- a/app/src/main/assets/drawable.xml
+++ b/app/src/main/assets/drawable.xml
@@ -649,7 +649,6 @@
-
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi.png b/app/src/main/res/drawable-nodpi/gosuslugi.png
index 610b9476d9..50565ec18f 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi.png and b/app/src/main/res/drawable-nodpi/gosuslugi.png differ
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi_auto.png b/app/src/main/res/drawable-nodpi/gosuslugi_auto.png
index b3668fcca3..cf69ad3f77 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi_auto.png and b/app/src/main/res/drawable-nodpi/gosuslugi_auto.png differ
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi_culture.png b/app/src/main/res/drawable-nodpi/gosuslugi_culture.png
index fe49b150f1..b3aba6e99d 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi_culture.png and b/app/src/main/res/drawable-nodpi/gosuslugi_culture.png differ
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi_dom.png b/app/src/main/res/drawable-nodpi/gosuslugi_dom.png
index 1632122e27..728f988510 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi_dom.png and b/app/src/main/res/drawable-nodpi/gosuslugi_dom.png differ
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi_key.png b/app/src/main/res/drawable-nodpi/gosuslugi_key.png
index 2f5280deaa..7e42fc490a 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi_key.png and b/app/src/main/res/drawable-nodpi/gosuslugi_key.png differ
diff --git a/app/src/main/res/drawable-nodpi/gosuslugi_pos.png b/app/src/main/res/drawable-nodpi/gosuslugi_pos.png
index 301cc5d760..bbac8469c2 100644
Binary files a/app/src/main/res/drawable-nodpi/gosuslugi_pos.png and b/app/src/main/res/drawable-nodpi/gosuslugi_pos.png differ
diff --git a/app/src/main/res/xml/drawable.xml b/app/src/main/res/xml/drawable.xml
index 1e1b1d2b6e..3de1064071 100644
--- a/app/src/main/res/xml/drawable.xml
+++ b/app/src/main/res/xml/drawable.xml
@@ -649,7 +649,6 @@
-
diff --git a/contribs/icons.yml b/contribs/icons.yml
index b6df8684e2..3b7568a136 100644
--- a/contribs/icons.yml
+++ b/contribs/icons.yml
@@ -12,3 +12,46 @@
# category: Google
# compinfos:
# - com.google.app/com.google.app.MainActivity
+
+uralsib:
+ - ru.bankuralsib.mb.android/ru.bankuralsib.mb.start.presentation.ui.activity.StartActivity
+
+yoxo:
+ - ro.orange.yoxo/ro.orange.yoxo.onboarding.ui.activity.SplashScreenActivity
+
+abr_direct:
+ - ru.artsofte.russiafl/ru.artsofte.russiafl.MainActivity
+
+gosuslugi_school:
+ - ru.gosuslugi.school/ru.gosuslugi.school.MainActivity
+
+gosuslugi_biometry: ru.rtlabs.mobile.ebs.gosuslugi.android/ru.rtlabs.mobile.ebs.ui.main.MainActivity
+
+maadhaar:
+ - in.gov.uidai.pehchaan/in.gov.uidai.pehchaan.onboarding.SplashActivity
+
+documents:
+ - andes.oplus.documentsreader/andes.oplus.documentsreader.launcher.activity.MainActivity
+
+colonist:
+ action: rebrand
+
+battery_alarm:
+ - simple.batttery.alarm/simple.batttery.alarm.main
+ - simple.battery.alarm/simple.battery.alarm.main
+
+weblibre: eu.weblibre.gecko/eu.weblibre.gecko.MainActivity
+
+umi: app.immersely.immersely/app.immersely.immersely.MainActivity
+
+kreditbee: com.kreditbee.android/com.kreditbee.android.splash.SplashActivity
+
+tosla: com.akode.tosla/com.valensas.tosla.modules.auth.landing.splash.SplashActivity
+
+xash3d:
+ - su.xash.engine/su.xash.engine.MainActivity
+ - su.xash.engine.test/su.xash.engine.MainActivity
+
+heyjom: com.heyjom.android/com.heyjom.android.MainActivity
+
+naver_plus: com.navercorp.navershopping/com.naver.nspa.app.MainActivity
\ No newline at end of file
diff --git a/contribs/icons/abr_direct.png b/contribs/icons/abr_direct.png
new file mode 100644
index 0000000000..f53ac15cf5
Binary files /dev/null and b/contribs/icons/abr_direct.png differ
diff --git a/contribs/icons/abr_direct.svg b/contribs/icons/abr_direct.svg
new file mode 100644
index 0000000000..3f5b8f64a5
--- /dev/null
+++ b/contribs/icons/abr_direct.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/battery_alarm.png b/contribs/icons/battery_alarm.png
new file mode 100644
index 0000000000..cde2fe35fe
Binary files /dev/null and b/contribs/icons/battery_alarm.png differ
diff --git a/contribs/icons/battery_alarm.svg b/contribs/icons/battery_alarm.svg
new file mode 100644
index 0000000000..bce4a05885
--- /dev/null
+++ b/contribs/icons/battery_alarm.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/colonist.png b/contribs/icons/colonist.png
new file mode 100644
index 0000000000..72998f4e25
Binary files /dev/null and b/contribs/icons/colonist.png differ
diff --git a/contribs/icons/colonist.svg b/contribs/icons/colonist.svg
new file mode 100644
index 0000000000..13f8761ca6
--- /dev/null
+++ b/contribs/icons/colonist.svg
@@ -0,0 +1,2 @@
+
+C
diff --git a/contribs/icons/gosuslugi_biometry.png b/contribs/icons/gosuslugi_biometry.png
new file mode 100644
index 0000000000..d1e57e6f41
Binary files /dev/null and b/contribs/icons/gosuslugi_biometry.png differ
diff --git a/contribs/icons/gosuslugi_biometry.svg b/contribs/icons/gosuslugi_biometry.svg
new file mode 100644
index 0000000000..eb9fe41d86
--- /dev/null
+++ b/contribs/icons/gosuslugi_biometry.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/gosuslugi_school.png b/contribs/icons/gosuslugi_school.png
new file mode 100644
index 0000000000..856c800d5e
Binary files /dev/null and b/contribs/icons/gosuslugi_school.png differ
diff --git a/contribs/icons/gosuslugi_school.svg b/contribs/icons/gosuslugi_school.svg
new file mode 100644
index 0000000000..37220eab0b
--- /dev/null
+++ b/contribs/icons/gosuslugi_school.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/heyjom.png b/contribs/icons/heyjom.png
new file mode 100644
index 0000000000..4b69e93273
Binary files /dev/null and b/contribs/icons/heyjom.png differ
diff --git a/contribs/icons/heyjom.svg b/contribs/icons/heyjom.svg
new file mode 100644
index 0000000000..474798989a
--- /dev/null
+++ b/contribs/icons/heyjom.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/kreditbee.png b/contribs/icons/kreditbee.png
new file mode 100644
index 0000000000..eb47d04258
Binary files /dev/null and b/contribs/icons/kreditbee.png differ
diff --git a/contribs/icons/kreditbee.svg b/contribs/icons/kreditbee.svg
new file mode 100644
index 0000000000..098737aa24
--- /dev/null
+++ b/contribs/icons/kreditbee.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/naver_store.png b/contribs/icons/naver_store.png
new file mode 100644
index 0000000000..d86268c28c
Binary files /dev/null and b/contribs/icons/naver_store.png differ
diff --git a/contribs/icons/naver_store.svg b/contribs/icons/naver_store.svg
new file mode 100644
index 0000000000..d3656c7146
--- /dev/null
+++ b/contribs/icons/naver_store.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/tosla.png b/contribs/icons/tosla.png
new file mode 100644
index 0000000000..319b948352
Binary files /dev/null and b/contribs/icons/tosla.png differ
diff --git a/contribs/icons/tosla.svg b/contribs/icons/tosla.svg
new file mode 100644
index 0000000000..ea8c26e656
--- /dev/null
+++ b/contribs/icons/tosla.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/uni.png b/contribs/icons/uni.png
new file mode 100644
index 0000000000..9f3ddbc334
Binary files /dev/null and b/contribs/icons/uni.png differ
diff --git a/contribs/icons/uni.svg b/contribs/icons/uni.svg
new file mode 100644
index 0000000000..b35cca896d
--- /dev/null
+++ b/contribs/icons/uni.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/uralsib.png b/contribs/icons/uralsib.png
new file mode 100644
index 0000000000..39d88d3dc5
Binary files /dev/null and b/contribs/icons/uralsib.png differ
diff --git a/contribs/icons/uralsib.svg b/contribs/icons/uralsib.svg
new file mode 100644
index 0000000000..b62f969e3e
--- /dev/null
+++ b/contribs/icons/uralsib.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/weblibre.png b/contribs/icons/weblibre.png
new file mode 100644
index 0000000000..401663b14b
Binary files /dev/null and b/contribs/icons/weblibre.png differ
diff --git a/contribs/icons/weblibre.svg b/contribs/icons/weblibre.svg
new file mode 100644
index 0000000000..4020336823
--- /dev/null
+++ b/contribs/icons/weblibre.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/xash3d.png b/contribs/icons/xash3d.png
new file mode 100644
index 0000000000..bdb495a625
Binary files /dev/null and b/contribs/icons/xash3d.png differ
diff --git a/contribs/icons/xash3d.svg b/contribs/icons/xash3d.svg
new file mode 100644
index 0000000000..6c05dd613a
--- /dev/null
+++ b/contribs/icons/xash3d.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/contribs/icons/yoxo.png b/contribs/icons/yoxo.png
new file mode 100644
index 0000000000..31abf6c5e4
Binary files /dev/null and b/contribs/icons/yoxo.png differ
diff --git a/contribs/icons/yoxo.svg b/contribs/icons/yoxo.svg
new file mode 100644
index 0000000000..556f89cd0a
--- /dev/null
+++ b/contribs/icons/yoxo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/resources/scripts/add_icons.py b/resources/scripts/add_icons.py
deleted file mode 100755
index 9d1a1c6484..0000000000
--- a/resources/scripts/add_icons.py
+++ /dev/null
@@ -1,352 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import re
-
-from filecmp import cmp as compare
-from itertools import chain
-from os import name as platform
-from os import system as execute
-from os.path import abspath, basename, dirname, exists, realpath
-from shutil import copyfile as copy
-from shutil import move
-from subprocess import check_output as get_output
-from sys import argv as args
-
-import requests_parser
-from resolve_paths import paths
-
-parser = argparse.ArgumentParser()
-parser.add_argument('-n', '--name', metavar='NAME', help='the name of a drawable entry')
-parser.add_argument('-c', '--compinfos', metavar='NAME', action='append', default=[], nargs='+', help='include one or multiple compinfos')
-parser.add_argument('-a', '--include-appfilter', action='store_true', help='process the appfilter.xml file')
-parser.add_argument('-d', '--include-drawable', action='store_true', help='process the drawable.xml file')
-parser.add_argument('-C', '--category', metavar='NAME', help='add to a specific category instead of the named one')
-parser.add_argument('-r', '--include-requests', action='store_true', help='process the requests.txt file')
-parser.add_argument('-i', '--include-icons', action='store_true', help='move icons to the target folders')
-parser.add_argument('-o', '--copy-icons', action='store_true', help='copy icons instead of moving')
-parser.add_argument('-f', '--force', action='store_true', help='overwrite icons in target folder')
-parser.add_argument('-g', '--git-commit', metavar='MESSAGE', help='commit local changes with a specific message')
-parser.add_argument('-D', '--dry-run', action='store_true', help='do things without changing any files')
-parser.add_argument('-I', '--ignore-errors', action='store_true', help='ignore errors')
-parser.add_argument('-P', '--path', metavar='PATH', help='set custom path to delta folder')
-parser.add_argument('-p', '--plain-text', action='store_true', help='disable colorized output')
-parser.add_argument('-v', '--verbose', action='store_true', help='more debug output')
-options = parser.parse_args()
-
-# -----------
-# definitions
-# -----------
-
-include_appfilter = options.include_appfilter
-include_drawable = options.include_drawable
-include_requests = options.include_requests
-include_icons = options.include_icons
-ignore_errors = options.ignore_errors
-copy_icons = options.copy_icons
-git_commit = options.git_commit
-plain_text = options.plain_text
-git_commit = options.git_commit
-compinfos = list(chain.from_iterable(options.compinfos)) # flatten array
-category = options.category
-dry_run = options.dry_run
-verbose = options.verbose
-force = options.force
-name = options.name
-path = options.path
-
-if path: delta_dir = abspath(path)
-else: delta_dir = dirname(realpath(__file__ + '/../..')) # if the script placed in utility_scripts
-
-work_dir = paths['scripts']
-icons_dir = paths['src']['dir']
-appfilter_files = paths['appfilter']
-drawable_files = paths['drawable']
-requests_file = paths['requests']
-icons_src_files = [
- paths['src']['png'].format(name),
- paths['src']['svg'].format(name)
-]
-icons_dst_files = [
- paths['dst']['png'].format(name),
- paths['dst']['svg'].format(name)
-]
-
-git_arguments = appfilter_files + drawable_files + [requests_file]
-
-blank = ''
-changes = False
-dry_run_prefix = 'will be ' if dry_run else blank
-
-execute(blank) # enable coloring in PowerShell and cmd (weird, I know)
-
-
-def write(file, content):
- # used to avoid repeating code
- if not dry_run:
- file.seek(0)
- file.write(content)
- file.truncate()
-
-
-class out():
- # reject stdlib logging, embrace custom logging without stderr
- reset = '\033[0m'
- def color(text=False, code=blank):
- # 0 gray / 1 red / 2 green / 3 yellow
- # 4 blue / 5 purple / 6 cyan / 7 white
- reset = '\033[0m'
- if plain_text: return text
- if code == blank: code = out.reset
- if code in range(0, 8): code = f'\033[9{code}m'
- else: code = out.reset
- if text: code = f'{code}{text}{reset}'
- return code
- def base(text, level='info', code=4):
- print(f'[{out.color(code=code, text=level)}] {text}')
- if level == 'fail': exit(1)
- info = lambda text: out.base(text)
- fail = lambda text: out.base(text + '', level='fail', code=1)
- done = lambda text: out.base(text, level='done', code=2)
- warn = lambda text: out.base(text, level='warn', code=3)
- def verb(text=False, code=0):
- if verbose:
- if not text: text = '...'
- out.base(out.color(code=code, text=text), level='verb', code=5)
- rem = color(code=1, text=f'- {{0}}')
- add = color(code=2, text=f'+ {{0}}')
-
-# ------------
-# testing zone
-# ------------
-
-test_files = appfilter_files + drawable_files + [requests_file]
-include_all = include_appfilter or include_drawable or include_icons
-
-if len(args) < 2:
- parser.print_usage()
- exit()
-
-for test_file in test_files:
- if not exists(test_file): out.fail(f"file '{test_file}' not found, maybe you forgot to set -P option (simply move the script to utility_scripts dir)")
-
-if not name:
- if include_all: out.fail('use -n option options -a/-d/-i')
- if category:
- out.fail('use -C option together with -n and -d options')
- if not include_drawable: out.warn('-C option is useless without -d option')
- if include_drawable: out.warn('-C option is useless without -d option')
-
-if name:
- if not category:
- category = name[0].upper()
- if category[0] == '_': category = '#'
- if name[0].isdigit(): out.fail(f"drawable name that starts with a number must be prefixed with '_'")
- if name[0] == '_' and name[1].isalpha(): out.fail(f"drawable name must have a leading number after '_'")
-
-if not exists(icons_dir): out.fail(f"dir '{icons_dir}' not found, create it next to the script")
-
-if include_icons and not ignore_errors:
- for test_file in icons_src_files:
- if not exists(test_file): out.fail(f"icon '{test_file}' not found")
- if not force:
- for test_file in icons_dst_files:
- if exists(test_file): out.fail(f"icon '{test_file}' exists in target folder, use -f option to overwrite it")
-
-compinfos = [*{*compinfos}] # https://stackoverflow.com/a/60518033
-
-for compinfo in compinfos:
- match = re.search(r'^(.*?)\/[\w+.$]+', compinfo)
- if not match:
- out.fail(f"compinfo '{compinfo}' not looks valid")
-
-include_all += include_requests
-
-if not dry_run: out.info('started')
-else: out.info('started in dry mode')
-
-# -----
-# main?
-# -----
-
-if include_drawable:
- filename = 'drawable.xml'
- with open(drawable_files[0], 'r+', encoding='utf-8', newline=blank) as file:
- content = file.read()
- drawable_entry = f' '
- categories = [' ', f' ']
- if category == 'New': out.fail(f"{filename}: category '{category}' can't be used")
- if not re.search(categories[1], content, re.IGNORECASE): out.fail(f"{filename}: category '{category}' not found")
- if re.search(drawable_entry, content, re.IGNORECASE): out.warn(f"{filename}: drawable '{name}' exists")
- else:
- # file.seek(0)
- # content_list = file.readlines()
- # drawables_count = 0
- # entries = []
- # category_name = category
- # for occurence, category in enumerate(categories):
- # index = False
- # scroll = False
- # stop = False
- # category_sorted = []
- # category_unsorted = []
- # for line in content_list:
- # search = re.search(re.compile(category, re.IGNORECASE), line)
- # if search:
- # categories[occurence] = search.group(0)
- # scroll = True
- # continue
- # if re.search('^\t?$', line): scroll = False
- # if scroll: category_unsorted.append(line.strip())
- # category_sorted = sorted(category_unsorted + [drawable_entry])
- # for category in category_unsorted:
- # if re.search(drawable_entry, category): stop = True
- # if stop: continue
- # for line in category_sorted:
- # if re.search(drawable_entry, line):
- # index = category_sorted.index(line)
- # break
- # if index:
- # pattern = category_sorted[index-1]
- # entries += [pattern]
- # replace = fr'{pattern}\n\t{drawable_entry}'
- # content = re.sub(pattern, replace, content, occurence + 1)
- # drawables_count += 1
- file.seek(0)
- content_list = file.readlines()
- drawables_count = 0
- category_name = category
- entries = []
-
- for category in categories:
- replace = fr'{category}\n\t{drawable_entry}'
- entries += [category]
- content = re.sub(category, replace, content)
- drawables_count += 1
-
- if drawables_count > 0:
- write(file, content)
- out.done(f'{filename}: {dry_run_prefix + "added"} 2 entries in \'New\' and \'{category_name}\'')
- out.verb()
- for id, entry in enumerate(entries):
- if verbose:
- out.verb(categories[id])
- out.verb(out.add.format(drawable_entry))
- out.verb()
- changes = True
- else: out.warn(f'{filename}: no new drawables entries to add')
-
-
-if include_appfilter:
- filename = 'appfilter.xml'
- if not compinfos: out.warn(f'{filename}: no compinfos passed')
- else:
- with open(appfilter_files[0], 'r+', encoding='utf-8', newline=blank) as file:
- content = file.read()
- pattern = ''
- replace = blank
- appfilter_entries = []
- for compinfo in compinfos:
- appfilter_entry = f' '
- compinfo_pattern = appfilter_entry.replace('$', '\\$')
- if not re.search(compinfo_pattern, content):
- appfilter_entries.append(appfilter_entry)
- replace += f'\t{appfilter_entry}\n'
- if appfilter_entries:
- content = re.sub(pattern, replace + pattern, content)
- write(file, content)
- entry = 'entry' if len(compinfos) == 1 else 'entries'
- out.done(f'{filename}: {dry_run_prefix + "added"} {len(compinfos)} {entry}')
- if verbose:
- out.verb()
- for entry in appfilter_entries: out.verb(out.add.format(entry))
- out.verb(pattern)
- out.verb()
- changes = True
- else: out.warn(f"{filename}: existing entries found")
-
-
-if include_requests:
- filename = 'requests.yml'
- if not compinfos: out.warn(f'{filename}: no compinfos passed')
- else:
- requests = requests_parser.read(requests_file)
- lines = []
- for compinfo in compinfos:
- if compinfo in requests:
- lines.append(compinfo)
- requests.pop(compinfo)
-
- lines_count = len(lines)
-
- if not dry_run: requests_parser.write(requests_file, requests)
-
- if lines_count > 0:
- entry = 'entry' if lines_count == 1 else 'entries'
- out.done(f'{filename}: {dry_run_prefix + "removed"} {lines_count} {entry}')
- if verbose:
- out.verb()
- for group in lines:
- for entry in group: out.verb(out.rem.format(entry))
- out.verb()
- changes = True
- else:
- out.warn(f'{filename}: no entries found to delete')
-
-
-try:
- if not dry_run:
- if include_appfilter:
- copy(appfilter_files[0], appfilter_files[1])
- if include_drawable:
- copy(drawable_files[0], drawable_files[1])
- if include_icons:
- message = dry_run_prefix
- message += 'copied' if copy_icons else 'moved'
- if force: message += ' with overwrite'
- formats = ['png', 'svg']
- for format in formats:
- index = formats.index(format)
- filename = f'{name}.{format}'
- source = icons_src_files[index]
- target = icons_dst_files[index]
- folder = 'vectors' if format == 'svg' else 'drawable-nodpi'
- if not exists(source): out.warn(f"{filename}: not found in '{basename(icons_dir)}'")
- elif exists(target): out.fail(f"{filename}: found in '{folder}'")
- else:
- git_arguments += [target]
- diff = compare(source, target) if exists(target) else False
- if diff:
- out.warn(f'{filename}: source and target icons are the same')
- else:
- if not dry_run:
- if copy_icons: copy(source, target)
- else: move(source, target)
- out.done(f"{filename}: '{source}' {message} to '{target}'" if verbose else f'{filename}: {message} to {folder}')
- changes = True
-
-
- if not dry_run and git_commit:
- git_command = f'cd {delta_dir} && git'
- git_arguments = ' '.join(git_arguments)
- null = '>NUL 2>NUL' if platform == 'nt' else '>/dev/null 2>&1'
- if execute(f'{git_command} add --dry-run {git_arguments} {null}') == 0: execute(f'cd {delta_dir} && git add {git_arguments}')
- if execute(f'{git_command} commit --dry-run -m "{git_commit}" {null}') == 0:
- execute(f'{git_command} commit -m "{git_commit}" {null}')
- message = ['git: commited']
- commit = get_output(f'{git_command} log -1 --pretty=format:%h', shell=True, encoding='utf-8')
- message += [out.color(code=6, text=commit)]
- if not verbose: message += ['with', get_output(f'{git_command} diff --staged --shortstat HEAD~1', shell=True, encoding='utf-8').strip()]
- out.done(' '.join(message))
- if verbose:
- diff = filter(None, get_output(f'{git_command} diff {"--color=always" if not plain_text else blank} --staged --stat HEAD~1',
- shell=True, encoding='utf-8').split('\n'))
- for line in diff: out.verb(text=line.strip(), code=out.reset)
- changes = True
- else: out.warn('git: no changes to commit')
- if not changes: out.fail('nothing to do')
- else:
- if dry_run: out.info(f'yay, I can do something!')
- else: out.info(f'yay, I did something!')
-except Exception as message:
- out.fail(message)
diff --git a/resources/scripts/add_icons_wrapper.py b/resources/scripts/add_icons_wrapper.py
deleted file mode 100755
index 363ec95dcb..0000000000
--- a/resources/scripts/add_icons_wrapper.py
+++ /dev/null
@@ -1,124 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse
-import re
-
-from os import system as execute
-from os import name as platform
-from os.path import basename, join
-from shlex import quote
-
-from requests_parser import read, write
-from resolve_paths import paths
-
-
-work_dir = paths['scripts']
-delta_dir = paths['root']
-
-add_icons = join(work_dir, 'add_icons.py')
-
-icons_file = paths['icons']
-requests_file = paths['requests']
-
-
-header = '''\
-#
-# new_icon_1:
-# - com.example/com.example.MainActivity
-#
-# new_icon_2:
-# - com.example.1/com.example.MainActivity
-# - com.example.2/com.example.MainActivity
-#
-# new_icon_2_alt_1: {}
-#
-# new_icon_3:
-# category: Google
-# compinfos:
-# - com.google.app/com.google.app.MainActivity
-'''
-
-
-parser = argparse.ArgumentParser()
-parser.add_argument('-D', '--dry-run', action='store_true', help='do things without changing any files')
-options = parser.parse_args()
-
-
-icons_yml = read(icons_file)
-
-if not icons_yml:
- print(f'{basename(icons_file)} is empty')
- exit(0)
-
-requests = read(requests_file)
-
-errors = []
-
-icons = [(key, value) for key, value in icons_yml.items()]
-
-
-command_base = f"python '{add_icons}' -P '{delta_dir}' -aidI"
-
-if options.dry_run: command_base += ' -D'
-
-
-for icon in icons:
-
- name = icon[0]
- data = icon[1]
-
- category = None
- compinfos = None
-
-
- if isinstance(data, dict):
- if len(data) == 0:
- print()
- else:
- category = data.get('category', None)
- compinfos = data.get('compinfos', None)
-
- elif isinstance(data, list):
- compinfos = icon[1]
- else:
- print('isinstance error')
- exit(2)
-
-
- command = f"{command_base} -n '{name}'"
-
- if compinfos:
- for compinfo in compinfos:
- command += f" -c '{compinfo}'"
- if compinfo in requests:
- requests.pop(compinfo)
- else:
- if re.match('^.*_alt_[0-9]+$', name):
- category = 'Alts'
-
- if category: command += f" -C '{quote(category)}'"
- print(command)
-
- print(f'[icon] {name}')
- status = execute(f'{command}')
- print()
-
-
- if status != 0:
- errors.append(name)
-
- if status == 0:
- icons_yml.pop(name)
-
-
-if errors:
- errors.sort()
- print(f'Issues with the next icons: {", ".join(errors)}')
- exit(1)
-
-if options.dry_run: exit(0)
-
-with open(icons_file, 'w', newline='') as file:
- file.write(header)
-
-write(requests_file, requests)
diff --git a/resources/scripts/bump_version.py b/resources/scripts/bump_version.py
index 740177fe21..deb75f464f 100755
--- a/resources/scripts/bump_version.py
+++ b/resources/scripts/bump_version.py
@@ -1,95 +1,106 @@
-#! /usr/bin/env python
-
-import argparse, re
-
-from os.path import join
-
-import semver
-
-from resolve_paths import paths
-
-
-argparser = argparse.ArgumentParser(description='Bump release version')
-
-argparser.add_argument('-c', '--custom',
- dest='custom',
- help='custom version name and code (format: \'name|code\')')
-argparser.add_argument('-r', '--release',
- dest='release',
- help='bump specific position',
- choices=['beta', 'promote', 'patch', 'minor', 'major'],
- default='beta')
-argparser.add_argument('-e', '--env',
- dest='env',
- help='print variables for shell exporting',
- default=False,
- action=argparse.BooleanOptionalAction)
-argparser.add_argument('-w', '--write',
- dest='write',
- help='write to file',
- default=False,
- action=argparse.BooleanOptionalAction)
-args = argparser.parse_args()
-
-
-target = join(paths['root'], 'app/build.gradle')
-
-regexp_version_code = re.compile(r'versionCode (\d+)')
-regexp_version_name = re.compile(r'versionName "((\d+\.\d+\.\d+)(-beta\.?(\d+))?)"')
-is_beta = 'true' if args.release == 'beta' else 'false'
-
-
-def build_version_code(version):
- major = str(version.major)
- minor = str(version.minor).zfill(2)
- patch = str(version.patch)
- beta = '00' if version.prerelease is None else \
- ''.join(filter(str.isdigit, version_name.prerelease)).zfill(2)
- return int(major + minor + patch + beta)
-
-
-with open(target, 'r+') as file:
- content = file.read()
-
- if args.custom:
- version_name, version_code = args.custom.split('|')
- else:
- version_code = re.search(regexp_version_code, content).group(1)
- version_name = semver.VersionInfo.parse(re.search(regexp_version_name, content).group(1))
-
- if args.release == 'promote':
- if version_name.prerelease:
- version_name = version_name.bump_prerelease(token='beta')
- version_code = build_version_code(version_name)
- version_name = version_name.finalize_version()
-
+#!/usr/bin/env python
+
+from re import compile, sub, search
+from argparse import ArgumentParser, BooleanOptionalAction
+
+from os.path import basename, join, relpath
+
+from semver import VersionInfo
+
+from shared import paths
+
+
+PATH = join(paths['root'], 'app/build.gradle')
+
+def create_parser():
+ parser = ArgumentParser()
+
+ parser.add_argument('-c', '--custom',
+ dest='custom',
+ help='custom version name and code (format: \'name|code\')')
+ parser.add_argument('-r', '--release',
+ dest='release',
+ help='bump specific position',
+ choices=['beta', 'promote', 'patch', 'minor', 'major'],
+ default='beta')
+ parser.add_argument('-e', '--env',
+ dest='env',
+ help='print variables for shell exporting',
+ default=False,
+ action=BooleanOptionalAction)
+ parser.add_argument('-i',
+ dest='input',
+ metavar='PATH',
+ help=f"path to {basename(PATH)} to process (default: '{relpath(PATH)}')",
+ default=PATH)
+ parser.add_argument('-w', '--write',
+ dest='write',
+ help='write to file',
+ default=False,
+ action=BooleanOptionalAction)
+ return parser
+
+
+def bump_version():
+
+ regexp_version_code = compile(r'versionCode (\d+)')
+ regexp_version_name = compile(r'versionName "((\d+\.\d+\.\d+)(-beta\.?(\d+))?)"')
+ is_beta = 'true' if args.release == 'beta' else 'false'
+
+ def build_version_code(version):
+ major = str(version.major)
+ minor = str(version.minor).zfill(2)
+ patch = str(version.patch)
+ beta = '00' if version.prerelease is None else \
+ ''.join(filter(str.isdigit, version_name.prerelease)).zfill(2)
+ return int(major + minor + patch + beta)
+
+ with open(args.input, 'r+') as file:
+ content = file.read()
+
+ if args.custom:
+ version_name, version_code = args.custom.split('|')
else:
- match args.release:
- case 'major': version_name = version_name.bump_major()
- case 'minor': version_name = version_name.bump_minor()
- case 'patch': version_name = version_name.bump_patch()
- case 'beta':
- if not version_name.prerelease:
- version_name = version_name.bump_minor()
+ version_code = search(regexp_version_code, content).group(1)
+ version_name = VersionInfo.parse(search(regexp_version_name, content).group(1))
+
+ if args.release == 'promote':
+ if version_name.prerelease:
version_name = version_name.bump_prerelease(token='beta')
-
- version_code = build_version_code(version_name)
-
- if not args.env:
- print(f'Name: {version_name}')
- print(f'Code: {version_code}')
-
- if args.write:
- content = re.sub(regexp_version_code, f'versionCode {version_code}', content)
- content = re.sub(regexp_version_name, f'versionName "{version_name}"', content)
- file.seek(0)
- file.write(content)
- file.truncate()
-
-
-if args.env:
- print(f'is_beta={is_beta}')
- print(f'version=v{version_name}')
- print(f'version_code={version_code}')
- print(f'version_name={version_name}')
- print(f'version_next={version_name.bump_minor()}')
+ version_code = build_version_code(version_name)
+ version_name = version_name.finalize_version()
+
+ else:
+ match args.release:
+ case 'major': version_name = version_name.bump_major()
+ case 'minor': version_name = version_name.bump_minor()
+ case 'patch': version_name = version_name.bump_patch()
+ case 'beta':
+ if not version_name.prerelease:
+ version_name = version_name.bump_minor()
+ version_name = version_name.bump_prerelease(token='beta')
+
+ version_code = build_version_code(version_name)
+
+ if args.env:
+ print(f'is_beta={is_beta}')
+ print(f'version=v{version_name}')
+ print(f'version_code={version_code}')
+ print(f'version_name={version_name}')
+ print(f'version_next={version_name.bump_minor()}')
+ else:
+ print(f'Name: {version_name}')
+ print(f'Code: {version_code}')
+
+ if args.write:
+ content = sub(regexp_version_code, f'versionCode {version_code}', content)
+ content = sub(regexp_version_name, f'versionName "{version_name}"', content)
+ file.seek(0)
+ file.write(content)
+ file.truncate()
+
+
+if __name__ == '__main__':
+ parser = create_parser()
+ args = parser.parse_args()
+ bump_version()
diff --git a/resources/scripts/count_icons.py b/resources/scripts/count_icons.py
index 346b2debb7..778c0a197f 100755
--- a/resources/scripts/count_icons.py
+++ b/resources/scripts/count_icons.py
@@ -1,92 +1,91 @@
-#! /usr/bin/env python
+#!/usr/bin/env python
-import argparse, re
import xml.etree.ElementTree as ET
-from os.path import join
+from argparse import ArgumentParser
+from re import sub
+from os.path import basename, join, relpath
-from resolve_paths import paths
+from shared import paths, transform_xml, list_categories
-target = paths['drawable'][0]
+path = paths['d1']
+cat_all = 'All'
-parser = argparse.ArgumentParser(description=f'count icons')
+def create_parser():
+ parser = ArgumentParser()
-parser.add_argument('-i', '--input',
- dest='input',
- help=f'path to drawable.xml',
- default=target)
-parser.add_argument('-c', '--category',
- dest='category',
- help=f'count icons in category',
- default=None)
-parser.add_argument('-w', '--write',
- dest='write',
- help='write to files',
- default=False,
- action=argparse.BooleanOptionalAction)
-parser.add_argument('-p', '--print',
- dest='print',
- help='output to console',
- default=False,
- action=argparse.BooleanOptionalAction)
+ parser.add_argument('-i',
+ dest='input',
+ metavar='PATH',
+ help=f"path to {basename(path)} (default: '{relpath(path)}')",
+ default=path)
+ parser.add_argument('-c',
+ dest='category',
+ metavar='NAME',
+ help=f"count number of icons in specific category (default: '{cat_all}')",
+ default=cat_all)
+ parser.add_argument('-l',
+ dest='list',
+ help=f"list available categories excluding category '{cat_all}'",
+ action='store_true')
+ parser.add_argument('-w',
+ dest='write',
+ help=f"write number of icons to CandyBar.java (only works with category '{cat_all}')",
+ action='store_true')
+ return parser
-def transform(xml):
- if '' in xml:
- xml = re.sub('\t', '', xml) # remove tags
- xml = re.sub(r'\n()', r'\1\n', xml) # remove \n before and add it after
- xml = re.sub(r'(', r'\1 />', xml) # ->
- else:
- xml = re.sub(r'/>\n(\n\t<)', r'/>\1/category>\1', xml) # add after latest
- xml = re.sub(r'()', r'\t\n\1', xml) # add before
- xml = re.sub(r'( ', r'\1>', xml) # remove slash from
- return xml
-
-
-def main(category = None):
-
- try:
- category = args.category
- except:
- pass
-
- with open(target) as file:
- xml = file.read().rstrip() # read drawable.xml to string
-
- root = ET.fromstring(transform(xml)) # transform to true XML format and convert it from string to ET
-
- if category:
- try:
- category = len(root.find(f'.//category[@title="{category}"]'))
- except:
- print(f'{category} does not exist')
- exit(1)
- return category
- else:
- count = 0
- for category in root.findall('category'):
- count += len(category)
- count -= len(root.find(f'.//category[@title="New"]'))
-
- try:
- if args.write:
- for type in ['play', 'foss']:
- path = join(paths['root'], f'app/src/{type}/java/website/leifs/delta/applications/CandyBar.java')
- with open(path, 'r+') as file:
- content = file.read()
- content = re.sub(r'setCustomIconsCount\(.*\)', f'setCustomIconsCount({count})', content)
- file.seek(0)
- file.write(content)
- file.truncate()
- except:
- pass
+def count_icons(input=path, category=cat_all, write=False, list_cats=False):
+
+ categories = list_categories(input)
+
+ if list_cats:
+ print('\n'.join(['- ' + x for x in categories]))
+ exit()
+
+ with open(input) as file:
+ xml = transform_xml(file.read().rstrip())
+ root = ET.fromstring(xml)
+
+ elements = root.findall('category')
+ category = category.capitalize()
+ categories = [cat_all] + categories
+
+ count = 0
+
+ if category not in categories:
return count
-
+
+ if category == cat_all:
+ for element in elements:
+ if element.get('title') in [cat_all, 'New']:
+ continue
+ count += len(element)
+
+ if write:
+ for type in ['play', 'foss']:
+ java_path = join(paths['root'], f'app/src/{type}/java/website/leifs/delta/applications/CandyBar.java')
+ with open(java_path, 'r+') as file:
+ content = file.read().rstrip()
+ content = sub(r'setCustomIconsCount\(.*\)', f'setCustomIconsCount({count})', content)
+ file.seek(0)
+ file.write(content)
+ file.truncate()
+ else:
+ count = len(root.find(f'category[@title="{category}"]'))
+
+ return count
+
if __name__ == '__main__':
+ parser = create_parser()
args = parser.parse_args()
- count = main()
- if args.print:
- print(count)
\ No newline at end of file
+ count = count_icons(
+ input=args.input,
+ category=args.category,
+ write=args.write,
+ list_cats=args.list,
+ )
+ print(count)
\ No newline at end of file
diff --git a/resources/scripts/create_changelog.py b/resources/scripts/create_changelog.py
index b06ea20daa..eb2c9472e7 100755
--- a/resources/scripts/create_changelog.py
+++ b/resources/scripts/create_changelog.py
@@ -1,50 +1,53 @@
-#! /usr/bin/env python
+#!/usr/bin/env python
-import html, re
-import base64
import xml.etree.ElementTree as ET
-import argparse
-from count_icons import main as count_icons
-
-
-parser = argparse.ArgumentParser(description=f'create changelog')
-
-parser.add_argument('-d', '--data',
- dest='data',
- help='changelog b64 encoded')
-parser.add_argument('-r', '--release-type',
- dest='release_type',
- help='type of release',
- default='beta')
-parser.add_argument('-p', '--print',
- dest='print',
- help='output to console',
- default=False,
- action=argparse.BooleanOptionalAction)
-parser.add_argument('-w', '--write',
- dest='write',
- help='write to files',
- default=False,
- action=argparse.BooleanOptionalAction)
-parser.add_argument('-t', '--txt',
- dest='txt',
- help='path to txt changelog',
- default='changelog.txt')
-parser.add_argument('-x', '--xml',
- dest='xml',
- help='path to xml changelog',
- default='changelog.xml')
-args = parser.parse_args()
+
+from argparse import ArgumentParser
+from base64 import b64decode
+from html import unescape
+from re import sub
+
+from count_icons import count_icons
+
+
+def create_parser():
+ parser = ArgumentParser(description=f'create changelog')
+
+ parser.add_argument('-d', '--data',
+ dest='data',
+ help='changelog b64 encoded')
+ parser.add_argument('-r', '--release-type',
+ dest='release_type',
+ help='type of release',
+ choices=['prod', 'beta', 'foss'],
+ default='beta')
+ parser.add_argument('-p', '--print',
+ dest='print',
+ help='output to console',
+ action='store_true')
+ parser.add_argument('-w', '--write',
+ dest='write',
+ help='write to files',
+ action='store_true')
+ parser.add_argument('-t', '--txt',
+ dest='txt',
+ help='path to txt changelog',
+ default='changelog.txt')
+ parser.add_argument('-x', '--xml',
+ dest='xml',
+ help='path to xml changelog',
+ default='changelog.xml')
+ return parser
def main():
icons_total = count_icons()
- icons_new = count_icons('New')
+ icons_new = count_icons(category='New')
txt = []
try:
- data = base64.b64decode(args.data).decode()
+ data = b64decode(args.data).decode()
except:
data = ''
@@ -75,19 +78,19 @@ def main():
for line in data.splitlines():
line = line.strip()
if not line: continue
- line = re.sub('^-+', '', line).strip()
+ line = sub('^-+', '', line).strip()
ET.SubElement(changelog, 'item').text = line
txt.append('- ' + line)
tree = ET.ElementTree(resources)
- ET.indent(tree, space="\t", level=0)
+ ET.indent(tree, space='\t', level=0)
tree = tree.getroot()
xml = ET.tostring(tree, encoding='unicode', xml_declaration=True, short_empty_elements=False)
- xml = html.unescape(re.sub(r"(\w+)='(.*?)'", r'\1="\2"', xml))
+ xml = unescape(sub(r"(\w+)='(.*?)'", r'\1="\2"', xml))
if args.print: print(xml)
@@ -99,4 +102,6 @@ def main():
file.write('\n'.join(txt))
if __name__ == '__main__':
+ parser = create_parser()
+ args = parser.parse_args()
main()
\ No newline at end of file
diff --git a/resources/scripts/email_dumper.py b/resources/scripts/email_dumper.py
deleted file mode 100755
index 4271ea8ecd..0000000000
--- a/resources/scripts/email_dumper.py
+++ /dev/null
@@ -1,73 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse, re
-
-from datetime import datetime
-from os import mkdir
-from os.path import exists, join
-
-from redbox import EmailBox as redbox
-
-from resolve_paths import paths
-
-parser = argparse.ArgumentParser(description='Dump a IMAP folder into .eml files')
-parser.add_argument('-s', '--host',
- dest='host',
- help='IMAP host',
- default='imap.gmail.com')
-parser.add_argument('-P', '--port',
- dest='port',
- help='IMAP port',
- default=993)
-parser.add_argument('-u', '--username',
- dest='username',
- help='IMAP username',
- required=True)
-parser.add_argument('-p', '--password',
- dest='password',
- help='IMAP password',
- required=True)
-parser.add_argument('-r', '--remote',
- dest='remote',
- help='Remote folder to download',
- default='INBOX')
-parser.add_argument('-l', '--local',
- dest='local',
- help='Local folder where to save .eml files',
- default=join(paths['scripts'], 'emails'))
-parser.add_argument('-U', '--unread',
- dest='unread',
- help='Keep emails unread in the inbox',
- default=False,
- action=argparse.BooleanOptionalAction)
-
-args = parser.parse_args()
-
-mail = redbox(host=args.host,
- port=args.port,
- username=args.username,
- password=args.password)
-
-messages = mail[args.remote].search(unseen=True)
-date_now = datetime.today().strftime('%Y-%m-%d %H:%M:%S')
-
-print(f'[{date_now}] New {len(messages)} requests!')
-
-if not exists(args.local):
- mkdir(args.local)
-
-
-for message in messages:
- try:
- index = messages.index(message) + 1
- output = f'{args.local}/{index}.eml'
- date = message.date.strftime('%Y-%m-%d %H:%M:%S')
- compinfo = re.search('(.*)\nhttp', message.text_body).group(1).strip()
- print(f'[{date}] [{index}] {compinfo}')
- with open(output, 'w', newline='') as file:
- file.write(message.content)
- except:
- continue
- finally:
- if args.unread:
- message.unread()
\ No newline at end of file
diff --git a/resources/scripts/email_parser.py b/resources/scripts/email_parser.py
deleted file mode 100755
index 9ad0d1e2db..0000000000
--- a/resources/scripts/email_parser.py
+++ /dev/null
@@ -1,98 +0,0 @@
-#!/usr/bin/env python3
-
-import base64
-import copy
-import io
-import re
-import os
-
-from datetime import datetime
-from hashlib import sha1
-from zipfile import ZipFile
-
-import mailparser
-import xmltodict
-
-from requests_parser import read, write
-from resolve_paths import paths
-
-DATE_NOW = datetime.now()
-
-EMAILS_DIR = os.path.join(paths['scripts'], 'emails')
-REQUESTS_FILE = paths['requests']
-
-GREEDY_COUNTER = 1
-REQUEST_LIFETIME = 90 # days
-
-STORES = [
- 'https://play.google.com/store/apps/details?id={}',
- 'https://f-droid.org/en/packages/{}/',
- # 'https://apkpure.com/delta/{}/',
- 'https://google.com/search?q={}',
-]
-
-greedy_users = {}
-
-requests_diff = read(REQUESTS_FILE)
-requests = copy.deepcopy(requests_diff)
-
-
-def decode_zip(data):
- decoded = base64.urlsafe_b64decode(data.encode('UTF-8'))
- return ZipFile(io.BytesIO(decoded))
-
-
-for key, value in requests_diff.items():
- insertion_date = value['reql']
- delta = DATE_NOW - insertion_date
- if delta.days > REQUEST_LIFETIME:
- requests.pop(key)
- print(f'[DEL] [{insertion_date}] {key}')
-
-
-for file in os.listdir(EMAILS_DIR):
- file = os.path.join(EMAILS_DIR, file)
-
- if not file.endswith('.eml'): continue
- if not os.path.isfile(file): continue
-
- try:
- with open(file, 'rb') as file:
- mail = mailparser.parse_from_bytes(file.read())
-
- sender = mail.from_[0][1]
-
- if sender in greedy_users:
- if greedy_users[sender] > GREEDY_COUNTER: continue
-
- date = mail.date
-
- attachments = decode_zip(mail.attachments[0]['payload'])
- xml = xmltodict.parse(attachments.read('appfilter.xml'), process_comments=True)['resources']
-
- name = xml['#comment']
- compinfo = re.search('ComponentInfo{(.*)}', xml['item']['@component'], re.IGNORECASE).group(1)
- id = compinfo.split('/')[0]
-
- if sender not in greedy_users: greedy_users[sender] = 1
-
- if compinfo not in requests:
- requests[compinfo] = {
- 'name': name,
- 'reqt': 1,
- 'reql': date,
- 'hash': sha1(compinfo.encode()).hexdigest(),
- 'urls': [url.format(id) for url in STORES]
- }
- print(f'[NEW] [{date}] {compinfo}')
- else:
- greedy_users[sender] += 1
- requests[compinfo]['reqt'] += 1
- if requests[compinfo]['reql'] < date:
- requests[compinfo]['reql'] = date
- print(f'[N++] [{date}] {compinfo}')
- except:
- continue
-
-
-write(REQUESTS_FILE, requests)
diff --git a/resources/scripts/process_emails.py b/resources/scripts/process_emails.py
new file mode 100755
index 0000000000..bb161abf5d
--- /dev/null
+++ b/resources/scripts/process_emails.py
@@ -0,0 +1,218 @@
+#!/usr/bin/env python
+
+from argparse import ArgumentParser
+from re import compile, search, IGNORECASE
+
+from base64 import urlsafe_b64decode
+from copy import deepcopy
+from datetime import datetime
+from hashlib import sha1
+from io import BytesIO
+from os import listdir, mkdir
+from os.path import exists, isfile, join, relpath
+from sys import argv
+from zipfile import ZipFile
+from xmltodict import parse as xmlparse
+
+from html2text import HTML2Text
+from mailparser import parse_from_bytes
+from redbox import EmailBox as redbox
+
+from shared import paths, stores
+from process_requests import read, write
+
+
+GREEDY_COUNTER = 1
+REQUEST_LIFETIME = 90 # days
+
+HOST = 'imap.gmail.com'
+PORT = 993
+LOCAL = paths['emails']
+REMOTE = 'Requests'
+
+
+def create_parser():
+ parser = ArgumentParser()
+
+ parser.add_argument('-d',
+ dest='dump',
+ help='dump emails',
+ action='store_true')
+ parser.add_argument('--host',
+ dest='host',
+ help=f"IMAP host (default: '{HOST}')",
+ default=HOST)
+ parser.add_argument('--port',
+ dest='port',
+ help=f"IMAP port (default: '{PORT}')",
+ default=PORT)
+ parser.add_argument('--user',
+ dest='username',
+ help='IMAP username',
+ required='-d' in argv)
+ parser.add_argument('--pass',
+ dest='password',
+ help='IMAP password',
+ required='-d' in argv)
+ parser.add_argument('--remote',
+ dest='remote',
+ help=f"remote email folder to parse (default: '{REMOTE}')",
+ default='Requests')
+ parser.add_argument('--unread',
+ dest='unread',
+ help='keep emails unread in remote folder',
+ action='store_true')
+ parser.add_argument('--local',
+ dest='local',
+ help=f"local folder where to dump/parse emails (default: '{relpath(LOCAL)}')",
+ default=paths['emails'])
+ parser.add_argument('-p',
+ dest='parse',
+ help='parse dumped emails',
+ action='store_true')
+ return parser
+
+def dump_emails(
+ host=HOST,
+ port=PORT,
+ username=None,
+ password=None,
+ local=LOCAL,
+ remote=REMOTE,
+ ):
+
+ if not username or not password:
+ print('username/password cannot be None')
+ exit(1)
+
+ mail = redbox(
+ host=host,
+ port=port,
+ username=username,
+ password=password,
+ )
+
+ messages = mail[args.remote].search(unseen=True)
+ date_now = datetime.today().strftime('%Y-%m-%d %H:%M:%S')
+
+ if len(messages) > 0:
+ print(f'[{date_now}] Found {len(messages)} emails')
+ else:
+ print(f'[{date_now}] No new requests!')
+ exit(0)
+
+ if not exists(args.local):
+ mkdir(args.local)
+
+ for index, message in enumerate(messages, 1):
+ try:
+ output = join(args.local, f'{index}.eml')
+ date = message.date.strftime('%Y-%m-%d %H:%M:%S')
+ with open(output, 'w', newline='') as file:
+ file.write(message.content)
+ print(f'[{date}] Saved to {index}.eml')
+ except Exception:
+ continue
+ finally:
+ if args.unread:
+ message.unread()
+
+
+def parse_emails(local=LOCAL):
+
+ date_now = datetime.now()
+
+ requests_file = paths['requests']
+
+ greedy_users = {}
+
+ requests_diff = read(requests_file)
+ requests = deepcopy(requests_diff)
+
+ html2text = HTML2Text()
+ html2text.ignore_links = True
+ pattern_ci = compile(r'^[a-zA-Z0-9._]+/[a-zA-Z0-9._$]')
+
+ def decode_zip(data):
+ decoded = urlsafe_b64decode(data.encode('UTF-8'))
+ return ZipFile(BytesIO(decoded))
+
+
+ for key, value in requests_diff.items():
+ insertion_date = value['reql']
+ delta = date_now - insertion_date
+ if delta.days > REQUEST_LIFETIME:
+ requests.pop(key)
+ print(f'[DEL] [{insertion_date}] {key}')
+
+
+ for file in listdir(local):
+ file = join(local, file)
+
+ if not file.endswith('.eml'): continue
+ if not isfile(file): continue
+
+ try:
+ with open(file, 'rb') as file:
+ mail = parse_from_bytes(file.read())
+
+ sender = mail.from_[0][1]
+
+ if sender in greedy_users:
+ if greedy_users[sender] > GREEDY_COUNTER: continue
+
+ date = mail.date.replace(tzinfo=None)
+
+ if mail.attachments:
+ attachments = decode_zip(mail.attachments[0]['payload'])
+ xml = xmlparse(attachments.read('appfilter.xml'), process_comments=True)['resources']
+ name = xml['#comment']
+ compinfo = search('ComponentInfo{(.*)}', xml['item']['@component'], IGNORECASE).group(1)
+ else:
+ body_plain = html2text.handle(mail.body).strip().split()
+ compinfo = [*{*list(filter(lambda s: pattern_ci.search(s), body_plain))}][0]
+
+ id = compinfo.split('/')[0]
+
+ if sender not in greedy_users: greedy_users[sender] = 1
+
+ if compinfo not in requests:
+ requests[compinfo] = {
+ 'name': name,
+ 'reqt': 1,
+ 'reql': date,
+ 'hash': sha1(compinfo.encode()).hexdigest(),
+ 'urls': [url.format(id) for url in stores]
+ }
+ print(f'[NEW] [{date}] {compinfo}')
+ else:
+ greedy_users[sender] += 1
+ requests[compinfo]['reqt'] += 1
+ if requests[compinfo]['reql'] < date:
+ requests[compinfo]['reql'] = date
+ print(f'[N++] [{date}] {compinfo}')
+ except Exception as error:
+ # print(file, error)
+ continue
+
+ write(requests_file, requests)
+
+
+if __name__ == '__main__':
+ parser = create_parser()
+ args = parser.parse_args()
+
+ if args.dump:
+ dump_emails(
+ host=args.host,
+ port=args.port,
+ username=args.username,
+ password=args.password,
+ local=args.local,
+ remote=args.remote,
+ )
+
+ if args.parse:
+ parse_emails(
+ local=args.local
+ )
\ No newline at end of file
diff --git a/resources/scripts/process_icons.py b/resources/scripts/process_icons.py
new file mode 100755
index 0000000000..3e0c45ea61
--- /dev/null
+++ b/resources/scripts/process_icons.py
@@ -0,0 +1,495 @@
+#!/usr/bin/env python
+
+import re
+import logging as log
+import xml.etree.ElementTree as ET
+
+from argparse import ArgumentParser
+from copy import deepcopy
+from os import environ, unlink
+from os.path import basename, isfile, relpath
+from shutil import copy, move
+
+from natsort import natsorted as sorted
+from termcolor import colored as color
+
+from process_requests import read
+from shared import paths, transform_xml
+
+ACTIONS = ('add', 'rewrite', 'rebrand', 'remove')
+
+PATTERN_ALT = re.compile(r'^.*_alt_\d+$')
+PATTERN_ALT_X = re.compile(r'^(.+)_alt_x\d+$')
+PATTERN_CI = re.compile(r'^[a-zA-Z0-9._]+/[a-zA-Z0-9._$]+$')
+PATTERN_DRAWABLE = re.compile(r'^[a-z0-9_]+$')
+
+ICONS_HEADER = '''\
+#
+# options:
+# https://github.com/Delta-Icons/android/blob/master/CONTRIBUTING.md#options
+#
+# examples:
+# https://github.com/Delta-Icons/android/blob/master/CONTRIBUTING.md#examples
+#
+'''
+
+args = ArgumentParser()
+args.add_argument('-d', '--dry-run', action='store_true', help='do things without changing any files')
+args.add_argument('-v', '--verbose', action='store_true', help='verbose')
+args.add_argument('-n', '--no-color', action='store_true', help='disable colors')
+args.add_argument('-s', '--sort', action='store_true', help='sort xml files only, skip icons.yml')
+args = args.parse_args()
+
+if args.no_color:
+ environ['NO_COLOR'] = '1'
+
+log.addLevelName(log.DEBUG, color('d', 'magenta'))
+log.addLevelName(log.INFO, color('i', 'blue'))
+log.addLevelName(log.WARNING, color('w', 'yellow'))
+log.addLevelName(log.ERROR, color('e', 'red'))
+log.addLevelName(log.CRITICAL, color('c', 'red'))
+log.basicConfig(
+ level=log.DEBUG if args.verbose else log.INFO,
+ datefmt='%H:%M:%S',
+ format='%(levelname)s %(message)s',
+)
+
+
+def log_sep(name):
+ print(color(f'\n<------- {name} ------->', 'dark_grey'))
+
+
+def xml_parser():
+ return ET.XMLParser(target=ET.TreeBuilder(insert_comments=True))
+
+
+def write_file(target, content):
+ with open(target, 'w') as file:
+ file.write(content)
+
+
+def parse_action(raw):
+ match raw:
+ case True:
+ return 'rebrand', None, None
+ case False | None:
+ return 'add', None, None
+ case str():
+ if '>' in raw:
+ action, _, target = raw.partition('>')
+ action, target = action.strip(), target.strip()
+ if action == 'rename':
+ if not target:
+ return 'add', None, f"rename requires a target, e.g. {color('rename > new_name', 'cyan')}"
+ return 'rename', target, None
+ if action == 'move':
+ if not target:
+ return 'add', None, f"move requires a target category, e.g. {color('move > google', 'cyan')}"
+ return 'move', target, None
+ return 'add', None, f"action {color(action, 'cyan')} doesn't support {color('>', 'cyan')} syntax"
+ if raw == 'rename':
+ return 'add', None, f"rename requires a target, e.g. {color('rename > new_name', 'cyan')}"
+ if raw == 'move':
+ return 'add', None, f"move requires a target category, e.g. {color('move > google', 'cyan')}"
+ if raw in ACTIONS:
+ return raw, None, None
+ expected = ', '.join((*ACTIONS, 'rename > name', 'move > category'))
+ return 'add', None, f"action {color(raw, 'cyan')} is invalid, expected: {color(expected, 'cyan')}"
+ case _:
+ expected = ', '.join((*ACTIONS, 'rename > name', 'move > category'))
+ return 'add', None, f"action {color(str(raw), 'cyan')} is invalid, expected: {color(expected, 'cyan')}"
+
+
+def parse_compinfos(value):
+ result = []
+ for key in ('compinfo', 'compinfos'):
+ entries = value.get(key, [])
+ if isinstance(entries, list):
+ result.extend(entries)
+ else:
+ result.append(entries)
+ return result
+
+
+def check_source_files(key, extensions=('png', 'svg')):
+ missing = []
+ for ext in extensions:
+ if not isfile(paths['src'][ext].format(key)):
+ missing.append(ext)
+ return missing
+
+
+def exists_in_dst(key):
+ return any(isfile(paths['dst'][fmt].format(key)) for fmt in ('png', 'svg'))
+
+
+def move_images(src_name, dst_name=None, from_dst=False, header=True):
+ if dst_name is None:
+ dst_name = src_name
+ src_paths = paths['dst'] if from_dst else paths['src']
+ if header:
+ found = any(isfile(src_paths[fmt].format(src_name)) for fmt in ('png', 'svg'))
+ if found:
+ log.info(f"images{color(':', 'dark_grey')}")
+ for fmt in ('png', 'svg'):
+ src = src_paths[fmt].format(src_name)
+ dst = paths['dst'][fmt].format(dst_name)
+ if isfile(src):
+ if not args.dry_run:
+ move(src, dst)
+ print(f" {color(f'~ {relpath(src)} -> {relpath(dst)}', 'blue')}")
+
+
+def remove_images(name):
+ if exists_in_dst(name):
+ log.info(f"images{color(':', 'dark_grey')}")
+ for fmt in ('png', 'svg'):
+ path = paths['dst'][fmt].format(name)
+ if isfile(path):
+ if not args.dry_run:
+ unlink(path)
+ print(f" {color(f'– {relpath(path)}', 'red')}")
+
+
+def parse_icons(icons_yml, root_drawable):
+ entries = []
+ errors = dict()
+ resolved_alts = {}
+
+ categories = [
+ x.get('title')
+ for x in root_drawable.findall('category')
+ if x.get('title') != 'New'
+ ]
+
+ for key, value in icons_yml.items():
+ entry_errors = []
+
+ if not PATTERN_DRAWABLE.fullmatch(key):
+ entry_errors.append(f"name {color(key, 'cyan')} looks invalid")
+ errors[key] = True
+
+ original_key = None
+ alt_x_match = PATTERN_ALT_X.fullmatch(key)
+ if alt_x_match:
+ base_name = alt_x_match.group(1)
+ if base_name not in resolved_alts:
+ max_alt = 0
+ for cat in root_drawable.findall('category'):
+ for item in cat.findall('item'):
+ d = item.get('drawable', '')
+ if d.startswith(base_name + '_alt_'):
+ try:
+ max_alt = max(max_alt, int(d.rsplit('_', 1)[1]))
+ except ValueError:
+ pass
+ resolved_alts[base_name] = max_alt
+ resolved_alts[base_name] += 1
+ original_key = key
+ key = f'{base_name}_alt_{resolved_alts[base_name]}'
+
+ action = 'add'
+ rename = None
+ compinfos = []
+
+ category = key[0].upper()
+ if key.startswith('_'):
+ category = '#'
+ if PATTERN_ALT.fullmatch(key):
+ category = 'Alts'
+
+ match value:
+ case dict():
+ compinfos = parse_compinfos(value)
+ category = value.get('category', category).capitalize()
+ action, rename, action_error = parse_action(value.get('action', 'add'))
+ if action_error:
+ entry_errors.append(action_error)
+ errors[key] = True
+ if action == 'remove':
+ if not exists_in_dst(key):
+ entry_errors.append(f"action is {color('remove', 'cyan')}, but {key} doesn't exist in destination")
+ errors[key] = True
+ if action == 'move':
+ category = rename.capitalize()
+ rename = None
+ elif rename:
+ if not PATTERN_DRAWABLE.fullmatch(rename):
+ entry_errors.append(f"rename target {color(rename, 'cyan')} looks invalid")
+ errors[key] = True
+ elif not exists_in_dst(key):
+ entry_errors.append(f"action is {color('rename', 'cyan')}, but {key} doesn't exist in destination")
+ errors[key] = True
+ elif 'category' not in value:
+ category = rename[0].upper()
+ if rename.startswith('_'):
+ category = '#'
+ if PATTERN_ALT.fullmatch(rename):
+ category = 'Alts'
+ if action in ('remove', 'move') and compinfos:
+ entry_errors.append(f"action {color(action, 'cyan')} doesn't support compinfos")
+ errors[key] = True
+ if action in ('remove', 'move') and 'category' in value:
+ entry_errors.append(f"action {color(action, 'cyan')} doesn't support explicit category")
+ errors[key] = True
+ if original_key and action != 'add':
+ entry_errors.append(f"_alt_x naming requires action {color('add', 'cyan')}, got {color(action, 'cyan')}")
+ errors[key] = True
+ case list():
+ compinfos = list(dict.fromkeys(value))
+ case str():
+ compinfos = [value]
+
+ if category not in categories and action != 'remove':
+ entry_errors.append(f"category {color(category.lower(), 'cyan')} doesn't exist")
+ errors[key] = True
+
+ source_key = original_key or key
+ if action in ('rewrite', 'rebrand'):
+ for ext in check_source_files(source_key):
+ entry_errors.append(f"action is {color(action, 'cyan')}, but {source_key}.{ext} not found")
+ errors[key] = True
+
+ compinfos = list(dict.fromkeys(x for x in compinfos if isinstance(x, str) and x))
+
+ if not compinfos and category == 'Alts' and action not in ('rename', 'remove', 'move'):
+ for ext in check_source_files(source_key):
+ entry_errors.append(f"category is Alts but {source_key}.{ext} not found")
+ errors[key] = True
+
+ for compinfo in compinfos:
+ if not PATTERN_CI.fullmatch(compinfo):
+ entry_errors.append(f"compinfo {color(compinfo, 'cyan')} looks invalid")
+ errors[key] = True
+
+ if entry_errors:
+ log_sep(key)
+ for error in entry_errors:
+ log.error(error)
+
+ entries.append((key, {
+ 'category': category,
+ 'compinfos': compinfos,
+ 'action': action,
+ 'rename': rename,
+ 'original': original_key,
+ }))
+
+ if errors:
+ log_sep('result')
+ log.critical('fix issues with the next icons:\n' + '\n'.join(
+ ' - ' + color(x, 'magenta') for x in errors
+ ))
+ print()
+ exit(1)
+
+ return entries
+
+
+def main():
+ if not args.sort:
+ icons_yml = read(paths['icons'])
+
+ if not icons_yml:
+ log.warning(f'{basename(paths["icons"])} is empty')
+
+ with open(paths['d1']) as file:
+ xml_drawable = file.read().rstrip()
+
+ with open(paths['a1']) as file:
+ xml_appfilter = file.read().rstrip()
+
+ root_drawable = ET.fromstring(transform_xml(xml_drawable), parser=xml_parser())
+ root_appfilter = ET.fromstring(xml_appfilter, parser=xml_parser())
+
+ scale = root_appfilter[0]
+ root_appfilter.remove(scale)
+
+ category_new = root_drawable.find("category[@title='New']")
+ category_alt = root_drawable.find("category[@title='Alts']")
+
+ if args.sort:
+ log.info('sorting xml files')
+
+ for drawable, values in (parse_icons(icons_yml, root_drawable) if not args.sort else []):
+ log_sep(drawable)
+
+ if values['original']:
+ log.info(f"action {color('=', 'dark_grey')} {color('rename', 'magenta')}{color(':', 'dark_grey')} {color(values['original'], 'cyan')} {color('->', 'dark_grey')} {color(drawable, 'cyan')}")
+
+ renamed_drawable = []
+ renamed_appfilter = []
+ added_drawable = []
+ old_drawable = drawable
+ move_from = None
+
+ if values['rename']:
+ new_name = values['rename']
+ log.info(f"action {color('=', 'dark_grey')} {color('rename', 'magenta')}{color(':', 'dark_grey')} {color(drawable, 'cyan')} {color('->', 'dark_grey')} {color(new_name, 'cyan')}")
+
+ for item in root_appfilter.findall('item'):
+ if item.get('drawable') == drawable:
+ renamed_appfilter.append((ET.tostring(item).decode().strip(), new_name))
+ item.set('drawable', new_name)
+
+ for cat in root_drawable.findall('category'):
+ for item in list(cat.findall('item')):
+ if item.get('drawable') == drawable:
+ renamed_drawable.append(ET.tostring(item).decode().strip())
+ cat.remove(item)
+
+ drawable = new_name
+ elif values['action'] == 'move':
+ move_from = next(
+ (cat.get('title').lower() for cat in root_drawable.findall('category')
+ for item in cat.findall('item') if item.get('drawable') == drawable),
+ '?'
+ )
+ log.info(f"action {color('=', 'dark_grey')} {color('move', 'magenta')}{color(':', 'dark_grey')} {color(move_from, 'cyan')} {color('->', 'dark_grey')} {color(values['category'].lower(), 'cyan')}")
+ else:
+ log.info(f"action {color('=', 'dark_grey')} {color(values['action'], 'magenta')}")
+
+ cat_name = values['category'].lower()
+ add_new = drawable not in [item.get('drawable') for item in category_new]
+
+ if values['action'] not in ('remove', 'move'):
+ adds_to_new = values['action'] in ('add', 'rename') or (values['action'] == 'rewrite' and add_new)
+ if adds_to_new:
+ log.info(f"categories {color('=', 'dark_grey')} {color(f'new, {cat_name}', 'magenta')}")
+ else:
+ log.info(f"category {color('=', 'dark_grey')} {color(cat_name, 'magenta')}")
+
+ match values['action']:
+ case 'rename':
+ move_images(old_drawable, drawable, from_dst=True)
+ case 'move':
+ for cat in root_drawable.findall('category'):
+ for item in list(cat.findall('item')):
+ if item.get('drawable') == drawable:
+ renamed_drawable.append(ET.tostring(item).decode().strip())
+ cat.remove(item)
+ case 'remove':
+ remove_images(drawable)
+ removed_drawable = []
+ for cat in root_drawable.findall('category'):
+ for item in list(cat.findall('item')):
+ if item.get('drawable') == drawable:
+ removed_drawable.append(ET.tostring(item).decode().strip())
+ cat.remove(item)
+ if removed_drawable:
+ log.info(f"drawable.xml{color(':', 'dark_grey')}")
+ for entry in removed_drawable:
+ print(f" {color(f'– {entry}', 'red')}")
+ removed_appfilter = []
+ for item in list(root_appfilter.findall('item')):
+ if item.get('drawable') == drawable:
+ removed_appfilter.append(ET.tostring(item).decode().strip())
+ root_appfilter.remove(item)
+ if removed_appfilter:
+ log.info(f"appfilter.xml{color(':', 'dark_grey')}")
+ for entry in removed_appfilter:
+ print(f" {color(f'– {entry}', 'red')}")
+ case 'rebrand':
+ counter = 1
+ targets = []
+ for item in root_appfilter.findall('item'):
+ if item.get('drawable') == drawable:
+ targets.append(item)
+ if item.get('drawable').startswith(drawable + '_alt_'):
+ alt_num = int(re.split(r'_', item.get('drawable'))[-1]) + 1
+ if alt_num > counter:
+ counter = alt_num
+ new_name = f'{drawable}_alt_{counter}'
+ if values['compinfos']:
+ for target in targets:
+ renamed_appfilter.append((ET.tostring(target).decode().strip(), new_name))
+ target.set('drawable', new_name)
+ item = ET.Element('item')
+ item.set('drawable', new_name)
+ item.tail = '\n\t'
+ category_alt.append(item)
+ move_images(drawable, new_name, from_dst=True)
+ move_images(drawable, header=False)
+ added_drawable.append(f' {color("(alts)", "dark_grey")}')
+ case 'add' | 'rewrite':
+ move_images(values['original'] or drawable, drawable)
+
+ if values['action'] not in ('remove',):
+ category = root_drawable.find(f"category[@title='{values['category']}']")
+ add_drawable = drawable not in [item.get('drawable') for item in category]
+
+ if renamed_drawable or added_drawable or add_drawable or (values['action'] == 'rewrite' and add_new):
+ log.info(f"drawable.xml{color(':', 'dark_grey')}")
+ for entry in renamed_drawable:
+ if values['action'] == 'move':
+ print(f" {color(f'– {entry}', 'red')} {color(f'({move_from})', 'dark_grey')}")
+ else:
+ print(f" {color(f'– {entry}', 'red')}")
+ for entry in added_drawable:
+ print(f" {color(f'+ {entry}', 'green')}")
+ if add_drawable:
+ item = ET.Element('item')
+ item.set('drawable', drawable)
+ item.tail = '\n\t'
+ category.append(item)
+ item_str = ET.tostring(item).decode().strip()
+ if values['action'] == 'move':
+ print(f" {color(f'+ {item_str}', 'green')} {color(f'({cat_name})', 'dark_grey')}")
+ else:
+ category_new.append(deepcopy(item))
+ print(f" {color(f'+ {item_str}', 'green')} {color('(new)', 'dark_grey')}")
+ print(f" {color(f'+ {item_str}', 'green')} {color(f'({cat_name})', 'dark_grey')}")
+ elif values['action'] == 'rewrite' and add_new:
+ item = ET.Element('item')
+ item.set('drawable', drawable)
+ item.tail = '\n\t'
+ category_new.append(item)
+ print(f" {color(f'+ {ET.tostring(item).decode()}', 'green')} {color('(new)', 'dark_grey')}")
+
+ if renamed_appfilter or values['compinfos']:
+ log.info(f"appfilter.xml{color(':', 'dark_grey')}")
+ for old_entry, new_drawable in renamed_appfilter:
+ print(f" {color(f'– {old_entry}', 'red')}")
+ new_entry = old_entry.replace(f'drawable="{old_drawable}"', f'drawable="{new_drawable}"')
+ print(f" {color(f'+ {new_entry}', 'green')}")
+
+ for compinfo in values['compinfos']:
+ component = f'ComponentInfo{{{compinfo}}}'
+ if any(item.get('drawable') == drawable and item.get('component') == component for item in root_appfilter):
+ log.warning(f"duplicate ci: {color(compinfo, 'cyan')}")
+ continue
+ item = ET.Element('item')
+ item.set('component', component)
+ item.set('drawable', drawable)
+ print(f" {color(f'+ {ET.tostring(item).decode()}', 'green')}")
+ item.tail = '\n\t'
+ root_appfilter.append(item)
+
+ for category in root_drawable.findall('category'):
+ category[:] = sorted(category.findall('item'), key=lambda item: item.get('drawable'))
+
+ xml_drawable = ET.tostring(root_drawable, encoding='unicode', short_empty_elements=True, xml_declaration=True)
+ xml_drawable = re.sub("'", '"', transform_xml(xml_drawable))
+
+ root_appfilter[:] = sorted(root_appfilter, key=lambda item: (item.tag, item.get('component')))
+ root_appfilter.insert(0, scale)
+
+ ET.indent(root_appfilter, space='\t')
+ xml_appfilter = ET.tostring(root_appfilter, encoding='unicode', xml_declaration=True)
+ xml_appfilter = re.sub("'", '"', xml_appfilter)
+
+ if not args.dry_run:
+ write_file(paths['a1'], xml_appfilter)
+ copy(paths['a1'], paths['a2'])
+
+ write_file(paths['d1'], xml_drawable)
+ copy(paths['d1'], paths['d2'])
+
+ if not args.sort:
+ write_file(paths['icons'], ICONS_HEADER)
+
+ print()
+
+
+if __name__ == '__main__':
+ main()
diff --git a/resources/scripts/process_requests.py b/resources/scripts/process_requests.py
new file mode 100755
index 0000000000..f6a9527553
--- /dev/null
+++ b/resources/scripts/process_requests.py
@@ -0,0 +1,199 @@
+#!/usr/bin/env python
+
+from argparse import ArgumentParser
+import argparse, re, sys
+import xml.etree.ElementTree as ET
+from datetime import datetime
+from difflib import SequenceMatcher
+from copy import deepcopy
+from os.path import basename, relpath
+import yaml
+from natsort import natsorted as sorted
+
+from shared import paths, stores
+
+
+class YamlNewLines(yaml.SafeDumper):
+ # https://github.com/yaml/pyyaml/issues/127#issuecomment-525800484
+ def write_line_break(self, data=None):
+ super().write_line_break(data)
+ if len(self.indents) == 1:
+ super().write_line_break()
+
+ def increase_indent(self, flow=False, indentless=False):
+ return super().increase_indent(flow, False)
+
+
+PATH = paths['requests']
+
+FORMATS = ['txt', 'xml', 'yml']
+FORMATS_DEFAULT = FORMATS[2]
+
+SORTS = ['name', 'ratio', 'request']
+SORTS_DEFAULT = SORTS[1]
+
+RATIO_RANGE = {'min': 0.25, 'default': 0.75, 'max': 1.0}
+
+
+def create_parser():
+ parser = ArgumentParser()
+
+ parser.add_argument('-i',
+ dest='input',
+ metavar='PATH',
+ help=f"path to {basename(PATH)} to process (default: '{relpath(PATH)}')",
+ default=PATH)
+ parser.add_argument('-f',
+ dest='format',
+ metavar=f"[{'|'.join(FORMATS)}]",
+ choices=FORMATS,
+ help=f"output in specific format (default: '{FORMATS_DEFAULT}')",
+ default=FORMATS_DEFAULT)
+ parser.add_argument('-r',
+ dest='ratio',
+ metavar='VALUE',
+ help=f"custom ratio value from {RATIO_RANGE['min']} to {RATIO_RANGE['max']} (default: {RATIO_RANGE['default']})",
+ default=RATIO_RANGE['default'])
+ parser.add_argument('-H',
+ dest='hide_ratios',
+ help='hide ratios in output',
+ action='store_true')
+ parser.add_argument('-s',
+ dest='sort',
+ metavar=f"[{'|'.join(SORTS)}]",
+ help=f"sort by specific value (default: '{SORTS_DEFAULT}')",
+ choices=SORTS,
+ default=SORTS_DEFAULT)
+ parser.add_argument('-u', '--update',
+ dest='update',
+ help=f'update and remove entries in {basename(PATH)}',
+ action='store_true')
+ parser.add_argument('--urls',
+ dest='urls',
+ help=f'update store urls (use with -u)',
+ action='store_true')
+ parser.add_argument('-q',
+ dest='quiet',
+ help='do not print anything',
+ action='store_true')
+ return parser
+
+def read(path):
+ with open(path, 'r+') as file:
+ loaded = yaml.safe_load(file)
+ return loaded if loaded is not None else {}
+
+
+def write(path, data):
+ with open(path, 'r+') as file:
+ data = dict(sorted(data.items(),
+ # sort by number of requests, then by last request time
+ key=lambda k: (k[1]['reqt'], k[1]['reql']),
+ # reverse array to set new requests at top of the list
+ reverse=True))
+ # header message with total number of requested icons and last time update
+ header = (f"# {len(data)} requested apps pending \n"
+ f"# updated {datetime.today().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
+ dump = yaml.dump(data, Dumper=YamlNewLines, allow_unicode=True, indent=4, sort_keys=False)
+ file.seek(0)
+ file.write(header + dump)
+ file.truncate()
+
+
+def parse_requests(
+ input=PATH,
+ format=FORMATS_DEFAULT,
+ ratio=RATIO_RANGE['default'],
+ sort=SORTS_DEFAULT,
+ hide_ratios=False,
+ update=False,
+ urls=False,
+ quiet=False,
+ ):
+
+ requests = read(input)
+ requests_copy = deepcopy(requests)
+
+ updatable = {}
+
+ RATIO = float(ratio) if RATIO_RANGE['min'] <= float(ratio) <= RATIO_RANGE['max'] else RATIO_RANGE['default']
+
+ with open(paths['a1'], 'r') as file:
+ appfilter = ET.ElementTree(ET.fromstring(file.read().rstrip())).getroot()
+
+ if urls:
+ for request in requests_copy:
+ id, activity = request.split('/')
+ requests[request]['urls'] = [url.format(id) for url in stores]
+
+ for item in appfilter:
+ try:
+ compinfo = re.search('ComponentInfo{(.*)}', item.attrib['component']).group(1)
+ id, activity = compinfo.split('/')
+ name = item.attrib['drawable']
+
+ for request in requests_copy:
+ if id not in request: continue
+
+ ratio = 0.0
+
+ if request.startswith(id + '/'):
+ ratio = 1.0
+ else:
+ diff = SequenceMatcher(None, request, compinfo).ratio()
+ ratio = round(diff, 2)
+
+ if ratio >= RATIO:
+
+ if ratio == 1.0:
+ requests.pop(request)
+
+ if request in updatable:
+ if updatable[request]['ratio'] < ratio:
+ ratio = updatable[request]['ratio']
+
+ updatable[request] = {
+ 'name': name,
+ 'ratio': ratio,
+ 'request': request
+ }
+ except:
+ continue
+
+ updatable = dict(sorted(updatable.items(), key=lambda x: x[1][sort]))
+
+ if update:
+ if requests != requests_copy:
+ write(input, requests)
+
+ if not quiet:
+ for [key, value] in updatable.items():
+ name = value['name']
+ ratio = int(value['ratio'] * 100)
+
+ match format:
+ case 'xml':
+ ratios = f' ' if not hide_ratios else ''
+ print(ratios + f' ')
+ case 'yml':
+ ratios = f' # {ratio}%' if not hide_ratios else ''
+ print(f'{name}:'+ ratios)
+ print(f' - {key}\n')
+ case 'txt':
+ ratios = f'[{ratio}%] ' if not hide_ratios else ''
+ print(ratios + f'{name} -> {key}')
+
+
+if __name__ == '__main__':
+ parser = create_parser()
+ args = parser.parse_args()
+ parse_requests(
+ input=args.input,
+ format=args.format,
+ ratio=args.ratio,
+ sort=args.sort,
+ hide_ratios=args.hide_ratios,
+ update=args.update,
+ urls=args.urls,
+ quiet=args.quiet,
+ )
\ No newline at end of file
diff --git a/resources/scripts/requests_parser.py b/resources/scripts/requests_parser.py
deleted file mode 100755
index 81871c6eeb..0000000000
--- a/resources/scripts/requests_parser.py
+++ /dev/null
@@ -1,149 +0,0 @@
-#!/usr/bin/env python3
-
-import argparse, re, sys
-import xml.etree.ElementTree as ET
-from datetime import datetime
-from difflib import SequenceMatcher
-from copy import deepcopy as copy
-
-import yaml
-from natsort import natsorted as sorted
-
-from resolve_paths import paths
-
-
-class YamlNewLines(yaml.SafeDumper):
- # https://github.com/yaml/pyyaml/issues/127#issuecomment-525800484
- def write_line_break(self, data=None):
- super().write_line_break(data)
- if len(self.indents) == 1:
- super().write_line_break()
-
- def increase_indent(self, flow=False, indentless=False):
- return super().increase_indent(flow, False)
-
-filename = 'requests.yml'
-
-
-parser = argparse.ArgumentParser(description=f'parse {filename}')
-
-parser.add_argument('-f', '--format',
- dest='format',
- choices=['xml', 'yml'],
- help='output in specific format')
-parser.add_argument('-r', '--remove',
- dest='remove',
- help=f'remove existing compinfos from {filename}',
- default=False,
- action=argparse.BooleanOptionalAction)
-parser.add_argument('-R', '--ratio',
- dest='ratio',
- help=f'custom ratio from 0.5 to 1.0',
- default=0.75)
-parser.add_argument('-s', '--sort',
- dest='sort',
- help='sort by specific value',
- choices=['name', 'ratio', 'request'],
- default='ratio')
-parser.add_argument('-H', '--hide-ratios',
- dest='hide_ratios',
- help='hide ratios in output',
- default=False,
- action=argparse.BooleanOptionalAction)
-
-
-def read(path):
- with open(path, 'r+') as file:
- loaded = yaml.safe_load(file)
- return loaded if loaded is not None else {}
-
-
-def write(path, data):
- with open(path, 'r+') as file:
- data = dict(sorted(data.items(),
- # sort by number of requests, then by last request time
- key=lambda k: (k[1]['reqt'], k[1]['reql']),
- # reverse array to set new requests at top of the list
- reverse=True))
- # header message with total number of requested icons and last time update
- header = (f"# {len(data)} requested apps pending \n"
- f"# updated {datetime.today().strftime('%Y-%m-%d %H:%M:%S')}\n\n")
- dump = yaml.dump(data, Dumper=YamlNewLines, allow_unicode=True, indent=4, sort_keys=False)
- file.seek(0)
- file.write(header + dump)
- file.truncate()
-
-def main():
- requests = read(paths['requests'])
- requests_copy = copy(requests)
-
- updatable = {}
-
- RATIO = float(args.ratio) if 0.5 <= float(args.ratio) <= 1.0 else 0.75
-
- with open(paths['appfilter'][0], 'r') as file:
- appfilter = ET.ElementTree(ET.fromstring(file.read())).getroot()
-
- for item in appfilter:
- try:
- compinfo = re.search('ComponentInfo{(.*)}', item.attrib['component']).group(1)
- id, activity = compinfo.split('/')
- name = item.attrib['drawable']
-
- for request in requests_copy:
- if id not in request: continue
-
- ratio = 0.0
-
- if request.startswith(id + '/'):
- ratio = 1.0
- else:
- diff = SequenceMatcher(None, request, compinfo).ratio()
- ratio = round(diff, 2)
-
- if ratio >= RATIO:
-
- if ratio == 1.0:
- requests.pop(request)
-
- if request in updatable:
- if updatable[request]['ratio'] < ratio:
- ratio = updatable[request]['ratio']
-
- updatable[request] = {
- 'name': name,
- 'ratio': ratio,
- 'request': request
- }
-
- except:
- continue
-
- updatable = dict(sorted(updatable.items(), key=lambda x: x[1][args.sort]))
-
- if args.remove:
- write(paths['requests'], requests)
-
- for [key, value] in updatable.items():
- name = value['name']
- ratio = int(value['ratio'] * 100)
-
- match args.format:
- case 'xml':
- ratios = f' ' if not args.hide_ratios else ''
- print(ratios + f' ')
- case 'yml':
- ratios = f' # {ratio}%' if not args.hide_ratios else ''
- print(f'{name}:'+ ratios)
- print(f' - {key}\n')
- case _:
- ratios = f'[{ratio}%] ' if not args.hide_ratios else ''
- print(ratios + f'{name} -> {key}')
-
-if __name__ == '__main__':
- if len(sys.argv) > 1:
- args = parser.parse_args()
- else:
- parser.print_help()
- sys.exit(1)
- main()
\ No newline at end of file
diff --git a/resources/scripts/requirements.txt b/resources/scripts/requirements.txt
index 75d52f8925..e3984482de 100644
--- a/resources/scripts/requirements.txt
+++ b/resources/scripts/requirements.txt
@@ -1,6 +1,8 @@
-mail-parser==4.0.0
+html2text==2025.4.15
+mail-parser==4.1.4
natsort==8.4.0
-pyyaml==6.0.2
-semver==3.0.4
+pyyaml==6.0.3
redbox==0.2.1
-xmltodict==0.14.2
+semver==3.0.4
+termcolor==3.2.0
+xmltodict==1.0.2
\ No newline at end of file
diff --git a/resources/scripts/resolve_paths.py b/resources/scripts/resolve_paths.py
deleted file mode 100755
index be9428556e..0000000000
--- a/resources/scripts/resolve_paths.py
+++ /dev/null
@@ -1,67 +0,0 @@
-#! /usr/bin/env python
-
-import argparse, sys
-
-from os.path import abspath, dirname, realpath, join
-
-
-parser = argparse.ArgumentParser(description='Process drawable.xml file')
-
-parser.add_argument('-p', '--print',
- dest='print',
- help='Print paths for shell exporting',
- default=False,
- action=argparse.BooleanOptionalAction)
-
-
-def format_paths():
- root = abspath(f'{dirname(realpath(__file__))}/../..')
- scripts = abspath(dirname(realpath(__file__)))
- contribs = join(root, 'contribs')
- icons = join(contribs, 'icons')
- return {
- 'root': root,
- 'scripts': scripts,
- 'contribs': contribs,
- 'icons': join(contribs, 'icons.yml'),
- 'requests': join(contribs, 'requests.yml'),
- 'vectors': join(root, 'resources/vectors'),
- 'src': {
- 'dir': icons,
- 'svg': join(icons, f'{{}}.svg'),
- 'png': join(icons, f'{{}}.png')
- },
- 'dst': {
- 'svg': join(root, f'resources/vectors/{{}}.svg'),
- 'png': join(root, f'app/src/main/res/drawable-nodpi/{{}}.png')
- },
- 'appfilter': [
- join(root, 'app/src/main/res/xml/appfilter.xml'),
- join(root, 'app/src/main/assets/appfilter.xml')
- ],
- 'drawable': [
- join(root, 'app/src/main/res/xml/drawable.xml'),
- join(root, 'app/src/main/assets/drawable.xml')
- ]
- }
-
-paths = format_paths()
-
-
-if __name__ == '__main__':
- args = parser.parse_args()
- if len(sys.argv) > 1:
- args = parser.parse_args()
- else:
- parser.print_help()
- sys.exit(1)
- if args.print:
- print(f"wd={paths['root']}")
- print(f"sd={paths['scripts']}")
- print(f"id={paths['src']['dir']}")
- print(f"vd={paths['vectors']}")
- print(f"rq={paths['requests']}")
- print(f"a1={paths['appfilter'][0]}")
- print(f"a2={paths['appfilter'][1]}")
- print(f"d1={paths['drawable'][0]}")
- print(f"d2={paths['drawable'][1]}")
diff --git a/resources/scripts/shared.py b/resources/scripts/shared.py
new file mode 100755
index 0000000000..3cdb8887e3
--- /dev/null
+++ b/resources/scripts/shared.py
@@ -0,0 +1,98 @@
+#! /usr/bin/env python
+
+import xml.etree.ElementTree as ET
+
+from argparse import ArgumentParser
+from re import sub
+from sys import argv
+from os.path import abspath, dirname, realpath, join
+
+
+stores = [
+ 'https://play.google.com/store/apps/details?id={}',
+ 'https://f-droid.org/en/packages/{}/',
+ 'https://apkpure.com/delta/{}/',
+ 'https://google.com/search?q={}',
+]
+
+
+def create_parser():
+ parser = ArgumentParser()
+
+ parser.add_argument('-p', '--paths',
+ dest='paths',
+ help='output paths as variables for shell exporting',
+ action='store_true')
+ return parser
+
+
+def format_paths():
+ root = abspath(f'{dirname(realpath(__file__))}/../..')
+ scripts = abspath(dirname(realpath(__file__)))
+ contribs = join(root, 'contribs')
+ icons = join(contribs, 'icons')
+ return {
+ 'root': root,
+ 'scripts': scripts,
+ 'contribs': contribs,
+ 'icons': join(contribs, 'icons.yml'),
+ 'emails': join(scripts, 'emails'),
+ 'requests': join(contribs, 'requests.yml'),
+ 'vectors': join(root, 'resources/vectors'),
+ 'src': {
+ 'dir': icons,
+ 'svg': join(icons, f'{{}}.svg'),
+ 'png': join(icons, f'{{}}.png')
+ },
+ 'dst': {
+ 'svg': join(root, f'resources/vectors/{{}}.svg'),
+ 'png': join(root, f'app/src/main/res/drawable-nodpi/{{}}.png')
+ },
+ 'a1': join(root, 'app/src/main/res/xml/appfilter.xml'),
+ 'a2': join(root, 'app/src/main/assets/appfilter.xml'),
+ 'd1': join(root, 'app/src/main/res/xml/drawable.xml'),
+ 'd2': join(root, 'app/src/main/assets/drawable.xml'),
+ }
+
+
+paths = format_paths()
+
+
+def transform_xml(xml):
+ if '' in xml:
+ xml = sub('\t', '', xml) # remove tags
+ xml = sub(r'\n()', r'\1\n', xml) # remove \n before and add it after
+ xml = sub(r'(', r'\1 />', xml) # ->
+ else:
+ xml = sub(r'/>\n(\n\t<)', r'/>\1/category>\1', xml) # add after latest
+ xml = sub(r'()', r'\t\n\1', xml) # add before
+ xml = sub(r'( ', r'\1>', xml) # remove slash from
+ return xml
+
+
+def list_categories(input=paths['d1']):
+ with open(input) as file:
+ xml = transform_xml(file.read().rstrip())
+ root = ET.fromstring(xml)
+ elements = root.findall('category')
+ return [x.get('title') for x in elements]
+
+
+if __name__ == '__main__':
+ parser = create_parser()
+ if len(argv) > 1:
+ args = parser.parse_args()
+ else:
+ parser.print_help()
+ exit(1)
+
+ if args.paths:
+ print(f"wd={paths['root']}")
+ print(f"sd={paths['scripts']}")
+ print(f"id={paths['src']['dir']}")
+ print(f"vd={paths['vectors']}")
+ print(f"rq={paths['requests']}")
+ print(f"a1={paths['a1']}")
+ print(f"a2={paths['a2']}")
+ print(f"d1={paths['d1']}")
+ print(f"d2={paths['d2']}")
diff --git a/resources/scripts/sort_appfilter.py b/resources/scripts/sort_appfilter.py
deleted file mode 100755
index 0ebdf967e1..0000000000
--- a/resources/scripts/sort_appfilter.py
+++ /dev/null
@@ -1,78 +0,0 @@
-#! /usr/bin/env python
-
-import argparse, re, sys
-import xml.etree.ElementTree as ET
-
-from natsort import natsorted as sorted
-
-from resolve_paths import paths
-
-
-class XmlKeepComments(ET.TreeBuilder):
- # https://gist.github.com/jamiejackson/a37e8d3dacb33b2dcbc1
- def __init__(self, *args, **kwargs):
- super(XmlKeepComments, self).__init__(*args, **kwargs)
-
- def comment(self, data):
- self.start(ET.Comment, {})
- self.data(data)
- self.end(ET.Comment)
-
-xml_parser = ET.XMLParser(target=XmlKeepComments())
-
-
-filename = 'appfilter.xml'
-
-
-parser = argparse.ArgumentParser(description=f'sort {filename}')
-
-parser.add_argument('-i', '--input',
- dest='input',
- help=f'path to {filename}',
- default=paths['appfilter'][0])
-parser.add_argument('-o', '--output',
- dest='output',
- help=f'output to file (default: app/src/main/res/xml/{filename})',
- nargs='?',
- const=paths['appfilter'][0])
-parser.add_argument('-p', '--print',
- dest='print',
- help='output to console',
- default=False,
- action=argparse.BooleanOptionalAction)
-
-
-def main():
- with open(args.input) as file:
- xml = file.read().rstrip() # read appfilter.xml to string
-
- root = ET.fromstring(xml, parser=xml_parser) # convert string to ET
-
- scale = root[0] # save tag
- root.remove(scale) # remove tag
-
- root[:] = sorted(root, key=lambda item: (item.tag, item.get('component'))) # sort items by tag name then my component attribute
- root.insert(0, scale) # restore tag
-
- xml = ET.tostring(root, encoding='unicode', xml_declaration=True) # convert ET to string
- xml = re.sub("'", '"', xml) # convert ' to " and transform to default format
- xml = re.sub(r'\t()', r'\1\n', xml) # remove \t before and \n after
-
- if args.output: # if -o passed
- try:
- with open(args.output, 'w', encoding='utf-8') as file:
- file.write(xml)
- except IsADirectoryError:
- print("Must be a file!")
-
- if args.print: # if -p passed
- print(xml)
-
-
-if __name__ == '__main__':
- if len(sys.argv) > 1:
- args = parser.parse_args()
- else:
- parser.print_help()
- sys.exit(1)
- main()
diff --git a/resources/scripts/sort_drawable.py b/resources/scripts/sort_drawable.py
deleted file mode 100755
index c142f80457..0000000000
--- a/resources/scripts/sort_drawable.py
+++ /dev/null
@@ -1,88 +0,0 @@
-#! /usr/bin/env python
-
-import argparse, re, sys
-import xml.etree.ElementTree as ET
-
-from natsort import natsorted as sorted
-
-from resolve_paths import paths
-
-
-class XmlKeepComments(ET.TreeBuilder):
- # https://gist.github.com/jamiejackson/a37e8d3dacb33b2dcbc1
- def __init__(self, *args, **kwargs):
- super(XmlKeepComments, self).__init__(*args, **kwargs)
-
- def comment(self, data):
- self.start(ET.Comment, {})
- self.data(data)
- self.end(ET.Comment)
-
-xml_parser = ET.XMLParser(target=XmlKeepComments())
-
-
-filename = 'drawable.xml'
-
-
-parser = argparse.ArgumentParser(description=f'sort {filename}')
-
-parser.add_argument('-i', '--input',
- dest='input',
- help=f'path to {filename}',
- default=paths['drawable'][0])
-parser.add_argument('-o', '--output',
- dest='output',
- help=f'output to file (default: app/src/main/res/xml/{filename})',
- nargs='?',
- const=paths['drawable'][0])
-parser.add_argument('-p', '--print',
- dest='print',
- help='output to console',
- default=False,
- action=argparse.BooleanOptionalAction)
-
-
-def transform(xml):
- if '' in xml:
- xml = re.sub('\t', '', xml) # remove tags
- xml = re.sub(r'\n()', r'\1\n', xml) # remove \n before and add it after
- xml = re.sub(r'(', r'\1 />', xml) # ->
- else:
- xml = re.sub(r'/>\n(\n\t<)', r'/>\1/category>\1', xml) # add after latest
- xml = re.sub(r'()', r'\t\n\1', xml) # add before
- xml = re.sub(r'( ', r'\1>', xml) # remove slash from
- return xml
-
-
-def main():
- with open(args.input) as file:
- xml = file.read().rstrip() # read drawable.xml to string
-
- root = ET.fromstring(transform(xml), parser=xml_parser) # transform to true XML format and convert it from string to ET
-
- for category in root.findall('category'):
- items = category.findall('item') # get all items from category
- children = sorted(items, key=lambda item: item.get('drawable')) # sort category items by drawable name
- category[:] = children # rewrite category items
-
- xml = ET.tostring(root, encoding='unicode', xml_declaration=True) # convert ET to string
- xml = re.sub("'", '"', transform(xml)) # convert ' to " and transform to default format
-
- if args.output: # if -o passed
- try:
- with open(args.output, 'w', encoding='utf-8') as file:
- file.write(xml)
- except IsADirectoryError:
- print("Path must be a file!")
-
- if args.print: # if -p passed
- print(xml)
-
-
-if __name__ == '__main__':
- if len(sys.argv) > 1:
- args = parser.parse_args()
- else:
- parser.print_help()
- sys.exit(1)
- main()
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi.svg b/resources/vectors/gosuslugi.svg
index 545ded2985..24094107e2 100644
--- a/resources/vectors/gosuslugi.svg
+++ b/resources/vectors/gosuslugi.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_alt_1.svg b/resources/vectors/gosuslugi_alt_1.svg
deleted file mode 100644
index e35ee5362b..0000000000
--- a/resources/vectors/gosuslugi_alt_1.svg
+++ /dev/null
@@ -1 +0,0 @@
-
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_auto.svg b/resources/vectors/gosuslugi_auto.svg
index 0163d94fdd..7c327cc7e2 100644
--- a/resources/vectors/gosuslugi_auto.svg
+++ b/resources/vectors/gosuslugi_auto.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_culture.svg b/resources/vectors/gosuslugi_culture.svg
index c6a486259e..ff30914ec7 100644
--- a/resources/vectors/gosuslugi_culture.svg
+++ b/resources/vectors/gosuslugi_culture.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_dom.svg b/resources/vectors/gosuslugi_dom.svg
index a482893d24..de3fee90fd 100644
--- a/resources/vectors/gosuslugi_dom.svg
+++ b/resources/vectors/gosuslugi_dom.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_key.svg b/resources/vectors/gosuslugi_key.svg
index 262cd2085c..e3f6565842 100644
--- a/resources/vectors/gosuslugi_key.svg
+++ b/resources/vectors/gosuslugi_key.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/resources/vectors/gosuslugi_pos.svg b/resources/vectors/gosuslugi_pos.svg
index a6c5e4b20e..f003e94f53 100644
--- a/resources/vectors/gosuslugi_pos.svg
+++ b/resources/vectors/gosuslugi_pos.svg
@@ -1 +1 @@
-
\ No newline at end of file
+
\ No newline at end of file