1
0
Fork 0
mirror of https://github.com/homarr-labs/dashboard-icons.git synced 2025-09-21 06:52:40 +02:00

feat: add issue_templates for creation of icons (#935)

Co-authored-by: Dashboard Icons Bot <homarr-labs@proton.me>
This commit is contained in:
Meier Lukas 2025-02-15 15:59:12 +01:00 committed by GitHub
parent 02aaf9bb7f
commit d1e008be5f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 18527 additions and 44 deletions

7
scripts/common.py Normal file
View file

@ -0,0 +1,7 @@
import re
def convert_to_kebab_case(name: str) -> str:
"""Convert a filename to kebab-case."""
cleaned = re.sub(r'[^a-zA-Z0-9\s-]', '', name)
kebab_case_name = re.sub(r'[\s_]+', '-', cleaned).lower()
return kebab_case_name

83
scripts/generate_icons.py Normal file
View file

@ -0,0 +1,83 @@
from icons import IssueFormType, checkAction, iconFactory, checkType
import os
import sys
from pathlib import Path
import requests
from PIL import Image
import cairosvg
ISSUE_FORM_ENV_VAR = "INPUT_ISSUE_FORM"
ROOT_DIR = Path(__file__).resolve().parent.parent
SVG_DIR = ROOT_DIR / "svg"
PNG_DIR = ROOT_DIR / "png"
WEBP_DIR = ROOT_DIR / "webp"
# Ensure the output folders exist
PNG_DIR.mkdir(parents=True, exist_ok=True)
WEBP_DIR.mkdir(parents=True, exist_ok=True)
def request_image(url: str) -> bytes:
response = requests.get(url)
response.raise_for_status()
return response.content
def save_image(image: bytes, path: Path):
with open(path, 'wb') as f:
f.write(image)
def convert_svg_to_png(svg_path: Path) -> bytes:
"""Convert SVG to PNG."""
try:
return cairosvg.svg2png(url=str(svg_path), output_height=512)
except Exception as e:
print(f"Failed to convert {svg_path} to PNG: {e}")
raise e
def save_image_as_webp(image_path: Path, webp_path: Path):
"""Convert an image (PNG or other) to WEBP."""
try:
image = Image.open(image_path).convert("RGBA")
image.save(webp_path, format='WEBP')
except Exception as e:
print(f"Failed to convert {image_path} to WEBP: {e}")
raise e
def main(type: str, action: IssueFormType, issue_form: str):
icon = iconFactory(type, issue_form, action)
convertions = icon.convertions()
for convertion in convertions:
svg_path = SVG_DIR / f"{convertion.name}.svg"
png_path = PNG_DIR / f"{convertion.name}.png"
webp_path = WEBP_DIR / f"{convertion.name}.webp"
imageBytes = request_image(convertion.source)
if icon.type == "svg":
save_image(imageBytes, svg_path)
print(f"Downloaded SVG: {svg_path}")
png_data = convert_svg_to_png(svg_path)
save_image(png_data, png_path)
print(f"Converted PNG: {png_path}")
if icon.type == "png":
save_image(imageBytes, png_path)
print(f"Downloaded PNG: {png_path}")
save_image_as_webp(png_path, webp_path)
print(f"Converted WEBP: {webp_path}")
if (__name__ == "__main__"):
type = checkType(sys.argv[1])
action = checkAction(sys.argv[2])
main(
type,
action,
os.getenv(ISSUE_FORM_ENV_VAR)
)

View file

@ -0,0 +1,33 @@
from pathlib import Path
import json
ROOT_DIR = Path(__file__).resolve().parent.parent
META_DIR = ROOT_DIR / "meta"
# Ensure the output folders exist
META_DIR.mkdir(parents=True, exist_ok=True)
def get_icon_names():
return [path.stem for path in META_DIR.glob("*.json")]
def read_meta_for(icon_name):
meta_file = META_DIR / f"{icon_name}.json"
if meta_file.exists():
with open(meta_file, 'r', encoding='UTF-8') as f:
return json.load(f)
return None
def generate_meta_json():
icon_names = get_icon_names()
fullMeta = dict()
for icon_name in icon_names:
meta = read_meta_for(icon_name)
if meta is None:
print(f"Missing metadata for {icon_name}")
continue
fullMeta[icon_name] = meta
with open(ROOT_DIR / "metadata.json", 'w', encoding='UTF-8') as f:
json.dump(fullMeta, f, indent=4)
if (__name__ == "__main__"):
generate_meta_json()

View file

@ -0,0 +1,51 @@
import json
import os
import sys
from icons import IssueFormType, checkAction, iconFactory, checkType
from pathlib import Path
from metadata import load_metadata
ISSUE_FORM_ENV_VAR = "INPUT_ISSUE_FORM"
AUTHOR_ID_ENV_VAR = "INPUT_ISSUE_AUTHOR_ID"
AUTHOR_LOGIN_ENV_VAR = "INPUT_ISSUE_AUTHOR_LOGIN"
ROOT_DIR = Path(__file__).resolve().parent.parent
META_DIR = ROOT_DIR / "meta"
# Ensure the output folders exist
META_DIR.mkdir(parents=True, exist_ok=True)
def main(type: str, action: IssueFormType, issue_form: str, author_id: int, author_login: str):
icon = iconFactory(type, issue_form, action)
if (action == IssueFormType.METADATA_UPDATE):
existing_metadata = load_metadata(icon.name)
author_id = existing_metadata["author"]["id"]
author_login = existing_metadata["author"]["login"]
metadata = icon.to_metadata({"id": author_id, "login": author_login})
FILE_PATH = META_DIR / f"{icon.name}.json"
with open(FILE_PATH, 'w', encoding='UTF-8') as f:
json.dump(metadata, f, indent=2)
def parse_author_id():
author_id_string = os.getenv(AUTHOR_ID_ENV_VAR)
if author_id_string != None:
return int(author_id_string)
return None
if (__name__ == "__main__"):
type = checkType(sys.argv[1])
action = checkAction(sys.argv[2])
main(
type,
action,
os.getenv(ISSUE_FORM_ENV_VAR),
parse_author_id(),
os.getenv(AUTHOR_LOGIN_ENV_VAR)
)

222
scripts/icons.py Normal file
View file

@ -0,0 +1,222 @@
import re
from common import convert_to_kebab_case
from datetime import datetime
import json
from enum import Enum
from metadata import load_metadata
class IconConvertion:
def __init__(self, name: str, source: str):
self.name = name
self.source = source
class Icon:
def __init__(self, name: str, type: str, categories: list, aliases: list):
self.name = name
self.type = type
self.categories = categories
self.aliases = aliases
def to_metadata(self, author: dict) -> dict:
return {
"base": self.type,
"aliases": self.aliases,
"categories": self.categories,
"update": {
"timestamp": datetime.now().isoformat(),
"author": author
}
}
def convertions(self) -> list[IconConvertion]:
raise NotImplementedError("Method 'files' must be implemented in subclass")
class NormalIcon(Icon):
def __init__(self, icon: str, name: str, type: str, categories: list, aliases: list):
super().__init__(name, type, categories, aliases)
self.icon = icon
def convertions(self) -> list[IconConvertion]:
return [
IconConvertion(self.name, self.icon)
]
def from_addition_issue_form(input: dict):
return NormalIcon(
mapUrlFromMarkdownImage(input, "Paste icon"),
convert_to_kebab_case(mapFromRequired(input, "Icon name")),
mapFileTypeFrom(input, "Icon type"),
mapListFrom(input, "Categories"),
mapListFrom(input, "Aliases")
)
def from_update_issue_form(input: dict):
try:
name = convert_to_kebab_case(mapFromRequired(input, "Icon name"))
metadata = load_metadata(name)
return NormalIcon(
mapUrlFromMarkdownImage(input, "Paste icon"),
mapFromRequired(input, "Icon name"),
mapFileTypeFrom(input, "Icon type"),
metadata["categories"],
metadata["aliases"]
)
except Exception as exeption:
raise ValueError(f"Icon '{name}' does not exist", exeption)
def from_metadata_update_issue_form(input: dict):
name = convert_to_kebab_case(mapFromRequired(input, "Icon name"))
metadata = load_metadata(name)
return NormalIcon(
None,
name,
metadata["base"],
mapListFrom(input, "Categories"),
mapListFrom(input, "Aliases")
)
class MonochromeIcon(Icon):
def __init__(self, lightIcon: str, darkIcon: str, name: str, type: str, categories: list, aliases: list):
super().__init__(name, type, categories, aliases)
self.lightIcon = lightIcon
self.darkIcon = darkIcon
def to_colors(self) -> dict:
try:
metadata = load_metadata(self.name)
return {
"light": f"{metadata['colors']['light']}",
"dark": f"{metadata['colors']['dark']}"
}
except:
return {
"light": f"{self.name}",
"dark": f"{self.name}-dark"
}
def to_metadata(self, author: dict) -> dict:
metadata = super().to_metadata(author)
metadata["colors"] = self.to_colors()
return metadata
def convertions(self) -> list[IconConvertion]:
colorNames = self.to_colors()
return [
IconConvertion(colorNames["light"], self.lightIcon),
IconConvertion(colorNames["dark"], self.darkIcon),
]
def from_addition_issue_form(input: dict):
return MonochromeIcon(
mapUrlFromMarkdownImage(input, "Paste light mode icon"),
mapUrlFromMarkdownImage(input, "Paste dark mode icon"),
convert_to_kebab_case(mapFromRequired(input, "Icon name")),
mapFileTypeFrom(input, "Icon type"),
mapListFrom(input, "Categories"),
mapListFrom(input, "Aliases")
)
def from_update_issue_form(input: dict):
try:
name = convert_to_kebab_case(mapFromRequired(input, "Icon name"))
metadata = load_metadata(name)
return MonochromeIcon(
mapUrlFromMarkdownImage(input, "Paste light mode icon"),
mapUrlFromMarkdownImage(input, "Paste dark mode icon"),
mapFromRequired(input, "Icon name"),
mapFileTypeFrom(input, "Icon type"),
metadata["categories"],
metadata["aliases"]
)
except Exception as exeption:
raise ValueError(f"Icon '{name}' does not exist", exeption)
def from_metadata_update_issue_form(input: dict):
name = convert_to_kebab_case(mapFromRequired(input, "Icon name"))
metadata = load_metadata(name)
return MonochromeIcon(
None,
None,
name,
metadata["base"],
mapListFrom(input, "Categories"),
mapListFrom(input, "Aliases")
)
def checkType(type: str):
if type not in ["normal", "monochrome"]:
raise ValueError(f"Invalid icon type: '{type}'")
return type
def checkAction(action: str):
if action == "addition":
return IssueFormType.ADDITION
elif action == "update":
return IssueFormType.UPDATE
elif action == "metadata_update":
return IssueFormType.METADATA_UPDATE
raise ValueError(f"Invalid action: '{action}'")
class IssueFormType(Enum):
ADDITION = "addition"
UPDATE = "update"
METADATA_UPDATE = "metadata_update"
def iconFactory(type: str, issue_form: str, issue_form_type: IssueFormType):
if type == "normal":
if (issue_form_type == IssueFormType.ADDITION):
return NormalIcon.from_addition_issue_form(json.loads(issue_form))
elif (issue_form_type == IssueFormType.UPDATE):
return NormalIcon.from_update_issue_form(json.loads(issue_form))
elif (issue_form_type == IssueFormType.METADATA_UPDATE):
return NormalIcon.from_metadata_update_issue_form(json.loads(issue_form))
else:
raise ValueError(f"Invalid issue form type: '{issue_form_type}'")
elif type == "monochrome":
if (issue_form_type == IssueFormType.ADDITION):
return MonochromeIcon.from_addition_issue_form(json.loads(issue_form))
elif (issue_form_type == IssueFormType.UPDATE):
return MonochromeIcon.from_update_issue_form(json.loads(issue_form))
elif (issue_form_type == IssueFormType.METADATA_UPDATE):
return MonochromeIcon.from_metadata_update_issue_form(json.loads(issue_form))
else:
raise ValueError(f"Invalid issue form type: '{issue_form_type}'")
raise ValueError(f"Invalid icon type: '{type}'")
def mapFrom(input: dict, label: str) -> str:
return input.get(label, None)
def mapFromRequired(input: dict, label: str) -> str:
value = mapFrom(input, label)
if value is None:
raise ValueError(f"Missing required field: '{label}'")
return value
def mapFileTypeFrom(input: dict, label: str) -> str:
fileType = mapFromRequired(input, label)
if fileType not in ["SVG", "PNG"]:
raise ValueError(f"Invalid file type: '{fileType}'")
return fileType.lower()
def mapListFrom(input: dict, label: str) -> list:
stringList = mapFrom(input, label)
if stringList is None:
return []
return list(map(str.strip, stringList.split(",")))
def mapUrlFromMarkdownImage(input: dict, label: str) -> re.Match[str]:
markdown = mapFromRequired(input, label)
try:
return re.match(r"!\[[^\]]+\]\((https:[^\)]+)\)", markdown)[1]
except IndexError:
raise ValueError(f"Invalid markdown image: '{markdown}'")

9
scripts/metadata.py Normal file
View file

@ -0,0 +1,9 @@
import json
def load_metadata(icon_name: str) -> dict:
try:
with open(f"meta/{icon_name}.json", "r") as f:
return json.load(f)
except FileNotFoundError:
raise ValueError(f"Icon '{icon_name}' does not exist")

View file

@ -0,0 +1,28 @@
import json
import os
ISSUE_FORM_ITEM_LABEL = "###"
ISSUE_EMPTY_RESPONSE = "_No response_"
INPUT_ENV_VAR_NAME = "INPUT_ISSUE_BODY"
def parse_issue_form(input: str) -> dict:
splitItems = input.split(ISSUE_FORM_ITEM_LABEL)
# Remove first empty item
splitItems.pop(0)
parsedForm = dict()
for item in splitItems:
item = item.strip()
itemLines = item.split("\n")
itemName = itemLines[0].strip()
itemValue = "\n".join(itemLines[1:]).strip()
if itemValue == ISSUE_EMPTY_RESPONSE:
itemValue = None
parsedForm[itemName] = itemValue
return parsedForm
def main(input: str):
parsedIssueForm = parse_issue_form(input)
print(json.dumps(parsedIssueForm))
if (__name__ == "__main__"):
main(os.getenv(INPUT_ENV_VAR_NAME))

View file

@ -0,0 +1,14 @@
import os
import sys
from icons import IssueFormType, checkAction, iconFactory, checkType
ISSUE_FORM_ENV_VAR = "INPUT_ISSUE_FORM"
def main(type: str, action: IssueFormType, issue_form: str):
icon = iconFactory(type, issue_form, action)
print(icon.name)
if (__name__ == "__main__"):
type = checkType(sys.argv[1])
action = checkAction(sys.argv[2])
main(type, action, os.getenv(ISSUE_FORM_ENV_VAR))