Created
July 29, 2026 23:30
-
-
Save Chadzilla-arilla/6a2f1918df43d1d8d405a38560d2f736 to your computer and use it in GitHub Desktop.
aio app launcher
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import tkinter as tk | |
| from tkinter import ttk, filedialog, messagebox, simpledialog | |
| import sqlite3, os, sys, subprocess, platform, zipfile, shutil | |
| from datetime import datetime | |
| import tkinter.font as tkFont | |
| import csv | |
| class CustomAppLunch: | |
| def __init__(self, master): | |
| self.master = master | |
| master.title("App Lunch 5") | |
| master.geometry("1100x600") | |
| master.minsize(800,600) | |
| master.resizable(True, True) | |
| self.unsaved = False | |
| self.undo_stack = [] | |
| # Data structures for app items and layout configuration. | |
| self.app_items = {} | |
| self.layout_config = {} | |
| self.next_iid = 1 | |
| # Set the default database path (same base name as script with .db extension) | |
| self.db_path = self.get_default_db_path() | |
| new_db = not os.path.exists(self.db_path) | |
| # Open the SQLite connection and create tables. | |
| self.conn = sqlite3.connect(self.db_path) | |
| self.conn.row_factory = sqlite3.Row | |
| self.create_tables() | |
| # If no DB exists, allow user to import a CSV. | |
| if new_db: | |
| csv_file = filedialog.askopenfilename(title="No database found. Select CSV file to import", | |
| filetypes=[("CSV files", "*.csv")]) | |
| if csv_file: | |
| self.import_csv_to_db(csv_file) | |
| # Now load data from the database. | |
| self.load_db() | |
| # Configure grid layout for master. | |
| master.grid_columnconfigure(0, weight=0, minsize=320) # Left: Tree view & controls | |
| master.grid_columnconfigure(1, weight=0, minsize=50) # Middle: Control buttons | |
| master.grid_columnconfigure(2, weight=1, minsize=400) # Right: App Info | |
| master.grid_rowconfigure(0, weight=1) | |
| master.grid_rowconfigure(1, weight=0) # Log row | |
| master.grid_rowconfigure(2, weight=0) # Lunch/Backup row | |
| self.left_frame = tk.Frame(master, bg="#f0f0f0", width=320, relief=tk.FLAT, borderwidth=0) | |
| self.left_frame.grid(row=0, column=0, sticky="nsew", padx=(5,5), pady=5) | |
| self.tree_frame = tk.Frame(self.left_frame, bg="#f0f0f0") | |
| style = ttk.Style() | |
| style.configure("Treeview", background="white", fieldbackground="white", borderwidth=0) | |
| style.configure("Treeview.Heading", background="#e0e0e0", foreground="black", relief=tk.FLAT) | |
| style.map('Treeview', background=[('selected', '#0078d4')], foreground=[('selected', 'white')]) | |
| # =============================== | |
| # Left Frame: Tree View and Add/Remove/Edit Buttons | |
| # =============================== | |
| self.left_frame = tk.Frame(master, bg="lightgray", width=320) | |
| self.left_frame.grid(row=0, column=0, sticky="nsew", padx=(10,5), pady=10) | |
| self.left_frame.grid_propagate(False) | |
| self.tree_frame = tk.Frame(self.left_frame, bg="lightgray") | |
| self.tree_frame.pack(side=tk.TOP, fill=tk.BOTH, expand=True) | |
| # Treeview with three columns: primary tree column and "version" and "Lunches" | |
| self.tree = ttk.Treeview(self.tree_frame, columns=("version", "Lunches"), show="tree headings") | |
| self.tree.heading("#0", text="App", anchor="w") | |
| self.tree.heading("version", text="Version", anchor="e") | |
| self.tree.heading("Lunches", text="Lunches", anchor="e") | |
| self.tree.column("#0", anchor="w") | |
| self.tree.column("version", anchor="e", width=80) | |
| self.tree.column("Lunches", anchor="e", width=80) | |
| self.tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) | |
| self.tree_scrollbar = tk.Scrollbar(self.tree_frame, orient="vertical", command=self.tree.yview) | |
| self.tree_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) | |
| self.tree.config(yscrollcommand=self.tree_scrollbar.set) | |
| self.tree.bind("<<TreeviewSelect>>", self.on_app_selected) | |
| button_font = tkFont.Font(family="Segoe UI", size=10) | |
| self.left_control_frame = tk.Frame(self.left_frame, bg="#f0f0f0") | |
| self.left_control_frame.pack(side=tk.BOTTOM, fill=tk.X, pady=(5,0)) | |
| self.add_button = tk.Button(self.left_control_frame, text="Add", command=self.add_entry, font=button_font, width=6, height=1, bg="#0078d4", fg="white", relief=tk.FLAT, cursor="hand2") | |
| self.add_button.pack(side=tk.LEFT, padx=2) | |
| self.remove_button = tk.Button(self.left_control_frame, text="Remove", command=self.remove_entry, font=button_font, width=8, height=1, bg="#d83b01", fg="white", relief=tk.FLAT, cursor="hand2") | |
| self.remove_button.pack(side=tk.LEFT, padx=2) | |
| self.edit_button = tk.Button(self.left_control_frame, text="Edit", command=self.edit_entry, font=button_font, width=6, height=1, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2") | |
| self.edit_button.pack(side=tk.LEFT, padx=2) | |
| self.undo_button = tk.Button(self.left_control_frame, text="Undo", command=self.undo_move, font=button_font, width=6, height=1, bg="#5c5c5c", fg="white", relief=tk.FLAT, cursor="hand2") | |
| self.undo_button.pack(side=tk.RIGHT, padx=2) | |
| # =============================== | |
| # Middle Frame: Control Buttons (vertical layout) | |
| # =============================== | |
| button_font = tkFont.Font(family="Segoe UI", size=12, weight="bold") | |
| self.mid_button_frame = tk.Frame(master, bg="#f8f8f8") | |
| self.mid_button_frame.grid(row=0, column=1, sticky="ns", padx=(0,5), pady=10) | |
| self.skip_up_button = tk.Button(self.mid_button_frame, text="⇈", command=self.move_item_skip_up, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.skip_up_button.grid(row=0, column=0, pady=1, padx=1, sticky="ew") | |
| self.up_button = tk.Button(self.mid_button_frame, text="↑", command=self.move_item_up, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.up_button.grid(row=1, column=0, pady=1, padx=1, sticky="ew") | |
| self.down_button = tk.Button(self.mid_button_frame, text="↓", command=self.move_item_down, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.down_button.grid(row=2, column=0, pady=1, padx=1, sticky="ew") | |
| self.skip_down_button = tk.Button(self.mid_button_frame, text="⇊", command=self.move_item_skip_down, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.skip_down_button.grid(row=3, column=0, pady=1, padx=1, sticky="ew") | |
| self.indent_button = tk.Button(self.mid_button_frame, text="→", command=self.indent_item, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.indent_button.grid(row=4, column=0, pady=1, padx=1, sticky="ew") | |
| self.outdent_button = tk.Button(self.mid_button_frame, text="←", command=self.outdent_item, font=button_font, width=3, height=1, bg="#e0e0e0", relief=tk.FLAT, cursor="hand2") | |
| self.outdent_button.grid(row=5, column=0, pady=1, padx=1, sticky="ew") | |
| # =============================== | |
| # Right Frame: App Info | |
| # =============================== | |
| self.right_frame = tk.LabelFrame(master, text="App Info", font=("Segoe UI", 11, "bold"), bg="#ffffff", relief=tk.FLAT, borderwidth=1) | |
| self.right_frame.grid(row=0, column=2, sticky="nsew", padx=(5,10), pady=10) | |
| self.right_frame.grid_columnconfigure(1, weight=1) | |
| self.field_names = ["App", "Path", "Filename", "Version"] | |
| self.field_vars = {} | |
| for i, field in enumerate(self.field_names): | |
| lbl = tk.Label(self.right_frame, text=field + ":", bg="#ffffff", font=("Segoe UI", 10)) | |
| lbl.grid(row=i, column=0, sticky="w", padx=10, pady=3) | |
| var = tk.StringVar() | |
| ent = tk.Entry(self.right_frame, textvariable=var, state="readonly", width=40, readonlybackground="#f8f8f8", relief=tk.FLAT, borderwidth=1, highlightthickness=1, highlightcolor="#0078d4") | |
| ent.grid(row=i, column=1, sticky="ew", padx=5, pady=2) | |
| self.field_vars[field] = var | |
| desc_label = tk.Label(self.right_frame, text="Description:", bg="#ffffff", font=("Segoe UI", 10)) | |
| desc_label.grid(row=len(self.field_names), column=0, sticky="nw", padx=10, pady=3) | |
| desc_frame = tk.Frame(self.right_frame) | |
| desc_frame.grid(row=len(self.field_names), column=1, columnspan=2, sticky="nsew", padx=5, pady=2) | |
| self.desc_text = tk.Text(desc_frame, height=6, width=36, wrap="word", relief=tk.FLAT, borderwidth=1, highlightthickness=1, highlightcolor="#0078d4") | |
| self.desc_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) | |
| desc_scroll = tk.Scrollbar(desc_frame, command=self.desc_text.yview) | |
| desc_scroll.pack(side=tk.RIGHT, fill=tk.Y) | |
| self.desc_text.config(yscrollcommand=desc_scroll.set) | |
| self.right_frame.grid_rowconfigure(len(self.field_names), weight=1) | |
| vh_label = tk.Label(self.right_frame, text="Version History:", bg="#ffffff", font=("Segoe UI", 10)) | |
| vh_label.grid(row=len(self.field_names)+1, column=0, sticky="w", padx=10, pady=3) | |
| self.version_combobox = ttk.Combobox(self.right_frame, state="readonly") | |
| self.version_combobox.grid(row=len(self.field_names)+1, column=1, sticky="w", padx=5, pady=2) | |
| self.version_combobox.bind("<<ComboboxSelected>>", self.on_version_selected) | |
| self.Lunch_count_label = tk.Label(self.right_frame, text="", bg="#ffffff", font=("Segoe UI", 9), fg="#0078d4") | |
| self.Lunch_count_label.grid(row=len(self.field_names)+1, column=2, sticky="w", padx=5, pady=2) | |
| self.add_version_button = tk.Button(self.right_frame, text="Add New Version", command=self.add_new_version, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 9)) | |
| self.add_version_button.grid(row=len(self.field_names)+2, column=0, columnspan=2, sticky="w", padx=5, pady=2) | |
| # Rating UI | |
| ''' | |
| rating_label = tk.Label(self.right_frame, text="Rating:", bg="#ffffff", font=("Segoe UI", 10)) | |
| rating_label.grid(row=len(self.field_names)+3, column=0, sticky="w", padx=10, pady=3) | |
| rating_frame = tk.Frame(self.right_frame, bg="#ffffff") | |
| rating_frame.grid(row=len(self.field_names)+3, column=1, sticky="w", padx=5, pady=2) | |
| self.rating_var = tk.IntVar(value=0) | |
| self.rating_label = tk.Label(rating_frame, text="☆☆☆☆☆", bg="#ffffff", font=("Segoe UI", 14), fg="#ffa500") | |
| self.rating_label.pack(side=tk.LEFT, padx=(0, 5)) | |
| self.rating_spinbox = tk.Spinbox(rating_frame, from_=0, to=5, textvariable=self.rating_var, width=5, | |
| command=self.on_rating_changed, font=("Segoe UI", 9)) | |
| self.rating_spinbox.pack(side=tk.LEFT) | |
| self.rating_spinbox.bind("<Return>", lambda e: self.on_rating_changed()) | |
| self.rating_spinbox.bind("<FocusOut>", lambda e: self.on_rating_changed()) | |
| ''' | |
| # =============================== | |
| # Log Frame | |
| # =============================== | |
| self.log_frame = tk.Frame(master) | |
| self.log_frame.grid(row=1, column=0, columnspan=3, sticky="nsew", padx=10, pady=(0,10)) | |
| self.log_text = tk.Text(self.log_frame, height=5, state="disabled", relief=tk.FLAT, borderwidth=1, bg="#f8f8f8", font=("Consolas", 9)) | |
| self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) | |
| self.log_scrollbar = tk.Scrollbar(self.log_frame, command=self.log_text.yview) | |
| self.log_scrollbar.pack(side=tk.RIGHT, fill=tk.Y) | |
| self.log_text.config(yscrollcommand=self.log_scrollbar.set) | |
| # =============================== | |
| # Lunch/Backup Frame | |
| # =============================== | |
| self.Lunch_frame = tk.Frame(master) | |
| self.Lunch_frame.grid(row=2, column=0, columnspan=3, sticky="e", padx=(0,10), pady=(0,10)) | |
| self.save_button = tk.Button(self.Lunch_frame, text="Save", command=self.manual_save, bg="#107c10", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10, "bold")) | |
| self.save_button.pack(side=tk.LEFT, padx=3) | |
| self.backup_button = tk.Button(self.Lunch_frame, text="Backup", command=self.backup_files, bg="#5c5c5c", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10)) | |
| self.backup_button.pack(side=tk.LEFT, padx=3) | |
| self.Lunch_button = tk.Button(self.Lunch_frame, text="Launch →", command=self.Lunch_app, bg="#0078d4", fg="white", relief=tk.FLAT, cursor="hand2", font=("Segoe UI", 10, "bold")) | |
| self.Lunch_button.pack(side=tk.LEFT, padx=3) | |
| self.build_tree() | |
| self.master.protocol("WM_DELETE_WINDOW", self.on_closing) | |
| def get_default_db_path(self): | |
| try: | |
| script_file = __file__ | |
| except NameError: | |
| script_file = sys.argv[0] | |
| script_file = os.path.abspath(script_file) | |
| base, _ = os.path.splitext(script_file) | |
| return base + ".db" | |
| def create_tables(self): | |
| c = self.conn.cursor() | |
| c.execute("""CREATE TABLE IF NOT EXISTS app_items ( | |
| iid INTEGER PRIMARY KEY AUTOINCREMENT, | |
| App TEXT, | |
| Path TEXT, | |
| Filename TEXT, | |
| Version TEXT, | |
| Description TEXT, | |
| Parent TEXT, | |
| LunchCount INTEGER, | |
| Version_float REAL, | |
| Rating INTEGER DEFAULT 0 | |
| )""") | |
| c.execute("""CREATE TABLE IF NOT EXISTS config ( | |
| key TEXT PRIMARY KEY, | |
| value TEXT | |
| )""") | |
| self.conn.commit() | |
| # Add Rating column to existing databases if it doesn't exist | |
| try: | |
| c.execute("SELECT Rating FROM app_items LIMIT 1") | |
| except sqlite3.OperationalError: | |
| c.execute("ALTER TABLE app_items ADD COLUMN Rating INTEGER DEFAULT 0") | |
| self.conn.commit() | |
| def import_csv_to_db(self, csv_file): | |
| c = self.conn.cursor() | |
| with open(csv_file, newline='', encoding="utf-8") as f: | |
| reader = csv.DictReader(f) | |
| for row in reader: | |
| if row.get("iid", "") == "__CONFIG__": | |
| config_str = row.get("Description", "") | |
| for setting in config_str.split(";"): | |
| if "=" in setting: | |
| key, value = setting.split("=", 1) | |
| c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key.strip(), value.strip())) | |
| else: | |
| try: | |
| lunch = int(row.get("LunchCount", 0)) | |
| except: | |
| lunch = 0 | |
| try: | |
| vf = float(row.get("Version", "0")) | |
| except: | |
| vf = 0.0 | |
| try: | |
| rating = int(row.get("Rating", 0)) | |
| if rating < 0 or rating > 5: | |
| rating = 0 | |
| except: | |
| rating = 0 | |
| c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", | |
| (row.get("iid"), row.get("App"), row.get("Path"), row.get("Filename"), | |
| row.get("Version"), row.get("Description"), row.get("Parent", ""), lunch, vf, rating)) | |
| self.conn.commit() | |
| def load_db(self): | |
| c = self.conn.cursor() | |
| c.execute("SELECT * FROM app_items") | |
| rows = c.fetchall() | |
| self.app_items = {} | |
| self.next_iid = 1 | |
| for row in rows: | |
| item = dict(row) | |
| self.app_items[str(item["iid"])] = item | |
| try: | |
| num = int(item["iid"]) | |
| if num >= self.next_iid: | |
| self.next_iid = num + 1 | |
| except: | |
| pass | |
| c.execute("SELECT key, value FROM config") | |
| rows = c.fetchall() | |
| self.layout_config = {row["key"]: row["value"] for row in rows} | |
| self.conn.commit() | |
| def save_db(self): | |
| c = self.conn.cursor() | |
| c.execute("DELETE FROM app_items") | |
| for item in self.app_items.values(): | |
| c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", | |
| (item["iid"], item["App"], item["Path"], item["Filename"], item["Version"], | |
| item["Description"], item["Parent"], item["LunchCount"], item.get("Version_float", 0.0), item.get("Rating", 0))) | |
| for key, value in self.layout_config.items(): | |
| c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value)) | |
| self.conn.commit() | |
| self.log_message(f"Database saved to {self.db_path}.") | |
| def build_tree(self): | |
| for item in self.tree.get_children(): | |
| self.tree.delete(item) | |
| def insert_item_and_children(iid): | |
| item = self.app_items[iid] | |
| parent = item["Parent"] if item["Parent"] else "" | |
| display_text = f"{item['App']} ({item['Version']})" if item["Version"] else item["App"] | |
| self.tree.insert(parent, "end", iid, text=display_text, values=(item["Version"], item.get("LunchCount", 0))) | |
| for child_iid in [cid for cid, citem in self.app_items.items() if str(citem["Parent"]) == iid]: | |
| insert_item_and_children(child_iid) | |
| for iid, item in self.app_items.items(): | |
| if item["Parent"] == "": | |
| insert_item_and_children(iid) | |
| top_items = self.tree.get_children("") | |
| if top_items: | |
| self.tree.selection_set(top_items[0]) | |
| self.tree.focus(top_items[0]) | |
| self.display_app_info(top_items[0]) | |
| def restore_layout_config(self): | |
| # In this grid version, we simulate layout config restoration. | |
| default_vertical = (0, 400) | |
| default_top = (320, 0) | |
| default_bottom = (0, 100) | |
| try: | |
| vs_str = self.layout_config.get("vertical_sash", f"{default_vertical[0]},{default_vertical[1]}") | |
| ts_str = self.layout_config.get("top_sash", f"{default_top[0]},{default_top[1]}") | |
| bs_str = self.layout_config.get("bottom_sash", f"{default_bottom[0]},{default_bottom[1]}") | |
| vs = tuple(int(x) for x in vs_str.split(",")) | |
| ts = tuple(int(x) for x in ts_str.split(",")) | |
| bs = tuple(int(x) for x in bs_str.split(",")) | |
| except Exception as e: | |
| vs, ts, bs = default_vertical, default_top, default_bottom | |
| # Since we are using grid, there's no actual sash to move. | |
| print("Layout restored from DB config (simulated):", vs, ts, bs) | |
| def reset_layout(self): | |
| default_vertical = (0, 400) | |
| default_top = (320, 0) | |
| default_bottom = (0, 100) | |
| self.layout_config["vertical_sash"] = f"{default_vertical[0]},{default_vertical[1]}" | |
| self.layout_config["top_sash"] = f"{default_top[0]},{default_top[1]}" | |
| self.layout_config["bottom_sash"] = f"{default_bottom[0]},{default_bottom[1]}" | |
| self.save_db() | |
| print("Layout reset to defaults.") | |
| def on_app_selected(self, event): | |
| selection = self.tree.selection() | |
| if selection: | |
| iid = selection[0] | |
| self.display_app_info(iid) | |
| def display_app_info(self, iid): | |
| if iid not in self.app_items: | |
| return | |
| selected_item = self.app_items[iid] | |
| group = [item for item in self.app_items.values() if item["App"] == selected_item["App"] and item["Parent"] == selected_item["Parent"]] | |
| if not group: | |
| return | |
| selected_row = max(group, key=lambda r: r.get("Version_float", 0.0)) | |
| for field in self.field_names: | |
| self.field_vars[field].set(selected_row.get(field, "")) | |
| self.desc_text.delete("1.0", tk.END) | |
| self.desc_text.insert(tk.END, selected_row.get("Description", "")) | |
| versions = [item["Version"] for item in group] | |
| unique_versions = sorted(list(set(versions)), key=lambda v: float(v) if v.replace('.', '', 1).isdigit() else 0, reverse=True) | |
| self.version_combobox['values'] = unique_versions | |
| self.version_combobox.set(selected_row["Version"]) | |
| if len(unique_versions) <= 1: | |
| self.version_combobox.configure(state="disabled") | |
| else: | |
| self.version_combobox.configure(state="readonly") | |
| Lunch_count = selected_row.get("LunchCount", 0) | |
| if Lunch_count > 0: | |
| self.Lunch_count_label.config(text=f"Lunches: {Lunch_count}") | |
| else: | |
| self.Lunch_count_label.config(text="") | |
| # Update rating display | |
| ''' | |
| rating = selected_row.get("Rating", 0) | |
| self.rating_var.set(rating) | |
| self.update_rating_stars(rating) | |
| ''' | |
| display_text = f"{selected_row['App']} ({selected_row['Version']})" if selected_row.get("Version") else selected_row["App"] | |
| self.tree.item(iid, text=display_text, values=(selected_row["Version"], selected_row.get("LunchCount", 0))) | |
| def on_version_selected(self, event): | |
| selection = self.tree.selection() | |
| if selection: | |
| current_iid = selection[0] | |
| current_item = self.app_items[current_iid] | |
| group = [(iid, item) for iid, item in self.app_items.items() if item["App"] == current_item["App"] and item["Parent"] == current_item["Parent"]] | |
| selected_version = self.version_combobox.get() | |
| for iid, item in group: | |
| if item.get("Version") == selected_version: | |
| for field in self.field_names: | |
| self.field_vars[field].set(item.get(field, "")) | |
| self.desc_text.delete("1.0", tk.END) | |
| self.desc_text.insert(tk.END, item.get("Description", "")) | |
| Lunch_count = item.get("LunchCount", 0) | |
| if Lunch_count > 0: | |
| self.Lunch_count_label.config(text=f"Lunches: {Lunch_count}") | |
| else: | |
| self.Lunch_count_label.config(text="") | |
| # Update rating display | |
| ''' | |
| rating = item.get("Rating", 0) | |
| self.rating_var.set(rating) | |
| self.update_rating_stars(rating) | |
| ''' | |
| break | |
| ''' | |
| def on_rating_changed(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| try: | |
| new_rating = int(self.rating_var.get()) | |
| if new_rating < 0: | |
| new_rating = 0 | |
| elif new_rating > 5: | |
| new_rating = 5 | |
| self.rating_var.set(new_rating) | |
| except ValueError: | |
| new_rating = 0 | |
| self.rating_var.set(0) | |
| # Update the rating for the selected item | |
| if iid in self.app_items: | |
| old_rating = self.app_items[iid].get("Rating", 0) | |
| self.app_items[iid]["Rating"] = new_rating | |
| self.update_rating_stars(new_rating) | |
| # Auto-save the rating change | |
| self.save_db() | |
| self.unsaved = False | |
| #self.save_button.config(bg="#107c10") | |
| if old_rating != new_rating: | |
| self.log_message(f"Rating updated to {new_rating} stars for '{self.app_items[iid]['App']}'") | |
| def update_rating_stars(self, rating): | |
| """Update the star display based on rating value""" | |
| filled = "★" * rating | |
| empty = "☆" * (5 - rating) | |
| self.rating_label.config(text=filled + empty) | |
| ''' | |
| def record_move(self, iid): | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| try: | |
| old_index = siblings.index(iid) | |
| except ValueError: | |
| old_index = 0 | |
| self.undo_stack.append((iid, parent, old_index)) | |
| if len(self.undo_stack) > 10: | |
| self.undo_stack.pop(0) | |
| def undo_move(self): | |
| if not self.undo_stack: | |
| messagebox.showinfo("Undo", "No moves to undo.") | |
| return | |
| iid, old_parent, old_index = self.undo_stack.pop() | |
| if not self.tree.exists(iid): | |
| messagebox.showerror("Undo Error", "The item to undo no longer exists.") | |
| return | |
| self.tree.move(iid, old_parent, old_index) | |
| self.tree.selection_set(iid) | |
| self.mark_unsaved() | |
| self.log_message(f"Undid move of item '{self.app_items[iid]['App']}' to its previous position.") | |
| def mark_unsaved(self): | |
| self.unsaved = True | |
| # self.save_button.config(bg="#ff8c00") | |
| def manual_save(self): | |
| self.save_db() | |
| self.unsaved = False | |
| #self.save_button.config(bg="#107c10") | |
| self.log_message("Changes saved.") | |
| def log_message(self, msg): | |
| timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") | |
| full_msg = f"{timestamp}: {msg}\n" | |
| self.log_text.config(state="normal") | |
| self.log_text.insert(tk.END, full_msg) | |
| self.log_text.config(state="disabled") | |
| self.log_text.see(tk.END) | |
| def add_entry(self): | |
| file_path = filedialog.askopenfilename(title="Select a file to add") | |
| if not file_path: | |
| return | |
| file_name = os.path.basename(file_path) | |
| file_dir = os.path.dirname(file_path) | |
| exists = any(item for item in self.app_items.values() if item["App"] == file_name and item["Parent"] == "") | |
| if exists: | |
| if messagebox.askyesno("App Exists", f"App '{file_name}' already exists.\nDo you want to add a new version?"): | |
| self.add_new_version() | |
| return | |
| version = simpledialog.askstring("Version", f"Enter version number for '{file_name}':") | |
| if version is None: | |
| return | |
| new_iid = str(self.next_iid) | |
| self.next_iid += 1 | |
| new_row = { | |
| "iid": new_iid, | |
| "App": file_name, | |
| "Path": file_dir, | |
| "Filename": file_name, | |
| "Version": version, | |
| "Description": "", | |
| "Parent": "", | |
| "LunchCount": 0, | |
| # "Rating": 0 | |
| } | |
| try: | |
| new_row["Version_float"] = float(version) | |
| except ValueError: | |
| new_row["Version_float"] = 0.0 | |
| self.app_items[new_iid] = new_row | |
| self.tree.insert("", "end", new_iid, text=f"{new_row['App']} ({new_row['Version']})", | |
| values=(new_row["Version"], new_row.get("LunchCount", 0))) | |
| self.tree.selection_set(new_iid) | |
| self.tree.focus(new_iid) | |
| self.log_message(f"App '{file_name}' added with version {version}.") | |
| self.mark_unsaved() | |
| def add_new_version(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| messagebox.showwarning("Add New Version", "No app selected.") | |
| return | |
| current_iid = selection[0] | |
| current_item = self.app_items[current_iid] | |
| file_path = filedialog.askopenfilename(title=f"Select file for new version of '{current_item['App']}'") | |
| if not file_path: | |
| return | |
| file_name = os.path.basename(file_path) | |
| file_dir = os.path.dirname(file_path) | |
| version = simpledialog.askstring("New Version", f"Enter version number for the new version of '{current_item['App']}':") | |
| if version is None or version.strip() == "": | |
| messagebox.showwarning("New Version", "Version number cannot be empty.") | |
| return | |
| group = [item for item in self.app_items.values() if item["App"] == current_item["App"] and item["Parent"] == current_item["Parent"]] | |
| if any(item for item in group if item.get("Version") == version): | |
| messagebox.showerror("New Version", f"Version {version} for '{current_item['App']}' already exists.") | |
| return | |
| new_iid = str(self.next_iid) | |
| self.next_iid += 1 | |
| new_row = { | |
| "iid": new_iid, | |
| "App": current_item["App"], | |
| "Path": file_dir, | |
| "Filename": file_name, | |
| "Version": version, | |
| "Description": "", | |
| "Parent": current_item["Parent"], | |
| "LunchCount": 0, | |
| # "Rating": 0 | |
| } | |
| try: | |
| new_row["Version_float"] = float(version) | |
| except ValueError: | |
| new_row["Version_float"] = 0.0 | |
| self.app_items[new_iid] = new_row | |
| parent = new_row["Parent"] | |
| self.tree.insert(parent, "end", new_iid, text=f"{new_row['App']} ({new_row['Version']})", | |
| values=(new_row["Version"], new_row.get("LunchCount", 0))) | |
| self.log_message(f"New version {version} added for app '{current_item['App']}'.") | |
| self.mark_unsaved() | |
| def remove_entry(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| messagebox.showwarning("Remove", "No app selected to remove.") | |
| return | |
| iid = selection[0] | |
| app_name = self.app_items[iid]["App"] | |
| children = self.tree.get_children(iid) | |
| if children: | |
| confirm = messagebox.askyesno("Confirm Remove", | |
| f"'{app_name}' contains nested items. Do you want to remove it and promote its children?") | |
| if not confirm: | |
| return | |
| parent = self.tree.parent(iid) | |
| for child in children: | |
| self.tree.move(child, parent, "end") | |
| self.app_items[child]["Parent"] = parent | |
| self.log_message(f"Promoted child '{self.app_items[child]['App']}' to parent level.") | |
| else: | |
| confirm = messagebox.askyesno("Confirm Remove", f"Are you sure you want to remove '{app_name}'?") | |
| if not confirm: | |
| return | |
| self.tree.delete(iid) | |
| if iid in self.app_items: | |
| del self.app_items[iid] | |
| for field in self.field_names: | |
| self.field_vars[field].set("") | |
| self.desc_text.delete("1.0", tk.END) | |
| self.version_combobox.set("") | |
| self.Lunch_count_label.config(text="") | |
| self.log_message(f"App '{app_name}' removed from list.") | |
| self.mark_unsaved() | |
| def edit_entry(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| messagebox.showwarning("Edit", "No app selected to edit.") | |
| return | |
| iid = selection[0] | |
| item = self.app_items[iid] | |
| edit_win = tk.Toplevel(self.master) | |
| edit_win.title(f"Edit '{item['App']}'") | |
| labels = ["App", "Path", "Filename", "Version", "Description"] | |
| entries = {} | |
| for i, field in enumerate(labels): | |
| tk.Label(edit_win, text=field + ":").grid(row=i, column=0, sticky="w", padx=5, pady=2) | |
| if field != "Description": | |
| var = tk.StringVar(value=item.get(field, "")) | |
| ent = tk.Entry(edit_win, textvariable=var, width=40) | |
| ent.grid(row=i, column=1, padx=5, pady=2) | |
| entries[field] = var | |
| else: | |
| txt = tk.Text(edit_win, height=6, width=36, wrap="word") | |
| txt.grid(row=i, column=1, padx=5, pady=2) | |
| txt.insert(tk.END, item.get("Description", "")) | |
| entries[field] = txt | |
| def save_changes(): | |
| for field in ["App", "Path", "Filename", "Version"]: | |
| item[field] = entries[field].get() | |
| try: | |
| item["Version_float"] = float(item["Version"]) | |
| except ValueError: | |
| item["Version_float"] = 0.0 | |
| item["Description"] = entries["Description"].get("1.0", tk.END).strip() | |
| self.app_items[iid] = item | |
| self.tree.item(iid, text=f"{item['App']} ({item['Version']})") | |
| self.log_message(f"App '{item['App']}' updated. Version set to {item['Version']}.") | |
| edit_win.destroy() | |
| messagebox.showinfo("Saved", "Changes have been saved.") | |
| self.mark_unsaved() | |
| save_button = tk.Button(edit_win, text="Save", command=save_changes) | |
| save_button.grid(row=len(labels), column=0, columnspan=2, pady=5) | |
| def Lunch_app(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| messagebox.showwarning("Lunch", "No app selected to Lunch.") | |
| return | |
| iid = selection[0] | |
| item = self.app_items[iid] | |
| path = item.get("Path", "") | |
| filename = item.get("Filename", "") | |
| full_path = os.path.join(path, filename) | |
| if not os.path.exists(full_path): | |
| messagebox.showerror("Lunch Error", f"File not found: {full_path}") | |
| return | |
| try: | |
| if platform.system() == 'Windows': | |
| os.startfile(full_path) | |
| elif platform.system() == 'Darwin': | |
| subprocess.call(['open', full_path]) | |
| else: | |
| subprocess.call(['xdg-open', full_path]) | |
| self.log_message(f"Lunched file: {full_path}") | |
| item["LunchCount"] = item.get("LunchCount", 0) + 1 | |
| self.display_app_info(iid) | |
| # Autosave after launching | |
| self.save_db() | |
| self.unsaved = False | |
| # self.save_button.config(bg="#107c10") | |
| except Exception as e: | |
| messagebox.showerror("Lunch Error", f"Failed to lunch file: {e}") | |
| def backup_files(self): | |
| backup_zip = "appLunchBackups.zip" | |
| unique_files = set() | |
| for item in self.app_items.values(): | |
| file_path = os.path.join(item.get("Path", ""), item.get("Filename", "")) | |
| if os.path.exists(file_path): | |
| unique_files.add(file_path) | |
| try: | |
| with zipfile.ZipFile(backup_zip, "w", zipfile.ZIP_DEFLATED) as zipf: | |
| for file in unique_files: | |
| arcname = os.path.basename(file) | |
| zipf.write(file, arcname=arcname) | |
| self.log_message(f"Backup created successfully to {backup_zip}.") | |
| messagebox.showinfo("Backup", f"Backup created successfully to {backup_zip}.") | |
| except Exception as e: | |
| messagebox.showerror("Backup Error", f"Failed to create backup: {e}") | |
| self.log_message(f"Backup failed: {e}") | |
| def move_item_up(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| index = siblings.index(iid) | |
| if index > 0: | |
| self.record_move(iid) | |
| self.tree.move(iid, parent, index-1) | |
| self.tree.selection_set(iid) | |
| self.mark_unsaved() | |
| def move_item_down(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| index = siblings.index(iid) | |
| if index < len(siblings) - 1: | |
| self.record_move(iid) | |
| self.tree.move(iid, parent, index+1) | |
| self.tree.selection_set(iid) | |
| self.mark_unsaved() | |
| def move_item_skip_up(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| self.record_move(iid) | |
| self.tree.move(iid, parent, 0) | |
| self.tree.selection_set(iid) | |
| self.mark_unsaved() | |
| def move_item_skip_down(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| self.record_move(iid) | |
| self.tree.move(iid, parent, len(siblings)-1) | |
| self.tree.selection_set(iid) | |
| self.mark_unsaved() | |
| def indent_item(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| index = siblings.index(iid) | |
| if index > 0: | |
| new_parent = siblings[index-1] | |
| self.record_move(iid) | |
| self.tree.move(iid, new_parent, "end") | |
| self.app_items[iid]["Parent"] = new_parent | |
| self.tree.item(new_parent, open=True) | |
| self.tree.selection_set(iid) | |
| self.log_message(f"Indented item '{self.app_items[iid]['App']}' under '{self.app_items[new_parent]['App']}'") | |
| self.mark_unsaved() | |
| def outdent_item(self): | |
| selection = self.tree.selection() | |
| if not selection: | |
| return | |
| iid = selection[0] | |
| parent = self.tree.parent(iid) | |
| if parent: | |
| grandparent = self.tree.parent(parent) | |
| siblings = list(self.tree.get_children(grandparent)) | |
| index_parent = siblings.index(parent) | |
| self.record_move(iid) | |
| self.tree.move(iid, grandparent, index_parent+1) | |
| self.app_items[iid]["Parent"] = grandparent | |
| self.tree.selection_set(iid) | |
| self.log_message(f"Outdented item '{self.app_items[iid]['App']}' to level of its former parent") | |
| self.mark_unsaved() | |
| def on_treeview_button_press(self, event): | |
| pass | |
| def on_treeview_motion(self, event): | |
| pass | |
| def on_treeview_button_release(self, event): | |
| pass | |
| def record_move(self, iid): | |
| parent = self.tree.parent(iid) | |
| siblings = list(self.tree.get_children(parent)) | |
| try: | |
| old_index = siblings.index(iid) | |
| except ValueError: | |
| old_index = 0 | |
| self.undo_stack.append((iid, parent, old_index)) | |
| if len(self.undo_stack) > 10: | |
| self.undo_stack.pop(0) | |
| def mark_unsaved(self): | |
| self.unsaved = True | |
| # self.save_button.config(fg="blue") | |
| def manual_save(self): | |
| self.save_db() | |
| self.unsaved = False | |
| # self.save_button.config(fg="green") | |
| self.log_message("Changes saved.") | |
| def get_default_db_path(self): | |
| try: | |
| script_file = __file__ | |
| except NameError: | |
| script_file = sys.argv[0] | |
| script_file = os.path.abspath(script_file) | |
| base, _ = os.path.splitext(script_file) | |
| return base + ".db" | |
| def save_db(self): | |
| c = self.conn.cursor() | |
| c.execute("DELETE FROM app_items") | |
| for item in self.app_items.values(): | |
| c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", | |
| (item["iid"], item["App"], item["Path"], item["Filename"], item["Version"], | |
| item["Description"], item["Parent"], item["LunchCount"], item.get("Version_float", 0.0), item.get("Rating", 0))) | |
| for key, value in self.layout_config.items(): | |
| c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key, value)) | |
| self.conn.commit() | |
| self.log_message(f"Database saved to {self.db_path}.") | |
| def load_db(self): | |
| c = self.conn.cursor() | |
| c.execute("SELECT * FROM app_items") | |
| rows = c.fetchall() | |
| self.app_items = {} | |
| self.next_iid = 1 | |
| for row in rows: | |
| item = dict(row) | |
| self.app_items[str(item["iid"])] = item | |
| try: | |
| num = int(item["iid"]) | |
| if num >= self.next_iid: | |
| self.next_iid = num + 1 | |
| except: | |
| pass | |
| c.execute("SELECT key, value FROM config") | |
| rows = c.fetchall() | |
| self.layout_config = {row["key"]: row["value"] for row in rows} | |
| self.conn.commit() | |
| def create_tables(self): | |
| c = self.conn.cursor() | |
| c.execute("""CREATE TABLE IF NOT EXISTS app_items ( | |
| iid INTEGER PRIMARY KEY AUTOINCREMENT, | |
| App TEXT, | |
| Path TEXT, | |
| Filename TEXT, | |
| Version TEXT, | |
| Description TEXT, | |
| Parent TEXT, | |
| LunchCount INTEGER, | |
| Version_float REAL, | |
| Rating INTEGER DEFAULT 0 | |
| )""") | |
| c.execute("""CREATE TABLE IF NOT EXISTS config ( | |
| key TEXT PRIMARY KEY, | |
| value TEXT | |
| )""") | |
| self.conn.commit() | |
| # Add Rating column to existing databases if it doesn't exist | |
| try: | |
| c.execute("SELECT Rating FROM app_items LIMIT 1") | |
| except sqlite3.OperationalError: | |
| c.execute("ALTER TABLE app_items ADD COLUMN Rating INTEGER DEFAULT 0") | |
| self.conn.commit() | |
| def import_csv_to_db(self, csv_file): | |
| c = self.conn.cursor() | |
| with open(csv_file, newline='', encoding="utf-8") as f: | |
| reader = csv.DictReader(f) | |
| for row in reader: | |
| if row.get("iid", "") == "__CONFIG__": | |
| config_str = row.get("Description", "") | |
| for setting in config_str.split(";"): | |
| if "=" in setting: | |
| key, value = setting.split("=", 1) | |
| c.execute("INSERT OR REPLACE INTO config (key, value) VALUES (?, ?)", (key.strip(), value.strip())) | |
| else: | |
| try: | |
| lunch = int(row.get("LunchCount", 0)) | |
| except: | |
| lunch = 0 | |
| try: | |
| vf = float(row.get("Version", "0")) | |
| except: | |
| vf = 0.0 | |
| try: | |
| rating = int(row.get("Rating", 0)) | |
| if rating < 0 or rating > 5: | |
| rating = 0 | |
| except: | |
| rating = 0 | |
| c.execute("""INSERT INTO app_items (iid, App, Path, Filename, Version, Description, Parent, LunchCount, Version_float, Rating) | |
| VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", | |
| (row.get("iid"), row.get("App"), row.get("Path"), row.get("Filename"), | |
| row.get("Version"), row.get("Description"), row.get("Parent", ""), lunch, vf, rating)) | |
| self.conn.commit() | |
| def on_closing(self): | |
| # For demonstration, we simulate layout config defaults. | |
| default_vertical = (0, 400) | |
| default_top = (320, 0) | |
| default_bottom = (0, 100) | |
| self.layout_config["vertical_sash"] = f"{default_vertical[0]},{default_vertical[1]}" | |
| self.layout_config["top_sash"] = f"{default_top[0]},{default_top[1]}" | |
| self.layout_config["bottom_sash"] = f"{default_bottom[0]},{default_bottom[1]}" | |
| self.save_db() | |
| self.conn.close() | |
| self.master.destroy() | |
| if __name__ == "__main__": | |
| root = tk.Tk() | |
| # Check if DB exists; if not, later the app will prompt for CSV import. | |
| app = CustomAppLunch(root) | |
| root.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment