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("", 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()