feat: rewrite application as modular fcco-Converter V2 with new GUI, metadata handling, and updater logic
This commit is contained in:
@@ -0,0 +1,304 @@
|
||||
"""
|
||||
fcco-Converter V2 – Icon-Modul
|
||||
|
||||
Erzeugt saubere, professionelle Icons programmatisch mit PIL.
|
||||
Keine externen Bilddateien nötig – alle Icons sind Vektorgrafiken,
|
||||
die in beliebiger Größe gerendert werden können.
|
||||
"""
|
||||
|
||||
from PIL import Image, ImageDraw
|
||||
import customtkinter as ctk
|
||||
|
||||
|
||||
def _create_canvas(size: int = 24, bg: str | None = None) -> tuple[Image.Image, ImageDraw.ImageDraw]:
|
||||
"""Erstellt eine transparente Zeichenfläche."""
|
||||
img = Image.new("RGBA", (size, size), (0, 0, 0, 0))
|
||||
draw = ImageDraw.Draw(img, "RGBA")
|
||||
return img, draw
|
||||
|
||||
|
||||
def _hex_to_rgba(hex_color: str, alpha: int = 255) -> tuple[int, int, int, int]:
|
||||
"""Konvertiert einen Hex-Farbwert zu RGBA."""
|
||||
hex_color = hex_color.lstrip("#")
|
||||
r, g, b = int(hex_color[0:2], 16), int(hex_color[2:4], 16), int(hex_color[4:6], 16)
|
||||
return (r, g, b, alpha)
|
||||
|
||||
|
||||
def make_ctk_image(light_img: Image.Image, dark_img: Image.Image | None = None, size: tuple[int, int] = (20, 20)) -> ctk.CTkImage:
|
||||
"""Erzeugt ein CTkImage aus PIL-Bildern."""
|
||||
return ctk.CTkImage(
|
||||
light_image=light_img,
|
||||
dark_image=dark_img or light_img,
|
||||
size=size,
|
||||
)
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Icon-Generatoren
|
||||
# =========================================================================
|
||||
|
||||
def icon_folder(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Ordner-Icon."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
# Ordner-Körper
|
||||
draw.rounded_rectangle(
|
||||
[s * 0.08, s * 0.25, s * 0.92, s * 0.85],
|
||||
radius=s * 0.08, fill=c
|
||||
)
|
||||
# Tab oben
|
||||
draw.rounded_rectangle(
|
||||
[s * 0.08, s * 0.15, s * 0.45, s * 0.35],
|
||||
radius=s * 0.06, fill=c
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
def icon_file_add(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Datei-hinzufügen-Icon."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Dokument
|
||||
draw.rounded_rectangle(
|
||||
[s * 0.17, s * 0.04, s * 0.71, s * 0.96],
|
||||
radius=s * 0.06, outline=c, width=lw
|
||||
)
|
||||
# Plus-Zeichen
|
||||
cx, cy = s * 0.44, s * 0.50
|
||||
arm = s * 0.15
|
||||
draw.line([(cx - arm, cy), (cx + arm, cy)], fill=c, width=lw)
|
||||
draw.line([(cx, cy - arm), (cx, cy + arm)], fill=c, width=lw)
|
||||
return img
|
||||
|
||||
|
||||
def icon_play(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Play/Start-Icon (Dreieck)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
# Dreieck (nach rechts zeigend)
|
||||
points = [
|
||||
(s * 0.25, s * 0.15),
|
||||
(s * 0.80, s * 0.50),
|
||||
(s * 0.25, s * 0.85),
|
||||
]
|
||||
draw.polygon(points, fill=c)
|
||||
return img
|
||||
|
||||
|
||||
def icon_settings(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Zahnrad/Einstellungen-Icon."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
cx, cy = s * 0.5, s * 0.5
|
||||
# Äußerer Ring
|
||||
r_outer = s * 0.38
|
||||
draw.ellipse(
|
||||
[cx - r_outer, cy - r_outer, cx + r_outer, cy + r_outer],
|
||||
outline=c, width=lw
|
||||
)
|
||||
# Innerer Punkt
|
||||
r_inner = s * 0.12
|
||||
draw.ellipse(
|
||||
[cx - r_inner, cy - r_inner, cx + r_inner, cy + r_inner],
|
||||
fill=c
|
||||
)
|
||||
# Zähne (6 Stück als kurze Linien)
|
||||
import math
|
||||
for i in range(6):
|
||||
angle = math.pi * i / 3
|
||||
x1 = cx + r_outer * math.cos(angle)
|
||||
y1 = cy + r_outer * math.sin(angle)
|
||||
x2 = cx + (r_outer + s * 0.12) * math.cos(angle)
|
||||
y2 = cy + (r_outer + s * 0.12) * math.sin(angle)
|
||||
draw.line([(x1, y1), (x2, y2)], fill=c, width=lw)
|
||||
return img
|
||||
|
||||
|
||||
def icon_refresh(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Refresh/Update-Icon (kreisförmiger Pfeil)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Kreisbogen
|
||||
draw.arc(
|
||||
[s * 0.15, s * 0.15, s * 0.85, s * 0.85],
|
||||
start=30, end=330, fill=c, width=lw
|
||||
)
|
||||
# Pfeilspitze
|
||||
import math
|
||||
angle = math.radians(30)
|
||||
cx, cy = s * 0.5, s * 0.5
|
||||
r = s * 0.35
|
||||
tip_x = cx + r * math.cos(angle)
|
||||
tip_y = cy - r * math.sin(angle)
|
||||
arrow_size = s * 0.15
|
||||
points = [
|
||||
(tip_x, tip_y),
|
||||
(tip_x + arrow_size, tip_y + arrow_size * 0.3),
|
||||
(tip_x + arrow_size * 0.3, tip_y + arrow_size),
|
||||
]
|
||||
draw.polygon(points, fill=c)
|
||||
return img
|
||||
|
||||
|
||||
def icon_save(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Speichern-Icon (Diskette)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Äußeres Rechteck
|
||||
draw.rounded_rectangle(
|
||||
[s * 0.10, s * 0.10, s * 0.90, s * 0.90],
|
||||
radius=s * 0.06, outline=c, width=lw
|
||||
)
|
||||
# Inneres Rechteck (Etikett)
|
||||
draw.rectangle(
|
||||
[s * 0.30, s * 0.55, s * 0.70, s * 0.85],
|
||||
outline=c, width=lw
|
||||
)
|
||||
# Oberer Bereich (Schieber)
|
||||
draw.rectangle(
|
||||
[s * 0.30, s * 0.10, s * 0.70, s * 0.38],
|
||||
fill=c
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
def icon_browse(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Durchsuchen-Icon (Lupe)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Kreis
|
||||
draw.ellipse(
|
||||
[s * 0.12, s * 0.08, s * 0.62, s * 0.58],
|
||||
outline=c, width=lw
|
||||
)
|
||||
# Griff
|
||||
draw.line(
|
||||
[(s * 0.55, s * 0.55), (s * 0.88, s * 0.88)],
|
||||
fill=c, width=max(2, int(s * 0.10))
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
def icon_check(color: str = "#34d399", size: int = 24) -> Image.Image:
|
||||
"""Häkchen-Icon."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(2, int(s * 0.10))
|
||||
draw.line(
|
||||
[(s * 0.18, s * 0.52), (s * 0.40, s * 0.75), (s * 0.82, s * 0.25)],
|
||||
fill=c, width=lw, joint="curve"
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
def icon_cross(color: str = "#f87171", size: int = 24) -> Image.Image:
|
||||
"""Kreuz/Fehler-Icon."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(2, int(s * 0.10))
|
||||
draw.line([(s * 0.22, s * 0.22), (s * 0.78, s * 0.78)], fill=c, width=lw)
|
||||
draw.line([(s * 0.78, s * 0.22), (s * 0.22, s * 0.78)], fill=c, width=lw)
|
||||
return img
|
||||
|
||||
|
||||
def icon_music(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Musik/Audio-Icon (Notensymbol)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Notenhals
|
||||
draw.line([(s * 0.60, s * 0.15), (s * 0.60, s * 0.70)], fill=c, width=lw)
|
||||
# Notenkopf
|
||||
draw.ellipse(
|
||||
[s * 0.35, s * 0.60, s * 0.65, s * 0.85],
|
||||
fill=c
|
||||
)
|
||||
# Fähnchen
|
||||
draw.arc(
|
||||
[s * 0.55, s * 0.12, s * 0.88, s * 0.45],
|
||||
start=270, end=90, fill=c, width=lw
|
||||
)
|
||||
return img
|
||||
|
||||
|
||||
def icon_info(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Info-Icon (i im Kreis)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Kreis
|
||||
draw.ellipse(
|
||||
[s * 0.10, s * 0.10, s * 0.90, s * 0.90],
|
||||
outline=c, width=lw
|
||||
)
|
||||
# Punkt oben
|
||||
draw.ellipse(
|
||||
[s * 0.44, s * 0.22, s * 0.56, s * 0.34],
|
||||
fill=c
|
||||
)
|
||||
# Strich unten
|
||||
draw.line([(s * 0.50, s * 0.42), (s * 0.50, s * 0.75)], fill=c, width=lw)
|
||||
return img
|
||||
|
||||
|
||||
def icon_download(color: str = "#e2e8f0", size: int = 24) -> Image.Image:
|
||||
"""Download-Icon (Pfeil nach unten)."""
|
||||
img, draw = _create_canvas(size)
|
||||
c = _hex_to_rgba(color)
|
||||
s = size
|
||||
lw = max(1, int(s * 0.08))
|
||||
# Vertikale Linie
|
||||
draw.line([(s * 0.50, s * 0.12), (s * 0.50, s * 0.62)], fill=c, width=lw)
|
||||
# Pfeilspitze
|
||||
draw.line([(s * 0.30, s * 0.48), (s * 0.50, s * 0.68)], fill=c, width=lw)
|
||||
draw.line([(s * 0.70, s * 0.48), (s * 0.50, s * 0.68)], fill=c, width=lw)
|
||||
# Unterstrich
|
||||
draw.line([(s * 0.20, s * 0.85), (s * 0.80, s * 0.85)], fill=c, width=lw)
|
||||
return img
|
||||
|
||||
|
||||
# =========================================================================
|
||||
# Vorgefertigte CTkImage-Sets (Light + Dark)
|
||||
# =========================================================================
|
||||
|
||||
class IconSet:
|
||||
"""Hält alle Icons als CTkImage-Instanzen.
|
||||
Muss nach CTk-Initialisierung erstellt werden.
|
||||
"""
|
||||
|
||||
def __init__(self, size: tuple[int, int] = (18, 18)):
|
||||
s = size[0]
|
||||
light = "#3f3f46" # Für hellen Modus
|
||||
dark = "#e2e8f0" # Für dunklen Modus
|
||||
accent = "#8b7cf6"
|
||||
success = "#34d399"
|
||||
error = "#f87171"
|
||||
|
||||
self.folder = make_ctk_image(icon_folder(light, s), icon_folder(dark, s), size)
|
||||
self.file_add = make_ctk_image(icon_file_add(light, s), icon_file_add(dark, s), size)
|
||||
self.play = make_ctk_image(icon_play(light, s), icon_play(dark, s), size)
|
||||
self.settings = make_ctk_image(icon_settings(light, s), icon_settings(dark, s), size)
|
||||
self.refresh = make_ctk_image(icon_refresh(light, s), icon_refresh(dark, s), size)
|
||||
self.save = make_ctk_image(icon_save(light, s), icon_save(dark, s), size)
|
||||
self.browse = make_ctk_image(icon_browse(light, s), icon_browse(dark, s), size)
|
||||
self.check = make_ctk_image(icon_check(success, s), icon_check(success, s), size)
|
||||
self.cross = make_ctk_image(icon_cross(error, s), icon_cross(error, s), size)
|
||||
self.music = make_ctk_image(icon_music(light, s), icon_music(dark, s), size)
|
||||
self.info = make_ctk_image(icon_info(light, s), icon_info(dark, s), size)
|
||||
self.download = make_ctk_image(icon_download(light, s), icon_download(dark, s), size)
|
||||
Reference in New Issue
Block a user