tk_main_menue

  1# Standardbibliothek
  2import platform
  3import sys
  4import tempfile
  5import webbrowser
  6from datetime import datetime, timedelta
  7from pathlib import Path
  8
  9# Drittanbieter-Bibliotheken
 10import tkinter as tk
 11from tkinter import messagebox, scrolledtext
 12
 13import ttkbootstrap as ttk
 14from ttkbootstrap.dialogs import Messagebox
 15from ttkbootstrap.scrolled import ScrolledText
 16
 17
 18# Eigene Module
 19from jamfscripts import *  # besser: gezielte Funktionen importieren
 20from tk_jamf_login import JamfLogin
 21
 22
 23JAMF_URL = ""
 24TOKEN = ""
 25#ZUSTIMMUNGSDATEI = os.path.join(os.getcwd(), "zustimmung.json")
 26#NUTZUNGSDATEI = os.path.join(os.getcwd(), "nutzung.json")
 27config_dir = user_data_dir("Classload", "chribdo")
 28os.makedirs(config_dir, exist_ok=True)
 29
 30ZUSTIMMUNGSDATEI  = os.path.join(config_dir, "zustimmung.json")
 31NUTZUNGSDATEI  = os.path.join(config_dir, "nutzung.json")
 32
 33def get_resource_path(filename):
 34    """
 35    Gibt den Pfad zur Datei zurück – funktioniert mit PyInstaller, py2app und lokal.
 36    """
 37    if hasattr(sys, '_MEIPASS'):
 38        return os.path.join(sys._MEIPASS, filename)
 39    elif hasattr(sys, 'frozen') and 'RESOURCEPATH' in os.environ:
 40        return os.path.join(os.environ['RESOURCEPATH'], filename)
 41    else:
 42        # Lokaler Entwicklungsmodus – Bezug relativ zur Python-Datei
 43        base_path = Path(__file__).resolve().parent
 44        return str(base_path / filename)
 45
 46LIZENZ = get_resource_path("LICENSE.txt")
 47
 48def init_dpi_awareness():
 49    """
 50    Aktiviert DPI-Awareness unter Windows, um eine scharfe und korrekt skalierte
 51    Darstellung in tkinter/ttkbootstrap-Fenstern zu gewährleisten.
 52    Hat keinen Effekt auf macOS oder Linux.
 53    """
 54    if sys.platform.startswith("win"):
 55        try:
 56            import ctypes
 57            # 1 = system DPI aware, 2 = per-monitor DPI aware
 58            ctypes.windll.shcore.SetProcessDpiAwareness(1)
 59        except Exception:
 60            pass
 61
 62def set_window_icon(widget):
 63    """setzt ein icon-Window. Wird aktuell nicht verwendet."""
 64    try:
 65        if sys.platform == "darwin":
 66            # macOS: kleineres Icon verwenden, sonst kommt "path is bad"
 67            icon_relpath = os.path.join("assets", "icon_small.png")
 68        else:
 69            # Windows/Linux: großes Icon mit Alpha verwenden
 70            icon_relpath = os.path.join("assets", "icon.png")
 71
 72        icon_path = get_resource_path(icon_relpath)
 73        print(f"🖼 Verwende Icon: {icon_path}")
 74
 75        icon_img = tk.PhotoImage(file=icon_path)
 76        widget.iconphoto(True, icon_img)
 77        widget.icon_img = icon_img  # Referenz halten!
 78    except Exception as e:
 79        print(f"⚠️ Icon konnte nicht gesetzt werden: {e}")
 80
 81def lade_nutzungsinfo():
 82    """öffnet die Nutzungsdatei, sofern vorhanden."""
 83    if os.path.exists(NUTZUNGSDATEI):
 84        with open(NUTZUNGSDATEI, "r") as f:
 85            return json.load(f)
 86    return {}
 87
 88def speichere_nutzungsinfo(info):
 89    """speichert die Art der Nutzung(privat/gewerblich) in einer.json-Datei"""
 90    with open(NUTZUNGSDATEI, "w") as f:
 91        json.dump(info, f)
 92
 93def pruefe_testversion(root, verbleibend):
 94    """Prüft ob die gewerbliche Lizenz noch gültig ist. Wenn ja wird nach Bestätigung das Login-Fenster Jamf-Login aufgerufen."""
 95    root.deiconify()
 96    #root.geometry("400x300")  # Fenstergröße setzen
 97    root.minsize(400, 250)
 98    if verbleibend.days < 0:
 99        def beenden():
100            root.destroy()
101            sys.exit()
102
103        frame = ttk.Frame(root, padding=30)
104        frame.pack(expand=True)
105        label = ttk.Label(frame,
106                          text="❌ Die 7-Tage-Testversion ist abgelaufen.\nBitte kontaktieren Sie den Entwickler für eine Lizenz.",
107                          font=("Arial", 12), justify="center")
108        label.pack()
109        root.after(5000, beenden)  # Automatisch schließen & Programm beenden
110    else:
111        def weiter():
112            JamfLogin(root)
113            hinweis.destroy()
114            root.withdraw()
115
116        hinweis = ttk.Frame(root, padding=20)
117        hinweis.place(relx=0.5, rely=0.5, anchor="center")
118
119        label = ttk.Label(hinweis, text=f"✔ Testversion aktiv\nNoch {verbleibend.days + 1} Tage verfügbar", font=("Arial", 11))
120        label.pack(pady=(0, 10))
121
122        btn = ttk.Button(hinweis, text="OK", command=weiter)
123        btn.pack()
124        root.update_idletasks()
125        root.minsize(root.winfo_width(), root.winfo_height())
126
127def pruefe_nutzungsart(root):
128    """
129    Die Nutzungsart wird überprüft.
130    Sie wird abgespeichert, falls sie noch nicht abgespeichert ist, sonst wird sie gelesen.
131    Für gewerbliche Nutzung wird die verbleibende Nutzungszeit geprüft und ggf. weitergegeben.
132    Bei privater Nutzung wird direkt das Login-Fenster JamfLogin geöffnet.
133    """
134    info = lade_nutzungsinfo()
135    if "nutzung" not in info:
136        nutzungsart = zeige_nutzungsdialog(root)
137        if not nutzungsart:
138            Messagebox.ok(title="Abbruch", message="Nutzungstyp nicht festgelegt. Programm wird beendet.", alert=True)
139            sys.exit()
140        nutzungsart = nutzungsart.strip().lower()
141        info["nutzung"] = nutzungsart
142        if nutzungsart == "gewerblich":
143            info["startdatum"] = datetime.today().strftime("%Y-%m-%d")
144        speichere_nutzungsinfo(info)
145    elif info["nutzung"] == "privat":
146        JamfLogin(root)
147    elif info["nutzung"] == "gewerblich":
148        startdatum = datetime.strptime(info["startdatum"], "%Y-%m-%d")
149        verbleibend = (startdatum + timedelta(days=7)) - datetime.today()
150        """
151        if verbleibend.days < 0:
152            Messagebox.ok(title="Testzeitraum abgelaufen",
153                          message="Die 7-Tage-Testversion ist abgelaufen. Bitte kontaktieren Sie den Entwickler für eine Lizenz.",
154                          alert=True)
155            sys.exit()
156        else:
157            Messagebox.ok(title="Testversion",
158                          message=f"Testversion aktiv. Noch {verbleibend.days + 1} Tage verfügbar.", alert=False)
159        """
160        pruefe_testversion(root, verbleibend)
161
162
163def zeige_lizenz():
164    """Die Lizenz wird einfach angezeigt. Wird aus dem Menü aufgerufen."""
165    if not os.path.exists(LIZENZ):
166        Messagebox.ok(title="Lizenz", message="LICENSE.txt nicht gefunden.", alert=False)
167        return
168    lizfenster = tk.Toplevel()
169    #set_window_icon(lizfenster)
170    lizfenster.title("Lizenz")
171    lizfenster.geometry("600x500")
172    textfeld = scrolledtext.ScrolledText(lizfenster, wrap="word")
173    with open(LIZENZ, "r", encoding="utf-8") as f:
174        textfeld.insert("1.0", f.read())
175    textfeld.config(state="disabled")
176    lizfenster.update_idletasks()
177    lizfenster.minsize(lizfenster.winfo_width(), lizfenster.winfo_height())
178    textfeld.pack(fill="both", expand=True)
179
180def zeige_nutzungsdialog(root):
181    """Die Art der Nutzung muss ausgewählt und bestätigt werden. Wird nur bei der Erstnutzung aufgerufen."""
182    auswahlfenster = tk.Toplevel()
183    auswahlfenster.title("Nutzungsart wählen")
184    auswahlfenster.geometry("500x300")
185    auswahlfenster.grab_set()
186    auswahlfenster.resizable(False, True)
187    auswahlfenster.update_idletasks()
188    auswahlfenster.minsize(auswahlfenster.winfo_width(), auswahlfenster.winfo_height())
189
190    auswahl = tk.StringVar()
191    auswahl.set("privat")
192
193    def bestätigen():
194        JamfLogin(root)
195        auswahlfenster.destroy()
196
197    label = ttk.Label(auswahlfenster, text="Bitte wählen Sie die Art der Nutzung:")
198    label.pack(pady=10)
199
200    r1 = ttk.Radiobutton(auswahlfenster, text="Privat/Schule (dauerhaft erlaubt)", variable=auswahl, value="privat")
201    r2 = ttk.Radiobutton(auswahlfenster, text="Gewerblich/Testversion (7 Tage)", variable=auswahl, value="gewerblich")
202    r1.pack(anchor="w", padx=30, pady=5)
203    r2.pack(anchor="w", padx=30, pady=5)
204
205    button = ttk.Button(auswahlfenster, text="Bestätigen", command=bestätigen)
206    button.pack(pady=20)
207
208    auswahlfenster.wait_window()
209    return auswahl.get()
210
211def zustimmung_bereits_erfolgt():
212    """prüft, ob die Zustimmung bereits erfolgt ist."""
213    if os.path.exists(ZUSTIMMUNGSDATEI):
214        try:
215            print(f"✅ Schreiben/Lesen von: {ZUSTIMMUNGSDATEI}")
216            with open(ZUSTIMMUNGSDATEI, "r") as f:
217                data = json.load(f)
218                return data.get("zugestimmt", False)
219        except Exception:
220            return False
221    return False
222
223def speichere_zustimmung():
224    """speichert die erfolgte Zustimmung in der Zustimmungsdatei (json)"""
225    print(f"✅ Schreiben/Lesen von: {ZUSTIMMUNGSDATEI}")
226    with open(ZUSTIMMUNGSDATEI, "w") as f:
227        json.dump({"zugestimmt": True}, f)
228
229def show_license_dialog(root):
230    """zeigt den Lizenz-Dialog zu Beginn. Man wird aufgefordert, die Lizenz zu bestötigen."""
231    try:
232        with open(LIZENZ, "r", encoding="utf-8") as f:
233            license_text = f.read()
234    except FileNotFoundError:
235        Messagebox.show_error("LICENSE.txt nicht gefunden.", "Fehler", parent=root)
236        return False
237
238    if sys.platform == "darwin":
239      dialog = ttk.Toplevel(root)
240    else:
241       dialog = ttk.Toplevel(root, iconphoto=None)
242    #set_window_icon(dialog)
243    dialog.title("Lizenzvereinbarung")
244    dialog.minsize(700, 500)
245    dialog.transient(root)
246    dialog.grab_set()
247
248    # Label oben
249    ttk.Label(dialog, text="Bitte lesen Sie die Lizenzbedingungen:", font=("Helvetica", 12)).pack(pady=10)
250
251    # Textbereich in eigenem Frame
252    text_frame = ttk.Frame(dialog)
253    text_frame.pack(fill="both", expand=True, padx=20, pady=10)
254
255    text_area = ScrolledText(text_frame, autohide=True)
256    text_area.pack(fill="both", expand=True)
257    text_area.text.insert("1.0", license_text)
258    text_area.text.configure(state="disabled")
259
260    # Button-Frame
261    button_frame = ttk.Frame(dialog)
262    button_frame.pack(pady=10)
263
264    result = {"accepted": None}
265
266    def agree():
267        result["accepted"] = True
268        dialog.destroy()
269
270    def disagree():
271        result["accepted"] = False
272        dialog.destroy()
273
274    ttk.Button(button_frame, text="Ich stimme zu", command=agree, bootstyle="success").pack(side="left", padx=10)
275    ttk.Button(button_frame, text="Ich lehne ab", command=disagree, bootstyle="danger").pack(side="right", padx=10)
276
277    dialog.wait_window()
278    return result["accepted"]
279
280def show_help():
281    """zeigt die zu hilfe.html umgewandelte README.md im Browser an."""
282    help_file = get_resource_path("hilfe.html")
283    if not os.path.exists(help_file):
284        print(f"❌ Hilfe-Datei nicht gefunden: {help_file}")
285        return
286    url = f"file://{help_file}"
287    print(f"🌐 Öffne Hilfe: {url}")
288    webbrowser.open(url)
289
290def show_about():
291    """zeigt die Aboutbox"""
292    # messagebox.showinfo("Über Classload", "Classload\nVersion 1.0\n(c) 2025")
293    messagebox.showinfo("Über Classload","Classload\nzum Austausch von Daten mit Jamf\nVersion 0.9\n(c)2025 Christiane Borchel")
294
295def main():
296    """
297    Bei der ersten Nutzung wird die Lizenz angezeigt, die bestätigt werden muss.
298    Danach muss die Nutzungsart (privat/gewerblich) gewählt werden.
299    Bei allen späteren Nutzungen wird zunächst die Nutzungsart geprüft.
300    Falls sie gewerblich ist, wird zunächst angezeigt, wie viele Tage die Lizenz noch gültig ist.
301    Falls sie nicht mehr gültig ist, endet das Programm. Sonst wird nach Bestätigung das Login-Fenster angezeigt.
302    Bei privater/schulischer Nutzung wird direkt das Login-Fenster JAMF-Login angezeigt.
303    """
304    init_dpi_awareness()
305    if sys.platform == "darwin":
306      root = ttk.Window(themename="cosmo")
307    else:
308      root = ttk.Window(themename="cosmo", iconphoto=None)
309    root.title("Classload")
310
311    root.lift()
312    root.focus_force()
313    root.attributes("-topmost", True)
314    root.after(100, lambda: root.attributes("-topmost", False))
315
316    base_path = getattr(sys, '_MEIPASS', os.path.dirname(__file__))
317    # icon_path = os.path.join(base_path, "assets", "icon.png")
318    #icon_img = tk.PhotoImage(file=icon_path)
319    #icon_relpath = os.path.join("assets", "icon.png")  # Pfad zusammensetzen
320    if sys.platform == "darwin":
321        # macOS → kleineres, sicheres Icon verwenden
322        icon_relpath = os.path.join("assets", "icon_small.png")
323    else:
324        # Windows/Linux → großes Icon mit Alpha
325        icon_relpath = os.path.join("assets", "icon.png")
326
327    icon_path = get_resource_path(icon_relpath)  # Pfad für PyInstaller/py2app auflösen
328    icon_img = tk.PhotoImage(file=icon_path)
329    print("🔍 Icon-Pfad:", icon_path)
330    print("📦 Existiert:", os.path.exists(icon_path))
331    #root.iconphoto(True, icon_img)
332    #root.icon_img = icon_img
333    if sys.platform.startswith("win"):
334        try:
335            icon_path = os.path.join(base_path, "assets", "icon.ico")
336            root.iconbitmap(icon_path)
337        except Exception as e:
338            print(f"Icon konnte nicht gesetzt werden: {e}")
339    #set_window_icon(root)
340
341    if not zustimmung_bereits_erfolgt():
342        if not show_license_dialog(root):
343            return
344        speichere_zustimmung()
345
346    menubar = tk.Menu(root)
347    hilfe_menu = tk.Menu(menubar, tearoff=0)
348    hilfe_menu.add_command(label="Lizenz anzeigen", command=zeige_lizenz)
349    hilfe_menu.add_command(label="Hilfe anzeigen", command=lambda: show_help())
350
351    if platform.system() == "Darwin":
352        apple_menu = tk.Menu(menubar, name="apple", tearoff=0)
353        apple_menu.add_command(label="Über Classload", command=show_about)
354        menubar.add_cascade(menu=apple_menu)
355        menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
356        root["menu"] = menubar
357    else:
358        menubar.add_command(label="Über Classload", command=show_about)
359        menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
360        root.config(menu=menubar)
361    # menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
362    root.config(menu=menubar)
363
364    root.withdraw()  # root bleibt im Hintergrund, aber notwendig für Tkinter
365
366
367    pruefe_nutzungsart(root)
368    #login = JamfLogin(root)
369    root.iconphoto(True, icon_img)
370    #root.icon_img = icon_img
371    root.mainloop()
372
373
374if __name__ == "__main__":
375    try:
376        main()
377    except Exception as e:
378        import traceback
379        log_path = os.path.join(tempfile.gettempdir(), "classload_error.log")
380        #log_path = os.path.join(os.path.expanduser("~"), "Desktop", "classload_error.log")
381
382
383        with open(log_path, "w", encoding="utf-8") as f:
384            f.write(traceback.format_exc())
385        raise
JAMF_URL = ''
TOKEN = ''
config_dir = '/home/runner/.local/share/Classload'
ZUSTIMMUNGSDATEI = '/home/runner/.local/share/Classload/zustimmung.json'
NUTZUNGSDATEI = '/home/runner/.local/share/Classload/nutzung.json'
def get_resource_path(filename):
34def get_resource_path(filename):
35    """
36    Gibt den Pfad zur Datei zurück – funktioniert mit PyInstaller, py2app und lokal.
37    """
38    if hasattr(sys, '_MEIPASS'):
39        return os.path.join(sys._MEIPASS, filename)
40    elif hasattr(sys, 'frozen') and 'RESOURCEPATH' in os.environ:
41        return os.path.join(os.environ['RESOURCEPATH'], filename)
42    else:
43        # Lokaler Entwicklungsmodus – Bezug relativ zur Python-Datei
44        base_path = Path(__file__).resolve().parent
45        return str(base_path / filename)

Gibt den Pfad zur Datei zurück – funktioniert mit PyInstaller, py2app und lokal.

LIZENZ = '/home/runner/work/classload/classload/LICENSE.txt'
def init_dpi_awareness():
49def init_dpi_awareness():
50    """
51    Aktiviert DPI-Awareness unter Windows, um eine scharfe und korrekt skalierte
52    Darstellung in tkinter/ttkbootstrap-Fenstern zu gewährleisten.
53    Hat keinen Effekt auf macOS oder Linux.
54    """
55    if sys.platform.startswith("win"):
56        try:
57            import ctypes
58            # 1 = system DPI aware, 2 = per-monitor DPI aware
59            ctypes.windll.shcore.SetProcessDpiAwareness(1)
60        except Exception:
61            pass

Aktiviert DPI-Awareness unter Windows, um eine scharfe und korrekt skalierte Darstellung in tkinter/ttkbootstrap-Fenstern zu gewährleisten. Hat keinen Effekt auf macOS oder Linux.

def set_window_icon(widget):
63def set_window_icon(widget):
64    """setzt ein icon-Window. Wird aktuell nicht verwendet."""
65    try:
66        if sys.platform == "darwin":
67            # macOS: kleineres Icon verwenden, sonst kommt "path is bad"
68            icon_relpath = os.path.join("assets", "icon_small.png")
69        else:
70            # Windows/Linux: großes Icon mit Alpha verwenden
71            icon_relpath = os.path.join("assets", "icon.png")
72
73        icon_path = get_resource_path(icon_relpath)
74        print(f"🖼 Verwende Icon: {icon_path}")
75
76        icon_img = tk.PhotoImage(file=icon_path)
77        widget.iconphoto(True, icon_img)
78        widget.icon_img = icon_img  # Referenz halten!
79    except Exception as e:
80        print(f"⚠️ Icon konnte nicht gesetzt werden: {e}")

