diff --git a/changelog.txt b/changelog.txt index a13daa5..e6a0978 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,2 +1,17 @@ -*Mon Apr 27 2015 Tobi Sim 0.0.1-1 --initial commit +*Mon Feb 13 2023 kirbylife 0.5.0-7 +- Add SVG support +- Improve performance +- Add resize slider +- Fix irregluar size of icons on the main grid +- Add the option to search a better icon directly on the html page +- Improve a bit the logging + +*Mon Feb 13 2023 kirbylife 0.4.5-5 +- Add aarch64 support +- Add logging file +- Skip the sailjail permissions +- Remove all the third-repo dependencies +- Now, when you re-edit an icon, the original icon is used instead of the edited one + +*Mon May 17 2021 kirbylife 0.4.0-1 +- First published version diff --git a/qml/Icon.qml b/qml/Icon.qml index 92a9540..23fea86 100644 --- a/qml/Icon.qml +++ b/qml/Icon.qml @@ -1,6 +1,7 @@ import QtQuick 2.0 import Sailfish.Silica 1.0 import io.thp.pyotherside 1.3 +import Nemo.Notifications 1.0 Page { id: iconEditor @@ -8,44 +9,106 @@ Page { property var attrs property var corners: [true, true, true, true] - PageHeader { - id: header - width: parent.width - title: "name: " + attrs.Name + Notification { + id: notification + appIcon: "/usr/share/icons/hicolor/86x86/apps/harbour-muchkin" + expireTimeout: 5000 + + function sendMessage(msg) { + notification.previewBody = msg + notification.publish() + } } - SilicaGridView { - anchors.top: header.bottom - width: parent.width - height: width - cellWidth: width / 2 - cellHeight: cellWidth + SilicaFlickable { + id: mainList + anchors.fill: parent - model: ListModel { - id: gridItems - - ListElement { - idCell: 0 - activated: true - } - ListElement { - idCell: 1 - activated: true - } - ListElement { - idCell: 2 - activated: true - } - ListElement { - idCell: 3 - activated: true + PullDownMenu { + MenuItem { + text: "Search a better icon" + onClicked: { + py.importModule("main", function(){ + py.call("main.scrape_icon", [iconEditor.attrs.URL], function(result){ + if(result) { + icon.source = result; + iconEditor.attrs.Icon = result; + notification.sendMessage("A better icon found"); + } else { + notification.sendMessage("Not better icon found"); + } + }) + }) + } } } - delegate: GridItem { - onClicked: { - activated = !activated - iconEditor.corners[idCell] = activated + PageHeader { + id: header + width: parent.width + title: "name: " + attrs.Name + } + + SilicaGridView { + anchors.top: header.bottom + width: parent.width + height: width + cellWidth: width / 2 + cellHeight: cellWidth + + model: ListModel { + id: gridItems + + ListElement { + idCell: 0 + activated: true + } + ListElement { + idCell: 1 + activated: true + } + ListElement { + idCell: 2 + activated: true + } + ListElement { + idCell: 3 + activated: true + } + } + + delegate: GridItem { + onClicked: { + activated = !activated + iconEditor.corners[idCell] = activated + py.importModule("main", function(){ + py.call("main.sailify", [(iconEditor.attrs.old_icon || iconEditor.attrs.Icon).toString(), iconEditor.corners, size.value], function(result){ + icon.source = result + }) + }) + } + } + } + + Image { + id: icon + anchors.top: header.bottom + width: parent.width + height: width + } + + Slider { + id: size + anchors.top: icon.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 10 + label: "Size:" + width: parent.width + minimumValue: 0 + maximumValue: 100 + stepSize: 10 + value: 100 + onReleased: { py.importModule("main", function(){ py.call("main.sailify", [(iconEditor.attrs.old_icon || iconEditor.attrs.Icon).toString(), iconEditor.corners, size.value], function(result){ icon.source = result @@ -53,66 +116,41 @@ Page { }) } } - } - Image { - id: icon - anchors.top: header.bottom - width: parent.width - height: width - } - - Slider { - id: size - anchors.top: icon.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 10 - label: "Size:" - width: parent.width - minimumValue: 0 - maximumValue: 100 - stepSize: 10 - value: 100 - onReleased: { - py.importModule("main", function(){ - py.call("main.sailify", [(iconEditor.attrs.old_icon || iconEditor.attrs.Icon).toString(), iconEditor.corners, size.value], function(result){ - icon.source = result + Button { + anchors.top: size.bottom + anchors.horizontalCenter: parent.horizontalCenter + anchors.topMargin: 20 + text: "Save" + onClicked: { + if(iconEditor.attrs.old_icon === undefined) { + iconEditor.attrs.old_icon = iconEditor.attrs.Icon.toString() + } + iconEditor.attrs.Icon = icon.source.toString() + py.importModule("main", function(){ + py.call("main.save_icon", [iconEditor.attrs], function(result){ + if(result){ + pageStack.pop() + pageStack.completeAnimation() + pageStack.currentPage.mainList.reload() + } + }) }) - }) - } - } - - Button { - anchors.top: size.bottom - anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: 20 - text: "Save" - onClicked: { - if(iconEditor.attrs.old_icon === undefined) { - iconEditor.attrs.old_icon = iconEditor.attrs.Icon.toString() } - iconEditor.attrs.Icon = icon.source.toString() - py.importModule("main", function(){ - py.call("main.save_icon", [iconEditor.attrs], function(result){ - if(result){ - pageStack.pop() - pageStack.completeAnimation() - pageStack.currentPage.mainList.reload() - } - }) - }) } + + Python { + id: py + Component.onCompleted: { + py.addImportPath(Qt.resolvedUrl("../src")); + py.importModule("main", function(){ + py.call("main.sailify", [(iconEditor.attrs.old_icon || iconEditor.attrs.Icon).toString(), [1, 1, 1, 1], size.value], function(result){ + icon.source = result + }) + }) + } + } + } - Python { - id: py - Component.onCompleted: { - py.addImportPath(Qt.resolvedUrl("../src")); - py.importModule("main", function(){ - py.call("main.sailify", [(iconEditor.attrs.old_icon || iconEditor.attrs.Icon).toString(), [1, 1, 1, 1], size.value], function(result){ - icon.source = result - }) - }) - } - } } diff --git a/src/decorators.py b/src/decorators.py new file mode 100644 index 0000000..7c07df5 --- /dev/null +++ b/src/decorators.py @@ -0,0 +1,13 @@ +import logging + +def log_error(f): + def wrap(*args, **kwargs): + try: + return f(*args, **kwargs) + except Exception as e: + logging.error(f"Error on: {f.__name__}") + logging.error(f"args: {args}") + logging.error(f"kwargs: {kwargs}") + logging.error(f"Exception: {e}") + raise e + return wrap diff --git a/src/main.py b/src/main.py index 782045c..4caff76 100644 --- a/src/main.py +++ b/src/main.py @@ -1,4 +1,5 @@ import pyotherside # pyright: ignore reportMissingImports +from decorators import log_error from PIL import Image, ImageDraw, ImageChops from PIL.Image import Image as ImageType from io import BytesIO @@ -6,7 +7,9 @@ from glob import glob from collections import Counter from subprocess import getstatusoutput from urllib.parse import urlparse +from urllib.parse import urljoin from functools import lru_cache +from lxml import etree import requests import base64 import os @@ -16,6 +19,10 @@ import tempfile ICON_SIZE = (256, 256) +HEADERS = { + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:124.0) Gecko/20100101 Firefox/124.0" +} + logging_filename = os.path.join(os.path.expanduser("~"), ".local", "share", @@ -69,7 +76,7 @@ def avg_colors(img): def add_background(img: ImageType, bg_tuple: tuple, size: float) -> ImageType: img = img.copy() - size = size / 100 + size = max(size, 1) / 100 bg = Image.new("RGBA", img.size, bg_tuple) img.thumbnail((value * size for value in img.size)) bg.paste(img, ((bg.size[0] - img.size[0]) // 2, (bg.size[1] - img.size[1]) // 2), img) @@ -123,6 +130,39 @@ def get_web_icons(): return list(map(parse_file, apps)) +@log_error +def scrape_icon(url: str) -> str: + url_parsed = urlparse(url) + origin = f"{url_parsed.scheme}://{url_parsed.netloc}" + html = etree.HTML(requests.get(origin, headers=HEADERS).text) + + possible_svg = None + possible_icon = None + max_size = 0 + + for icon in html.xpath("//link[contains(@rel, 'icon')]"): + if "SVG" in next(iter(icon.xpath("@type")), "").upper(): + possible_svg = icon.xpath("@href")[0] + break + possible_href = next(iter(icon.xpath("@href")), None) + possible_size = next(iter(icon.xpath("@sizes")), None) + possible_rel = next(iter(icon.xpath("@rel")), "") + if possible_size and possible_href and possible_size != "any": + size = int(possible_size.split("x")[0]) + if size > max_size: + max_size = size + possible_icon = possible_href + if possible_href and not possible_size and possible_rel.upper() == "apple-touch-icon".upper(): + possible_icon = possible_href + + new_icon = possible_svg or possible_icon + if new_icon: + parsed_new_icon = urlparse(new_icon) + if not parsed_new_icon.scheme or not parsed_new_icon.netloc: + new_icon = urljoin(origin, new_icon) + return sailify(new_icon, [True] * 4, 100) + +@log_error def sailify(raw_str, pattern, size) -> str: url_parsed = urlparse(raw_str) if url_parsed.scheme == "data":