import tkinter as tk from tkinter import simpledialog, messagebox from pathlib import Path import urllib.request import shutil import threading class DownloadModelDialog(simpledialog.Dialog): def __init__(self, parent, model_dir: Path): self.model_dir = model_dir self.active_downloads = {} # filename -> {progress, thread, cancel_flag} super().__init__(parent, title="Download Model") def body(self, master): tk.Label(master, text="Model URL:").grid(row=0, column=0, sticky="w") self.url_entry = tk.Entry(master, width=60) self.url_entry.grid(row=0, column=1, padx=6, pady=4) # Browse Hugging Face button browse_btn = tk.Button(master, text="Browse Hugging Face...", command=self.open_hf_browser, bg="#e0e0e0", fg="#111", width=22) browse_btn.grid(row=1, column=0, columnspan=2, sticky="w", padx=(0, 0), pady=(2, 2)) # Comprehensive caption/instruction at the bottom caption_text = ( "Paste a direct download URL for a model file. " "\n\n" "Where to find models:\n" "- Hugging Face: https://huggingface.co (search for GGUF or compatible models)\n" "- Official model provider pages\n" "\n" "The URL should point directly to a downloadable file (e.g., .gguf, .bin).\n" "Example: https://huggingface.co/ORG/MODEL/resolve/main/model-file.gguf\n" "\n" "Downloaded files will be saved to your selected model directory and will appear in the model selection list." ) caption = tk.Label( master, text=caption_text, justify="left", anchor="w", wraplength=440, fg="#fff", bg="#444", font=(None, 10, "italic") ) caption.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(16, 2)) # Download cards frame (below caption) self.cards_frame = tk.Frame(master, bg="#222") self.cards_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(8, 2)) self.refresh_cards() return self.url_entry def open_hf_browser(self): # Popup window with embedded browser to Hugging Face try: from tkhtmlview import HtmlFrame except ImportError: messagebox.showerror("Missing Dependency", "tkhtmlview is required for the integrated browser. Please install it with 'pip install tkhtmlview'.") return popup = tk.Toplevel(self) popup.title("Hugging Face Model Catalogue") popup.geometry("1000x700") # Info label tk.Label(popup, text="Browse models. Right-click a download link and select 'Copy Link', then paste it in the URL field.", bg="#222", fg="#fff").pack(fill="x") # Embedded browser (read-only, not full browser) frame = HtmlFrame(popup, horizontal_scrollbar="auto", vertical_scrollbar="auto") frame.pack(fill="both", expand=True) frame.load_website("https://huggingface.co/models") # Comprehensive caption/instruction at the bottom caption_text = ( "Paste a direct download URL for a model file. " "\n\n" "Where to find models:\n" "- Hugging Face: https://huggingface.co (search for GGUF or compatible models)\n" "- Official model provider pages\n" "\n" "The URL should point directly to a downloadable file (e.g., .gguf, .bin).\n" "Example: https://huggingface.co/ORG/MODEL/resolve/main/model-file.gguf\n" "\n" "Downloaded files will be saved to your selected model directory and will appear in the model selection list." ) caption = tk.Label( master, text=caption_text, justify="left", anchor="w", wraplength=440, fg="#fff", bg="#444", font=(None, 10, "italic") ) caption.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(16, 2)) # Download cards frame (below caption) self.cards_frame = tk.Frame(master, bg="#222") self.cards_frame.grid(row=3, column=0, columnspan=2, sticky="ew", pady=(8, 2)) self.refresh_cards() return self.url_entry def refresh_cards(self): # Clear previous cards for widget in self.cards_frame.winfo_children(): widget.destroy() if not self.active_downloads: placeholder = tk.Label( self.cards_frame, text="No downloads in progress.", bg="#222", fg="#aaa", font=(None, 10, "italic") ) placeholder.pack(fill="x", pady=12) else: row = 0 for fname, info in self.active_downloads.items(): card = tk.Frame(self.cards_frame, bg="#333", bd=2, relief="groove") card.grid(row=row, column=0, sticky="ew", pady=4, padx=2) # File name tk.Label(card, text=fname, bg="#333", fg="#fff", font=(None, 10, "bold")).pack(anchor="w", padx=8, pady=(4,0)) bar_frame = tk.Frame(card, bg="#333") bar_frame.pack(fill="x", padx=8, pady=(2,6)) # Percentage label percent_var = info.get('percent_var') if not percent_var: percent_var = tk.StringVar(value="0%") info['percent_var'] = percent_var percent_label = tk.Label(bar_frame, textvariable=percent_var, width=5, anchor="e", bg="#333", fg="#fff") percent_label.pack(side="left") # Progress bar progress_var = info.get('progress_var') if not progress_var: progress_var = tk.DoubleVar(value=0) info['progress_var'] = progress_var bar = tk.ttk.Progressbar(bar_frame, variable=progress_var, maximum=100, length=220) bar.pack(side="left", fill="x", expand=True, padx=(6,6)) # Cancel button cancel_btn = tk.Button(bar_frame, text="Cancel", command=lambda f=fname: self.cancel_download(f), bg="#a33", fg="#fff") cancel_btn.pack(side="right", padx=(6,0)) row += 1 def apply(self): url = self.url_entry.get().strip() if not url: messagebox.showerror("Error", "Model URL is required.") return filename = url.split("/")[-1] dest_path = self.model_dir / filename cancel_flag = threading.Event() info = { 'progress_var': tk.DoubleVar(value=0), 'percent_var': tk.StringVar(value="0%"), 'cancel_flag': cancel_flag, } self.active_downloads[filename] = info self.refresh_cards() def download(): try: with urllib.request.urlopen(url) as response, open(dest_path, 'wb') as out_file: total = int(response.getheader('Content-Length', 0)) downloaded = 0 chunk = 8192 while True: if cancel_flag.is_set(): break data = response.read(chunk) if not data: break out_file.write(data) downloaded += len(data) percent = int((downloaded / total) * 100) if total else 0 info['progress_var'].set(percent) info['percent_var'].set(f"{percent}%") self.cards_frame.after(50, self.refresh_cards) if not cancel_flag.is_set(): messagebox.showinfo("Success", f"Model downloaded to {dest_path}") except Exception as e: if not cancel_flag.is_set(): messagebox.showerror("Download Failed", str(e)) finally: del self.active_downloads[filename] self.cards_frame.after(100, self.refresh_cards) t = threading.Thread(target=download, daemon=True) info['thread'] = t t.start() def cancel_download(self, filename): info = self.active_downloads.get(filename) if info: info['cancel_flag'].set() del self.active_downloads[filename] self.refresh_cards()