setzt ein icon-Window. Wird aktuell nicht verwendet.

def lade_nutzungsinfo():
82def lade_nutzungsinfo():
83    """öffnet die Nutzungsdatei, sofern vorhanden."""
84    if os.path.exists(NUTZUNGSDATEI):
85        with open(NUTZUNGSDATEI, "r") as f:
86            return json.load(f)
87    return {}

öffnet die Nutzungsdatei, sofern vorhanden.

def speichere_nutzungsinfo(info):
89def speichere_nutzungsinfo(info):
90    """speichert die Art der Nutzung(privat/gewerblich) in einer.json-Datei"""
91    with open(NUTZUNGSDATEI, "w") as f:
92        json.dump(info, f)

speichert die Art der Nutzung(privat/gewerblich) in einer.json-Datei

def pruefe_testversion(root, verbleibend):
 94def pruefe_testversion(root, verbleibend):
 95    """Prüft ob die gewerbliche Lizenz noch gültig ist. Wenn ja wird nach Bestätigung das Login-Fenster Jamf-Login aufgerufen."""
 96    root.deiconify()
 97    #root.geometry("400x300")  # Fenstergröße setzen
 98    root.minsize(400, 250)
 99    if verbleibend.days < 0:
100        def beenden():
101            root.destroy()
102            sys.exit()
103
104        frame = ttk.Frame(root, padding=30)
105        frame.pack(expand=True)
106        label = ttk.Label(frame,
107                          text="❌ Die 7-Tage-Testversion ist abgelaufen.\nBitte kontaktieren Sie den Entwickler für eine Lizenz.",
108                          font=("Arial", 12), justify="center")
109        label.pack()
110        root.after(5000, beenden)  # Automatisch schließen & Programm beenden
111    else:
112        def weiter():
113            JamfLogin(root)
114            hinweis.destroy()
115            root.withdraw()
116
117        hinweis = ttk.Frame(root, padding=20)
118        hinweis.place(relx=0.5, rely=0.5, anchor="center")
119
120        label = ttk.Label(hinweis, text=f"✔ Testversion aktiv\nNoch {verbleibend.days + 1} Tage verfügbar", font=("Arial", 11))
121        label.pack(pady=(0, 10))
122
123        btn = ttk.Button(hinweis, text="OK", command=weiter)
124        btn.pack()
125        root.update_idletasks()
126        root.minsize(root.winfo_width(), root.winfo_height())

