Files

326 lines
16 KiB
Python
Raw Permalink Normal View History

2026-05-31 18:33:31 +08:00
import customtkinter as ctk
import tkinter as tk
from tkinter import filedialog, messagebox
import subprocess
import json
import os
import threading
import webbrowser
# Настройки стиля под Windows 11
ctk.set_appearance_mode("System")
ctk.set_default_color_theme("blue")
class NpmManagerPro(ctk.CTk):
def __init__(self):
super().__init__()
self.title("Windows 11 NPM Package Manager Pro")
self.geometry("1100x750")
# Состояние приложения
self.current_mode = "global" # "global" или "local"
self.current_path = None # Путь к локальному проекту
self.checkbox_vars = {} # Состояния чекбоксов
self.setup_ui()
self.fetch_packages()
def setup_ui(self):
# --- ВЕРХНЯЯ ПАНЕЛЬ: Режимы и Установка ---
self.header_frame = ctk.CTkFrame(self)
self.header_frame.pack(fill="x", padx=10, pady=(10, 5))
# Заголовок
self.title_label = ctk.CTkLabel(self.header_frame, text="🌍 Глобальные пакеты", font=("Segoe UI", 20, "bold"))
self.title_label.pack(side="left", padx=10, pady=10)
# Кнопка смены режима
self.switch_mode_btn = ctk.CTkButton(self.header_frame, text="📁 Выбрать локальный проект",
fg_color="#5bc0de", hover_color="#31b0d5", text_color="black",
command=self.switch_to_local)
self.switch_mode_btn.pack(side="left", padx=10)
self.global_mode_btn = ctk.CTkButton(self.header_frame, text="🌍 Глобальный режим",
command=self.switch_to_global, state="disabled")
self.global_mode_btn.pack(side="left", padx=5)
# Установка новых пакетов
self.install_btn = ctk.CTkButton(self.header_frame, text="Установить", width=100, command=self.install_pkg)
self.install_btn.pack(side="right", padx=10)
self.install_entry = ctk.CTkEntry(self.header_frame, placeholder_text="Имя пакета (напр. lodash)", width=200)
self.install_entry.pack(side="right", padx=5)
# --- ПАНЕЛЬ МАССОВЫХ ДЕЙСТВИЙ ---
self.batch_frame = ctk.CTkFrame(self, fg_color="transparent")
self.batch_frame.pack(fill="x", padx=10, pady=5)
self.select_all_var = ctk.BooleanVar(value=False)
self.select_all_cb = ctk.CTkCheckBox(self.batch_frame, text="Выбрать все", variable=self.select_all_var, command=self.toggle_select_all)
self.select_all_cb.pack(side="left", padx=10)
self.batch_update_btn = ctk.CTkButton(self.batch_frame, text="Обновить выбранные", width=140, fg_color="#28a745", hover_color="#218838", command=self.batch_update)
self.batch_update_btn.pack(side="left", padx=5)
self.batch_delete_btn = ctk.CTkButton(self.batch_frame, text="Удалить выбранные", width=140, fg_color="#D9534F", hover_color="#C9302C", command=self.batch_delete)
self.batch_delete_btn.pack(side="left", padx=5)
self.refresh_btn = ctk.CTkButton(self.batch_frame, text="🔄 Обновить список", width=140, command=self.fetch_packages)
self.refresh_btn.pack(side="right", padx=10)
# --- ОСНОВНОЙ СПИСОК ПАКЕТОВ ---
self.scroll_frame = ctk.CTkScrollableFrame(self)
self.scroll_frame.pack(fill="both", expand=True, padx=10, pady=5)
# --- ТЕРМИНАЛ / ЛОГИ ---
self.log_box = ctk.CTkTextbox(self, height=130, font=("Consolas", 12))
self.log_box.pack(fill="x", padx=10, pady=(5, 10))
self.log("Приложение запущено. Готово к работе.")
# --- УТИЛИТЫ ---
def log(self, message):
self.log_box.insert("end", message + "\n")
self.log_box.see("end")
def run_command(self, cmd, cwd=None):
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, encoding="utf-8", errors="ignore", cwd=cwd)
return result.stdout, result.stderr, result.returncode
def extract_json(self, text):
try:
start = text.find('{')
end = text.rfind('}') + 1
if start != -1 and end != 0:
return json.loads(text[start:end])
except Exception:
pass
return {}
# --- ПЕРЕКЛЮЧЕНИЕ РЕЖИМОВ ---
def switch_to_local(self):
folder = filedialog.askdirectory(title="Выберите папку проекта с package.json")
if folder:
if not os.path.exists(os.path.join(folder, "package.json")):
messagebox.showwarning("Внимание", "В выбранной папке нет файла package.json!")
return
self.current_mode = "local"
self.current_path = folder
self.title_label.configure(text=f"📁 Локальные: {os.path.basename(folder)}")
self.global_mode_btn.configure(state="normal")
self.fetch_packages()
def switch_to_global(self):
self.current_mode = "global"
self.current_path = None
self.title_label.configure(text="🌍 Глобальные пакеты")
self.global_mode_btn.configure(state="disabled")
self.fetch_packages()
# --- ЗАГРУЗКА ДАННЫХ ---
def fetch_packages(self):
self.refresh_btn.configure(state="disabled")
self.select_all_var.set(False)
for widget in self.scroll_frame.winfo_children():
widget.destroy()
self.log("Поиск пакетов и проверка обновлений (это может занять несколько секунд)...")
threading.Thread(target=self._fetch_thread, daemon=True).start()
def _fetch_thread(self):
cwd = self.current_path if self.current_mode == "local" else None
g_flag = "" if self.current_mode == "local" else "-g"
# 1. Получаем точный абсолютный путь до папки node_modules через 'npm root'
out_root, _, _ = self.run_command(f"npm root {g_flag}", cwd)
node_modules_path = out_root.strip()
# Получаем установленные пакеты
out_ls, err_ls, _ = self.run_command(f"npm ls {g_flag} --depth=0 --json", cwd)
ls_data = self.extract_json(out_ls)
# Получаем устаревшие пакеты
out_out, err_out, _ = self.run_command(f"npm outdated {g_flag} --json", cwd)
outdated_data = self.extract_json(out_out)
self.after(0, lambda: self.populate_list(ls_data, outdated_data, node_modules_path))
def populate_list(self, ls_data, outdated_data, node_modules_path):
deps = ls_data.get("dependencies", {})
self.checkbox_vars.clear()
if not deps:
lbl = ctk.CTkLabel(self.scroll_frame, text="Пакеты не найдены.", font=("Segoe UI", 16))
lbl.pack(pady=20)
self.log("Пакеты не найдены.")
else:
self.log(f"Отображено пакетов: {len(deps)}")
for pkg, info in deps.items():
current_version = info.get("version", "неизвестно")
# 2. Формируем правильный абсолютный путь для Windows
pkg_path = os.path.normpath(os.path.join(node_modules_path, pkg))
# Проверка на наличие обновлений
is_outdated = pkg in outdated_data
latest_version = outdated_data[pkg].get("latest", "") if is_outdated else ""
row = ctk.CTkFrame(self.scroll_frame)
row.pack(fill="x", pady=5, padx=5)
# Чекбокс
var = ctk.BooleanVar(value=False)
self.checkbox_vars[pkg] = var
cb = ctk.CTkCheckBox(row, text="", variable=var, width=20)
cb.pack(side="left", padx=(10, 5))
# Кликабельное название пакета (Попап с инфо)
name_btn = ctk.CTkButton(row, text=pkg, width=150, anchor="w", fg_color="transparent",
text_color=("black", "white"), hover_color=("#e0e0e0", "#2a2d2e"),
font=("Segoe UI", 14, "bold"), command=lambda p=pkg: self.show_pkg_info(p))
name_btn.pack(side="left", padx=5)
# Версия (с подсветкой если устарела)
if is_outdated:
v_text = f"{current_version}{latest_version}"
v_color = "#ff9900" # Оранжевый
else:
v_text = f"v{current_version}"
v_color = "gray"
version_lbl = ctk.CTkLabel(row, text=v_text, text_color=v_color, font=("Segoe UI", 13, "bold" if is_outdated else "normal"), width=120, anchor="w")
version_lbl.pack(side="left", padx=5)
# Путь и кнопка "Открыть в проводнике"
path_lbl = ctk.CTkLabel(row, text=pkg_path, anchor="w", text_color="gray", font=("Segoe UI", 11))
path_lbl.pack(side="left", padx=5, fill="x", expand=True)
open_folder_btn = ctk.CTkButton(row, text="📂", width=30, fg_color="transparent",
hover_color="#cccccc", text_color=("black", "white"),
command=lambda p=pkg_path: self.open_in_explorer(p))
open_folder_btn.pack(side="left", padx=5)
# Кнопка обновления (зеленая, если есть апдейт)
upd_color = "#28a745" if is_outdated else ("#3b8ed0", "#1f6aa5")
update_btn = ctk.CTkButton(row, text="Обновить", width=80, fg_color=upd_color,
command=lambda p=pkg: self.update_pkg(p))
update_btn.pack(side="left", padx=5)
# Кнопка удаления
delete_btn = ctk.CTkButton(row, text="Удалить", width=80, fg_color="#D9534F", hover_color="#C9302C",
command=lambda p=pkg: self.delete_pkg(p))
delete_btn.pack(side="left", padx=5)
self.refresh_btn.configure(state="normal")
# --- ИНФОРМАЦИЯ О ПАКЕТЕ (ПОПАП) ---
def show_pkg_info(self, pkg):
info_window = ctk.CTkToplevel(self)
info_window.title(f"Информация: {pkg}")
info_window.geometry("450x300")
info_window.transient(self)
loading_lbl = ctk.CTkLabel(info_window, text="Загрузка данных из реестра NPM...", font=("Segoe UI", 14))
loading_lbl.pack(expand=True)
threading.Thread(target=self._fetch_info_thread, args=(pkg, info_window, loading_lbl), daemon=True).start()
def _fetch_info_thread(self, pkg, window, loading_lbl):
out, _, _ = self.run_command(f"npm view {pkg} --json")
info = self.extract_json(out)
self.after(0, lambda: loading_lbl.destroy())
self.after(0, lambda: self._build_info_ui(window, pkg, info))
def _build_info_ui(self, window, pkg, info):
if not info:
ctk.CTkLabel(window, text="Не удалось загрузить информацию.").pack(pady=20)
return
desc = info.get("description", "Описание отсутствует")
author = info.get("author", "Неизвестен")
if isinstance(author, dict): author = author.get("name", "Неизвестен")
license_ = info.get("license", "Не указана")
homepage = info.get("homepage", "")
ctk.CTkLabel(window, text=pkg, font=("Segoe UI", 22, "bold")).pack(pady=(10, 5))
desc_box = ctk.CTkTextbox(window, height=60, wrap="word")
desc_box.pack(fill="x", padx=20, pady=5)
desc_box.insert("1.0", desc)
desc_box.configure(state="disabled")
ctk.CTkLabel(window, text=f"👤 Автор: {author}", font=("Segoe UI", 14)).pack(anchor="w", padx=20, pady=2)
ctk.CTkLabel(window, text=f"📄 Лицензия: {license_}", font=("Segoe UI", 14)).pack(anchor="w", padx=20, pady=2)
if homepage:
link_lbl = ctk.CTkLabel(window, text=f"🌐 Сайт: {homepage}", text_color="#1f538d", cursor="hand2", font=("Segoe UI", 14, "underline"))
link_lbl.pack(anchor="w", padx=20, pady=5)
link_lbl.bind("<Button-1>", lambda e: webbrowser.open(homepage))
# --- ДЕЙСТВИЯ (Открыть папку, Установить) ---
def open_in_explorer(self, path):
if os.path.exists(path):
os.startfile(path)
else:
messagebox.showwarning("Ошибка", f"Папка не найдена:\n{path}")
def install_pkg(self):
pkg = self.install_entry.get().strip()
if not pkg: return
self.log(f"Установка нового пакета '{pkg}'...")
self.install_entry.delete(0, 'end')
threading.Thread(target=self._exec_npm_command, args=("install", [pkg]), daemon=True).start()
# --- МАССОВЫЕ ДЕЙСТВИЯ ---
def toggle_select_all(self):
state = self.select_all_var.get()
for var in self.checkbox_vars.values():
var.set(state)
def get_selected_packages(self):
return[pkg for pkg, var in self.checkbox_vars.items() if var.get()]
def batch_update(self):
selected = self.get_selected_packages()
if not selected: return
self.log(f"Массовое обновление: {', '.join(selected)}")
threading.Thread(target=self._exec_npm_command, args=("update", selected), daemon=True).start()
def batch_delete(self):
selected = self.get_selected_packages()
if not selected: return
confirm = messagebox.askyesno("Подтверждение", f"Удалить {len(selected)} пакетов?")
if confirm:
self.log(f"Массовое удаление: {', '.join(selected)}")
threading.Thread(target=self._exec_npm_command, args=("uninstall", selected), daemon=True).start()
# --- ИНДИВИДУАЛЬНЫЕ ДЕЙСТВИЯ ---
def update_pkg(self, pkg):
self.log(f"Обновление пакета {pkg}...")
threading.Thread(target=self._exec_npm_command, args=("update", [pkg]), daemon=True).start()
def delete_pkg(self, pkg):
confirm = messagebox.askyesno("Подтверждение", f"Вы действительно хотите удалить '{pkg}'?")
if confirm:
self.log(f"Удаление {pkg}...")
threading.Thread(target=self._exec_npm_command, args=("uninstall", [pkg]), daemon=True).start()
# --- ОБЩИЙ ИСПОЛНИТЕЛЬ КОМАНД ---
def _exec_npm_command(self, action, pkgs_list):
cwd = self.current_path if self.current_mode == "local" else None
g_flag = "" if self.current_mode == "local" else "-g"
pkgs_str = " ".join(pkgs_list)
cmd = f"npm {action} {g_flag} {pkgs_str}"
out, err, code = self.run_command(cmd, cwd)
if code == 0:
self.after(0, lambda: self.log(f"✅ Действие '{action}' успешно выполнено для {pkgs_str}."))
else:
self.after(0, lambda: self.log(f"❌ Ошибка при '{action}' {pkgs_str}.\n{err}"))
self.after(0, self.fetch_packages)
if __name__ == "__main__":
app = NpmManagerPro()
app.mainloop()