import os import subprocess import threading import tkinter as tk from tkinter import messagebox from storage import StorageManager from git_analyzer import GitAnalyzer class UniversalGitAnalyzerApp: def __init__(self): self.root = tk.Tk() self.root.title("Universal Git Analyzer") self.root.geometry("1920x1080+0+0") self.root.minsize(1200, 720) self.root.resizable(True, True) try: self.root.attributes("-zoomed", True) except Exception: pass self.storage = StorageManager() self.git_analyzer = GitAnalyzer() self.current_user = None self.current_repo = None self.current_index = None self.current_data = None self.bg = "#eef4ff" self.panel = "#ffffff" self.button = "#6aa6ff" self.logo_image = None self.loading_canvas = None self.loading_cat = None self.loading_step = 0 self.root.configure(bg=self.bg) self.show_splash() self.root.mainloop() def clear(self): for widget in self.root.winfo_children(): widget.destroy() def resource_path(self, relative_path): base_path = getattr(__import__("sys"), "_MEIPASS", os.path.abspath(os.path.dirname(__file__))) return os.path.join(base_path, relative_path) def play_sound(self, filename): path = self.resource_path(os.path.join("assets", filename)) if not os.path.exists(path): return if os.name == "nt": try: import winsound winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC) return except Exception: return for player in ("paplay", "aplay"): try: subprocess.Popen([player, path], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) return except FileNotFoundError: continue except Exception: break def play_startup_sound(self): self.play_sound("startup.wav") def make_button(self, parent, text, command, width=22): return tk.Button( parent, text=text, command=command, width=width, bg=self.button, fg="white", activebackground="#4d8ee8", font=("Arial", 10, "bold"), relief="flat", cursor="hand2", pady=6, wraplength=360, ) def show_splash(self): self.clear() self.play_startup_sound() frame = tk.Frame(self.root, bg="#07101f") frame.pack(fill="both", expand=True) logo_path = self.resource_path(os.path.join("assets", "logo.png")) if os.path.exists(logo_path): try: self.logo_image = tk.PhotoImage(file=logo_path) if self.logo_image.width() > 560: factor = max(1, (self.logo_image.width() + 519) // 520) self.logo_image = self.logo_image.subsample(factor, factor) tk.Label(frame, image=self.logo_image, bg="#07101f").pack(pady=(25, 8)) except Exception: tk.Label(frame, text="Universal Git Analyzer", font=("Arial", 30, "bold"), bg="#07101f", fg="white").pack(pady=80) else: tk.Label(frame, text="Universal Git Analyzer", font=("Arial", 30, "bold"), bg="#07101f", fg="white").pack(pady=80) tk.Label(frame, text="Загрузка проекта...", font=("Arial", 13, "bold"), bg="#07101f", fg="#dce8ff").pack(pady=5) self.loading_canvas = tk.Canvas(frame, width=640, height=100, bg="#07101f", highlightthickness=0) self.loading_canvas.pack(pady=(45, 0)) self.draw_loading_bar() self.animate_loading_cat() self.root.after(4500, self.show_login) def draw_loading_bar(self): if not self.loading_canvas: return c = self.loading_canvas c.delete("all") c.create_line(70, 58, 545, 58, fill="#2a3e63", width=6, capstyle="round") c.create_line(70, 58, 545, 58, fill="#4e8cff", width=2, capstyle="round") c.create_oval(545, 30, 595, 80, fill="#24551f", outline="#7dd36f", width=2) c.create_arc(585, 42, 620, 70, start=-70, extent=140, outline="#7dd36f", width=4, style="arc") c.create_rectangle(559, 45, 582, 69, fill="#24551f", outline="#24551f") c.create_text(570, 57, text="☕", fill="#e8ffe4", font=("Arial", 18, "bold")) c.create_text(570, 88, text="Gitea", fill="#9dc8ff", font=("Arial", 9, "bold")) def animate_loading_cat(self): if not self.loading_canvas: return c = self.loading_canvas c.delete("cat") steps = 90 x = 70 + int((455 * self.loading_step) / steps) y = 58 c.create_oval(x - 18, y - 22, x + 18, y + 14, fill="#02050a", outline="white", width=2, tags="cat") c.create_polygon(x - 16, y - 16, x - 7, y - 34, x + 0, y - 16, fill="#02050a", outline="white", width=2, tags="cat") c.create_polygon(x + 4, y - 16, x + 13, y - 34, x + 20, y - 16, fill="#02050a", outline="white", width=2, tags="cat") c.create_line(x - 18, y - 1, x - 36, y - 8, fill="white", width=2, tags="cat") c.create_line(x - 18, y + 5, x - 38, y + 5, fill="white", width=2, tags="cat") c.create_line(x + 18, y - 1, x + 36, y - 8, fill="white", width=2, tags="cat") c.create_line(x + 18, y + 5, x + 38, y + 5, fill="white", width=2, tags="cat") c.create_arc(x - 36, y - 2, x - 5, y + 35, start=190, extent=170, outline="white", width=3, style="arc", tags="cat") c.create_text(x, y + 31, text="🐾", fill="#dce8ff", font=("Arial", 12), tags="cat") self.loading_step += 1 if self.loading_step <= steps: self.root.after(45, self.animate_loading_cat) def show_login(self): self.clear() frame = tk.Frame(self.root, bg=self.panel, padx=35, pady=35) frame.place(relx=0.5, rely=0.5, anchor="center") tk.Label(frame, text="Вход в аккаунт", font=("Arial", 22, "bold"), bg=self.panel).pack(pady=10) tk.Label(frame, text="Логин", bg=self.panel, font=("Arial", 11)).pack(anchor="w") login_entry = tk.Entry(frame, width=35, font=("Arial", 12)) login_entry.pack(pady=5) tk.Label(frame, text="Пароль", bg=self.panel, font=("Arial", 11)).pack(anchor="w") password_entry = tk.Entry(frame, width=35, font=("Arial", 12), show="*") password_entry.pack(pady=5) def login(): login_value = login_entry.get().strip() password_value = password_entry.get().strip() if not login_value or not password_value: messagebox.showerror("Ошибка", "Введите логин и пароль") return if self.storage.check_user(login_value, password_value): self.current_user = login_value self.show_main_screen() else: messagebox.showerror("Ошибка", "Неверный логин или пароль") self.make_button(frame, "Войти", login).pack(pady=12) self.make_button(frame, "Создать профиль", self.show_register).pack(pady=5) def show_register(self): self.clear() frame = tk.Frame(self.root, bg=self.panel, padx=35, pady=35) frame.place(relx=0.5, rely=0.5, anchor="center") tk.Label(frame, text="Создание профиля", font=("Arial", 22, "bold"), bg=self.panel).pack(pady=10) tk.Label(frame, text="Логин", bg=self.panel, font=("Arial", 11)).pack(anchor="w") login_entry = tk.Entry(frame, width=35, font=("Arial", 12)) login_entry.pack(pady=5) tk.Label(frame, text="Пароль", bg=self.panel, font=("Arial", 11)).pack(anchor="w") password_entry = tk.Entry(frame, width=35, font=("Arial", 12), show="*") password_entry.pack(pady=5) def register(): login_value = login_entry.get().strip() password_value = password_entry.get().strip() if not login_value or not password_value: messagebox.showerror("Ошибка", "Введите логин и пароль") return if len(password_value) < 4: messagebox.showerror("Ошибка", "Пароль должен быть минимум 4 символа") return ok, text = self.storage.register_user(login_value, password_value) if ok: messagebox.showinfo("Готово", text) self.show_login() else: messagebox.showerror("Ошибка", text) self.make_button(frame, "Зарегистрироваться", register).pack(pady=12) self.make_button(frame, "Назад", self.show_login).pack(pady=5) def show_main_screen(self): self.clear() top = tk.Frame(self.root, bg="#1d3557", height=55) top.pack(fill="x") tk.Label(top, text=f"Universal Git Analyzer | Пользователь: {self.current_user}", bg="#1d3557", fg="white", font=("Arial", 16, "bold")).pack(side="left", padx=15, pady=12) tk.Button(top, text="Выйти", command=self.show_login, bg="#e63946", fg="white", relief="flat", font=("Arial", 11)).pack(side="right", padx=15) content = tk.Frame(self.root, bg=self.bg) content.pack(fill="both", expand=True, padx=10, pady=10) left = tk.Frame(content, bg=self.panel, padx=12, pady=12, width=520) left.pack(side="left", fill="y") left.pack_propagate(False) right = tk.Frame(content, bg=self.panel, padx=10, pady=10) right.pack(side="right", fill="both", expand=True, padx=(10, 0)) tk.Label(left, text="Мои репозитории", bg=self.panel, font=("Arial", 20, "bold")).pack(anchor="w") self.repo_listbox = tk.Listbox(left, width=58, height=20, font=("Arial", 11)) self.repo_listbox.pack(fill="x", pady=8) self.repo_listbox.bind("<>", lambda event: self.on_repo_click()) tk.Label(left, text="Новая ссылка", bg=self.panel, font=("Arial", 11)).pack(anchor="w") self.repo_entry = tk.Entry(left, font=("Arial", 11)) self.repo_entry.pack(fill="x", pady=5) self.make_button(left, "Сохранить ссылку", self.save_repo, width=42).pack(fill="x", pady=4) self.make_button(left, "Обновить выбранный репозиторий", self.refresh_selected, width=42).pack(fill="x", pady=4) self.make_button(left, "Обновить все репозитории", self.refresh_all, width=42).pack(fill="x", pady=4) self.make_button(left, "Удалить ссылку", self.remove_repo, width=42).pack(fill="x", pady=4) text_frame = tk.Frame(right, bg=self.panel) text_frame.pack(fill="both", expand=True) self.result_text = tk.Text(text_frame, font=("Consolas", 11), wrap="word") self.result_text.pack(side="left", fill="both", expand=True) scrollbar = tk.Scrollbar(text_frame, command=self.result_text.yview) scrollbar.pack(side="right", fill="y") self.result_text.config(yscrollcommand=scrollbar.set) self.result_text.config(state="disabled") nav = tk.Frame(right, bg=self.panel) nav.pack(fill="x", pady=(6, 0)) self.prev_button = self.make_button(nav, "<-", self.prev_commit, width=8) self.prev_button.pack(side="left") self.version_label = tk.Label(nav, text="Версия: —", bg=self.panel, font=("Arial", 11, "bold")) self.version_label.pack(side="left", padx=12) self.next_button = self.make_button(nav, "->", self.next_commit, width=8) self.next_button.pack(side="left") self.refresh_list() def set_text(self, text): self.result_text.config(state="normal") self.result_text.delete("1.0", tk.END) self.result_text.insert(tk.END, text) self.result_text.config(state="disabled") def refresh_list(self): self.repo_listbox.delete(0, tk.END) for repo in self.storage.get_repositories(self.current_user): self.repo_listbox.insert(tk.END, repo) def get_selected_repo(self): selection = self.repo_listbox.curselection() if not selection: return None return self.repo_listbox.get(selection[0]) def save_repo(self): link = self.repo_entry.get().strip() if not link: messagebox.showerror("Ошибка", "Введите ссылку") return added = self.storage.add_repository(self.current_user, link) if added: self.repo_entry.delete(0, tk.END) self.refresh_list() messagebox.showinfo("Готово", "Ссылка сохранена") else: messagebox.showinfo("Информация", "Эта ссылка уже сохранена") def remove_repo(self): link = self.get_selected_repo() if not link: messagebox.showerror("Ошибка", "Выберите репозиторий") return self.storage.delete_repository(self.current_user, link) self.refresh_list() self.current_repo = None self.current_index = None self.current_data = None self.set_text("") self.version_label.config(text="Версия: —") def on_repo_click(self): link = self.get_selected_repo() if link: self.load_repo(link, selected_index=None) def refresh_selected(self): link = self.get_selected_repo() or self.current_repo if not link: messagebox.showerror("Ошибка", "Выберите репозиторий") return self.load_repo(link, self.current_index) def refresh_all(self): repos = self.storage.get_repositories(self.current_user) if not repos: messagebox.showinfo("Информация", "Сохранённых репозиториев нет") return self.set_text("Обновление всех репозиториев...\n") def worker(): results = [] for repo in repos: try: data = self.git_analyzer.analyze_repository(repo) results.append(self.git_analyzer.format_analysis(data)) except Exception as e: results.append(f"Ссылка: {repo}\nОшибка: {e}") self.root.after(0, lambda: self.set_text("\n\n" + "=" * 70 + "\n\n".join(results))) threading.Thread(target=worker, daemon=True).start() def load_repo(self, link, selected_index=None): self.current_repo = link self.set_text("Загрузка данных...\n") self.version_label.config(text="Версия: загрузка...") def worker(): try: data = self.git_analyzer.analyze_repository(link, selected_index) self.root.after(0, lambda: self.show_analysis(data)) except Exception as e: self.root.after(0, lambda: self.show_error(e)) threading.Thread(target=worker, daemon=True).start() def show_analysis(self, data): self.current_data = data self.current_index = data["selected_index"] self.set_text(self.git_analyzer.format_analysis(data)) commit = data["selected_commit"] self.version_label.config(text=f"Версия: {self.current_index + 1} / {len(data['commits'])} ({commit['short']})") def show_error(self, error): self.set_text("") self.version_label.config(text="Версия: —") messagebox.showerror("Ошибка", str(error)) def prev_commit(self): if not self.current_repo or self.current_index is None: return if self.current_index <= 0: return self.load_repo(self.current_repo, self.current_index - 1) def next_commit(self): if not self.current_repo or self.current_index is None or not self.current_data: return if self.current_index >= len(self.current_data["commits"]) - 1: return self.load_repo(self.current_repo, self.current_index + 1) if __name__ == "__main__": UniversalGitAnalyzerApp()