Prüft ob die gewerbliche Lizenz noch gültig ist. Wenn ja wird nach Bestätigung das Login-Fenster Jamf-Login aufgerufen.

def pruefe_nutzungsart(root):
128def pruefe_nutzungsart(root):
129    """
130    Die Nutzungsart wird überprüft.
131    Sie wird abgespeichert, falls sie noch nicht abgespeichert ist, sonst wird sie gelesen.
132    Für gewerbliche Nutzung wird die verbleibende Nutzungszeit geprüft und ggf. weitergegeben.
133    Bei privater Nutzung wird direkt das Login-Fenster JamfLogin geöffnet.
134    """
135    info = lade_nutzungsinfo()
136    if "nutzung" not in info:
137        nutzungsart = zeige_nutzungsdialog(root)
138        if not nutzungsart:
139            Messagebox.ok(title="Abbruch", message="Nutzungstyp nicht festgelegt. Programm wird beendet.", alert=True)
140            sys.exit()
141        nutzungsart = nutzungsart.strip().lower()
142        info["nutzung"] = nutzungsart
143        if nutzungsart == "gewerblich":
144            info["startdatum"] = datetime.today().strftime("%Y-%m-%d")
145        speichere_nutzungsinfo(info)
146    elif info["nutzung"] == "privat":
147        JamfLogin(root)
148    elif info["nutzung"] == "gewerblich":
149        startdatum = datetime.strptime(info["startdatum"], "%Y-%m-%d")
150        verbleibend = (startdatum + timedelta(days=7)) - datetime.today()
151        """
152        if verbleibend.days < 0:
153            Messagebox.ok(title="Testzeitraum abgelaufen",
154                          message="Die 7-Tage-Testversion ist abgelaufen. Bitte kontaktieren Sie den Entwickler für eine Lizenz.",
155                          alert=True)
156            sys.exit()
157        else:
158            Messagebox.ok(title="Testversion",
159                          message=f"Testversion aktiv. Noch {verbleibend.days + 1} Tage verfügbar.", alert=False)
160        """
161        pruefe_testversion(root, verbleibend)

Die Nutzungsart wird überprüft. Sie wird abgespeichert, falls sie noch nicht abgespeichert ist, sonst wird sie gelesen. Für gewerbliche Nutzung wird die verbleibende Nutzungszeit geprüft und ggf. weitergegeben. Bei privater Nutzung wird direkt das Login-Fenster JamfLogin geöffnet.

def zeige_lizenz():
164def zeige_lizenz():
165    """Die Lizenz wird einfach angezeigt. Wird aus dem Menü aufgerufen."""
166    if not os.path.exists(LIZENZ):
167        Messagebox.ok(title="Lizenz", message="LICENSE.txt nicht gefunden.", alert=False)
168        return
169    lizfenster = tk.Toplevel()
170    #set_window_icon(lizfenster)
171    lizfenster.title("Lizenz")
172    lizfenster.geometry("600x500")
173    textfeld = scrolledtext.ScrolledText(lizfenster, wrap="word")
174    with open(LIZENZ, "r", encoding="utf-8") as f:
175        textfeld.insert("1.0", f.read())
176    textfeld.config(state="disabled")
177    lizfenster.update_idletasks()
178    lizfenster.minsize(lizfenster.winfo_width(), lizfenster.winfo_height())
179    textfeld.pack(fill="both", expand=True)

Die Lizenz wird einfach angezeigt. Wird aus dem Menü aufgerufen.

def zeige_nutzungsdialog(root):
181def zeige_nutzungsdialog(root):
182    """Die Art der Nutzung muss ausgewählt und bestätigt werden. Wird nur bei der Erstnutzung aufgerufen."""
183    auswahlfenster = tk.Toplevel()
184    auswahlfenster.title("Nutzungsart wählen")
185    auswahlfenster.geometry("500x300")
186    auswahlfenster.grab_set()
187    auswahlfenster.resizable(False, True)
188    auswahlfenster.update_idletasks()
189    auswahlfenster.minsize(auswahlfenster.winfo_width(), auswahlfenster.winfo_height())
190
191    auswahl = tk.StringVar()
192    auswahl.set("privat")
193
194    def bestätigen():
195        JamfLogin(root)
196        auswahlfenster.destroy()
197
198    label = ttk.Label(auswahlfenster, text="Bitte wählen Sie die Art der Nutzung:")
199    label.pack(pady=10)
200
201    r1 = ttk.Radiobutton(auswahlfenster, text="Privat/Schule (dauerhaft erlaubt)", variable=auswahl, value="privat")
202    r2 = ttk.Radiobutton(auswahlfenster, text="Gewerblich/Testversion (7 Tage)", variable=auswahl, value="gewerblich")
203    r1.pack(anchor="w", padx=30, pady=5)
204    r2.pack(anchor="w", padx=30, pady=5)
205
206    button = ttk.Button(auswahlfenster, text="Bestätigen", command=bestätigen)
207    button.pack(pady=20)
208
209    auswahlfenster.wait_window()
210    return auswahl.get()

Die Art der Nutzung muss ausgewählt und bestätigt werden. Wird nur bei der Erstnutzung aufgerufen.

def zustimmung_bereits_erfolgt():
212def zustimmung_bereits_erfolgt():
213    """prüft, ob die Zustimmung bereits erfolgt ist."""
214    if os.path.exists(ZUSTIMMUNGSDATEI):
215        try:
216            print(f"✅ Schreiben/Lesen von: {ZUSTIMMUNGSDATEI}")
217            with open(ZUSTIMMUNGSDATEI, "r") as f:
218                data = json.load(f)
219                return data.get("zugestimmt", False)
220        except Exception:
221            return False
222    return False

prüft, ob die Zustimmung bereits erfolgt ist.

def speichere_zustimmung():
224def speichere_zustimmung():
225    """speichert die erfolgte Zustimmung in der Zustimmungsdatei (json)"""
226    print(f"✅ Schreiben/Lesen von: {ZUSTIMMUNGSDATEI}")
227    with open(ZUSTIMMUNGSDATEI, "w") as f:
228        json.dump({"zugestimmt": True}, f)

speichert die erfolgte Zustimmung in der Zustimmungsdatei (json)

def show_license_dialog(root):
230def show_license_dialog(root):
231    """zeigt den Lizenz-Dialog zu Beginn. Man wird aufgefordert, die Lizenz zu bestötigen."""
232    try:
233        with open(LIZENZ, "r", encoding="utf-8") as f:
234            license_text = f.read()
235    except FileNotFoundError:
236        Messagebox.show_error("LICENSE.txt nicht gefunden.", "Fehler", parent=root)
237        return False
238
239    if sys.platform == "darwin":
240      dialog = ttk.Toplevel(root)
241    else:
242       dialog = ttk.Toplevel(root, iconphoto=None)
243    #set_window_icon(dialog)
244    dialog.title("Lizenzvereinbarung")
245    dialog.minsize(700, 500)
246    dialog.transient(root)
247    dialog.grab_set()
248
249    # Label oben
250    ttk.Label(dialog, text="Bitte lesen Sie die Lizenzbedingungen:", font=("Helvetica", 12)).pack(pady=10)
251
252    # Textbereich in eigenem Frame
253    text_frame = ttk.Frame(dialog)
254    text_frame.pack(fill="both", expand=True, padx=20, pady=10)
255
256    text_area = ScrolledText(text_frame, autohide=True)
257    text_area.pack(fill="both", expand=True)
258    text_area.text.insert("1.0", license_text)
259    text_area.text.configure(state="disabled")
260
261    # Button-Frame
262    button_frame = ttk.Frame(dialog)
263    button_frame.pack(pady=10)
264
265    result = {"accepted": None}
266
267    def agree():
268        result["accepted"] = True
269        dialog.destroy()
270
271    def disagree():
272        result["accepted"] = False
273        dialog.destroy()
274
275    ttk.Button(button_frame, text="Ich stimme zu", command=agree, bootstyle="success").pack(side="left", padx=10)
276    ttk.Button(button_frame, text="Ich lehne ab", command=disagree, bootstyle="danger").pack(side="right", padx=10)
277
278    dialog.wait_window()
279    return result["accepted"]

zeigt den Lizenz-Dialog zu Beginn. Man wird aufgefordert, die Lizenz zu bestötigen.

def show_help():
281def show_help():
282    """zeigt die zu hilfe.html umgewandelte README.md im Browser an."""
283    help_file = get_resource_path("hilfe.html")
284    if not os.path.exists(help_file):
285        print(f"❌ Hilfe-Datei nicht gefunden: {help_file}")
286        return
287    url = f"file://{help_file}"
288    print(f"🌐 Öffne Hilfe: {url}")
289    webbrowser.open(url)

zeigt die zu hilfe.html umgewandelte README.md im Browser an.

def show_about():
291def show_about():
292    """zeigt die Aboutbox"""
293    # messagebox.showinfo("Über Classload", "Classload\nVersion 1.0\n(c) 2025")
294    messagebox.showinfo("Über Classload","Classload\nzum Austausch von Daten mit Jamf\nVersion 0.9\n(c)2025 Christiane Borchel")

zeigt die Aboutbox

def main():
296def main():
297    """
298    Bei der ersten Nutzung wird die Lizenz angezeigt, die bestätigt werden muss.
299    Danach muss die Nutzungsart (privat/gewerblich) gewählt werden.
300    Bei allen späteren Nutzungen wird zunächst die Nutzungsart geprüft.
301    Falls sie gewerblich ist, wird zunächst angezeigt, wie viele Tage die Lizenz noch gültig ist.
302    Falls sie nicht mehr gültig ist, endet das Programm. Sonst wird nach Bestätigung das Login-Fenster angezeigt.
303    Bei privater/schulischer Nutzung wird direkt das Login-Fenster JAMF-Login angezeigt.
304    """
305    init_dpi_awareness()
306    if sys.platform == "darwin":
307      root = ttk.Window(themename="cosmo")
308    else:
309      root = ttk.Window(themename="cosmo", iconphoto=None)
310    root.title("Classload")
311
312    root.lift()
313    root.focus_force()
314    root.attributes("-topmost", True)
315    root.after(100, lambda: root.attributes("-topmost", False))
316
317    base_path = getattr(sys, '_MEIPASS', os.path.dirname(__file__))
318    # icon_path = os.path.join(base_path, "assets", "icon.png")
319    #icon_img = tk.PhotoImage(file=icon_path)
320    #icon_relpath = os.path.join("assets", "icon.png")  # Pfad zusammensetzen
321    if sys.platform == "darwin":
322        # macOS → kleineres, sicheres Icon verwenden
323        icon_relpath = os.path.join("assets", "icon_small.png")
324    else:
325        # Windows/Linux → großes Icon mit Alpha
326        icon_relpath = os.path.join("assets", "icon.png")
327
328    icon_path = get_resource_path(icon_relpath)  # Pfad für PyInstaller/py2app auflösen
329    icon_img = tk.PhotoImage(file=icon_path)
330    print("🔍 Icon-Pfad:", icon_path)
331    print("📦 Existiert:", os.path.exists(icon_path))
332    #root.iconphoto(True, icon_img)
333    #root.icon_img = icon_img
334    if sys.platform.startswith("win"):
335        try:
336            icon_path = os.path.join(base_path, "assets", "icon.ico")
337            root.iconbitmap(icon_path)
338        except Exception as e:
339            print(f"Icon konnte nicht gesetzt werden: {e}")
340    #set_window_icon(root)
341
342    if not zustimmung_bereits_erfolgt():
343        if not show_license_dialog(root):
344            return
345        speichere_zustimmung()
346
347    menubar = tk.Menu(root)
348    hilfe_menu = tk.Menu(menubar, tearoff=0)
349    hilfe_menu.add_command(label="Lizenz anzeigen", command=zeige_lizenz)
350    hilfe_menu.add_command(label="Hilfe anzeigen", command=lambda: show_help())
351
352    if platform.system() == "Darwin":
353        apple_menu = tk.Menu(menubar, name="apple", tearoff=0)
354        apple_menu.add_command(label="Über Classload", command=show_about)
355        menubar.add_cascade(menu=apple_menu)
356        menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
357        root["menu"] = menubar
358    else:
359        menubar.add_command(label="Über Classload", command=show_about)
360        menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
361        root.config(menu=menubar)
362    # menubar.add_cascade(label="Hilfe", menu=hilfe_menu)
363    root.config(menu=menubar)
364
365    root.withdraw()  # root bleibt im Hintergrund, aber notwendig für Tkinter
366
367
368    pruefe_nutzungsart(root)
369    #login = JamfLogin(root)
370    root.iconphoto(True, icon_img)
371    #root.icon_img = icon_img
372    root.mainloop()

Bei der ersten Nutzung wird die Lizenz angezeigt, die bestätigt werden muss. Danach muss die Nutzungsart (privat/gewerblich) gewählt werden. Bei allen späteren Nutzungen wird zunächst die Nutzungsart geprüft. Falls sie gewerblich ist, wird zunächst angezeigt, wie viele Tage die Lizenz noch gültig ist. Falls sie nicht mehr gültig ist, endet das Programm. Sonst wird nach Bestätigung das Login-Fenster angezeigt. Bei privater/schulischer Nutzung wird direkt das Login-Fenster JAMF-Login angezeigt.