Universal Git Analyzer v12 final
This commit is contained in:
378
main.py
Normal file
378
main.py
Normal file
@@ -0,0 +1,378 @@
|
||||
import os
|
||||
import subprocess
|
||||
import threading
|
||||
import tkinter as tk
|
||||
from tkinter import messagebox
|
||||
|
||||
from storage import register_user, check_user, get_repositories, add_repository, delete_repository
|
||||
from git_analyzer import analyze_repository, format_analysis
|
||||
|
||||
|
||||
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.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
|
||||
|
||||
# Windows: проигрываем wav без внешних программ
|
||||
if os.name == "nt":
|
||||
try:
|
||||
import winsound
|
||||
winsound.PlaySound(path, winsound.SND_FILENAME | winsound.SND_ASYNC)
|
||||
return
|
||||
except Exception:
|
||||
return
|
||||
|
||||
# Linux: пробуем стандартные проигрыватели звука
|
||||
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")
|
||||
# Чашка Gitea справа
|
||||
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 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 = 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("<<ListboxSelect>>", 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 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 = 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
|
||||
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 = get_repositories(self.current_user)
|
||||
if not repos:
|
||||
messagebox.showinfo("Информация", "Сохранённых репозиториев нет")
|
||||
return
|
||||
self.set_text("Обновление всех репозиториев...\n")
|
||||
def worker():
|
||||
results = []
|
||||
for repo in repos:
|
||||
try:
|
||||
data = analyze_repository(repo)
|
||||
results.append(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 = 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(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()
|
||||
Reference in New Issue
Block a user