commit 28f767a1532eb9c18acda9ca0d10f1bde7d0d570 Author: masbudikusuma Date: Mon Sep 14 08:17:50 2026 +0700 Upload files to "/" diff --git a/litemon_v1.exe b/litemon_v1.exe new file mode 100644 index 0000000..c7ce573 Binary files /dev/null and b/litemon_v1.exe differ diff --git a/litemon_v1.py b/litemon_v1.py new file mode 100644 index 0000000..e84c572 --- /dev/null +++ b/litemon_v1.py @@ -0,0 +1,890 @@ +๏ปฟimport sys +import time +import socket +import threading +import subprocess +import json +import os +import urllib.request +import webbrowser +import winreg +import psutil +import ctypes +import ctypes.wintypes +from PyQt6.QtCore import QTimer, Qt, QRectF +from PyQt6.QtWidgets import ( + QApplication, QLabel, QVBoxLayout, QWidget, QMenu, QMessageBox, + QColorDialog, QFontDialog, QSystemTrayIcon +) +from PyQt6.QtGui import QFont, QIcon, QPixmap, QPainter, QColor, QPen, QBrush, QRegion, QPainterPath + +CONFIG_FILE = "litemon_config.json" +REG_PATH = r"Software\Microsoft\Windows\CurrentVersion\Run" +APP_REG_NAME = "LiteMonWidget" + +def run_sys_cmd(cmd): + try: + if hasattr(os, "startfile"): + os.startfile(cmd) + else: + subprocess.Popen(cmd, shell=True) + except Exception: + pass + +MENU_STYLE = """ +QMenu { + background-color: #1e1e2e; + color: #cdd6f4; + border: 1px solid #45475a; + border-radius: 8px; + padding: 6px; + font-family: 'Segoe UI', sans-serif; + font-size: 12px; +} +QMenu::item { + background-color: transparent; + padding: 6px 24px 6px 12px; + border-radius: 4px; + color: #cdd6f4; +} +QMenu::item:selected { + background-color: #313244; + color: #89b4fa; +} +QMenu::separator { + height: 1px; + background-color: #45475a; + margin: 5px 2px; +} +""" + +LANG = { + "ID": { + "layout": "๐Ÿ“ Mode Layout", + "horizontal_1": "Horizontal (1 Baris Memanjang)", + "horizontal_2": "Horizontal (2 Baris - Rapi di Toolbar)", + "vertical": "Vertikal (1 Kolom - Rata Kanan)", + "metrics": "๐Ÿ“Œ Opsi Tampilan Metrics", + "ref_ip": "๐Ÿ”„ Refresh IP & Jaringan Sekarang", + "net_if": "๐ŸŒ Pilih Interface Jaringan", + "basic": "โš™๏ธ Pengaturan Dasar", + "unit": "๐Ÿ”ค Satuan Bandwidth", + "refresh": "โฑ๏ธ Auto Refresh Rate", + "autostart": "๐Ÿš€ Auto-Start (Jalankan Saat Windows Boot)", + "taskbar": "๐Ÿ“Œ Tampilkan Ikon Taskbar Utama", + "tray": "๐Ÿ”” Tampilkan Ikon System Tray (Kanan Jam)", + "lang": "๐ŸŒ Bahasa / Language", + "style": "๐ŸŽจ Tampilan & Font", + "preset_size": "๐Ÿ“ Preset Ukuran", + "preset_theme": "๐ŸŒˆ Preset Tema Warna", + "dynamic_color": "โš ๏ธ Dynamic Color (Merah Jika >85%)", + "rounded": "๐Ÿ”ฒ Sudut Melengkung (Rounded)", + "custom_font": "๐Ÿ”ค Pilih Custom Font...", + "custom_bg": "๐ŸŽจ Pilih Warna Background...", + "custom_fg": "๐ŸŽจ Pilih Warna Teks...", + "reset_style": "๐Ÿ”„ Reset Tampilan ke Default", + "opacity": "๐Ÿ‘๏ธ Transparansi Window", + "tools": "๐Ÿ› ๏ธ Quick Utilities (Shortcut Windows)", + "about": "โ„น๏ธ Tentang LiteMon (v1.0)", + "donate": "โ˜• Donasi / Dukung Creator", + "exit": "โŒ Keluar LiteMon", + "about_title": "Tentang LiteMon Widget", + "free_tag": "๐ŸŽ Widget 100% GRATIS (Free Software)", + }, + "EN": { + "layout": "๐Ÿ“ Layout Mode", + "horizontal_1": "Horizontal (1 Line Compact)", + "horizontal_2": "Horizontal (2 Rows - Toolbar Style)", + "vertical": "Vertical (1 Column - Aligned)", + "metrics": "๐Ÿ“Œ Display Metrics Options", + "ref_ip": "๐Ÿ”„ Refresh IP & Network Now", + "net_if": "๐ŸŒ Select Network Interface", + "basic": "โš™๏ธ Basic Settings", + "unit": "๐Ÿ”ค Bandwidth Unit", + "refresh": "โฑ๏ธ Auto Refresh Rate", + "autostart": "๐Ÿš€ Auto-Start (Run on Windows Boot)", + "taskbar": "๐Ÿ“Œ Show Main Taskbar Icon", + "tray": "๐Ÿ”” Show System Tray Icon (Right/Notification)", + "lang": "๐ŸŒ Language / Bahasa", + "style": "๐ŸŽจ Appearance & Fonts", + "preset_size": "๐Ÿ“ Size Presets", + "preset_theme": "๐ŸŒˆ Theme Presets", + "dynamic_color": "โš ๏ธ Dynamic Colors (Red Alert >85%)", + "rounded": "๐Ÿ”ฒ Rounded Corners", + "custom_font": "๐Ÿ”ค Choose Custom Font...", + "custom_bg": "๐ŸŽจ Choose Background Color...", + "custom_fg": "๐ŸŽจ Choose Text Color...", + "reset_style": "๐Ÿ”„ Reset Appearance to Default", + "opacity": "๐Ÿ‘๏ธ Window Opacity", + "tools": "๐Ÿ› ๏ธ Quick Utilities (Windows Shortcuts)", + "about": "โ„น๏ธ About LiteMon (v1.0)", + "donate": "โ˜• Donate / Support Creator", + "exit": "โŒ Exit LiteMon", + "about_title": "About LiteMon Widget", + "free_tag": "๐ŸŽ Widget is 100% FREE (Open Application)", + } +} + +class SystemWidget(QWidget): + def __init__(self): + super().__init__() + self.drag_position = None + + self.version = "1.0" + self.current_lang = "ID" + self.layout_mode = "horizontal_2" + self.scale_mode = "normal" + self.current_theme = "Catppuccin Dark" + self.selected_interface = "Auto (Overall)" + self.unit_mode = "bytes" + self.refresh_interval = 1 + self.show_in_taskbar = False + self.show_in_tray = True + self.dynamic_color = True + self.is_rounded = True # OPSI ROUNDED BARU + + self.custom_font_family = "Consolas" + self.custom_bg = "" + self.custom_fg = "" + self.custom_border = "" + + self.show_metrics = { + "cpu": True, "ram": True, "uptime": True, "ping": True, + "up": True, "down": True, "local_ip": False, "public_ip": False + } + + self.local_ip = self._get_local_ip() + self.public_ip = "Loading..." + self.ping_ms = 0 + + self.load_config() + + self.last_net = self._get_net_counters() + self.last_time = time.time() + + threading.Thread(target=self._async_fetch_loop, daemon=True).start() + + self.themes_base = { + "Catppuccin Dark": {"bg": "rgba(30, 30, 46, 0.92)", "fg": "#cdd6f4", "border": "#45475a"}, + "Tokyo Night": {"bg": "rgba(26, 27, 38, 0.95)", "fg": "#7aa2f7", "border": "#bb9af7"}, + "Dracula Dark": {"bg": "rgba(40, 42, 54, 0.95)", "fg": "#f8f8f2", "border": "#bd93f9"}, + "Nord Dark": {"bg": "rgba(46, 52, 64, 0.95)", "fg": "#eceff4", "border": "#88c0d0"}, + "Cyberpunk Neon": {"bg": "rgba(13, 2, 33, 0.95)", "fg": "#00f5d4", "border": "#ff007f"}, + "Matrix Green": {"bg": "rgba(0, 15, 0, 0.95)", "fg": "#00ff66", "border": "#00ff66"}, + "Light Glass": {"bg": "rgba(250, 250, 250, 0.92)", "fg": "#1e1e2e", "border": "#aaaaaa"} + } + + self.scales = { + "mini": {"font_size": 10, "radius": 5}, + "normal": {"font_size": 11, "radius": 7}, + "large": {"font_size": 13, "radius": 9} + } + + self.initUI() + self.setup_tray() + + self.timer = QTimer() + self.timer.timeout.connect(self.update_metrics) + self.timer.start(self.refresh_interval * 1000) + + # Pengunci Z-Order untuk menempel ke Taskbar + self.topmost_timer = QTimer() + self.topmost_timer.timeout.connect(self.force_topmost) + self.topmost_timer.start(1000) + + def force_topmost(self): + try: + hwnd = int(self.winId()) + user32 = ctypes.windll.user32 + taskbar_hwnd = user32.FindWindowW("Shell_TrayWnd", None) + + if taskbar_hwnd: + if sys.maxsize > 2**32: + user32.SetWindowLongPtrW.argtypes = [ctypes.wintypes.HWND, ctypes.c_int, ctypes.wintypes.HWND] + user32.SetWindowLongPtrW(hwnd, -8, taskbar_hwnd) + else: + user32.SetWindowLongW.argtypes = [ctypes.wintypes.HWND, ctypes.c_int, ctypes.wintypes.HWND] + user32.SetWindowLongW(hwnd, -8, taskbar_hwnd) + + user32.SetWindowPos(hwnd, -1, 0, 0, 0, 0, 0x0001 | 0x0002 | 0x0010) + except Exception: + pass + + def apply_shape_mask(self): + scale = self.scales.get(self.scale_mode, self.scales["normal"]) + # Set radius 0 jika is_rounded mati (bentuk kotak sempurna) + radius = float(scale["radius"]) if self.is_rounded else 0.0 + + path = QPainterPath() + path.addRoundedRect(QRectF(self.rect()), radius, radius) + region = QRegion(path.toFillPolygon().toPolygon()) + self.setMask(region) + + def paintEvent(self, event): + painter = QPainter(self) + painter.setRenderHint(QPainter.RenderHint.Antialiasing) + + theme = self.themes_base.get(self.current_theme, self.themes_base["Catppuccin Dark"]) + scale = self.scales.get(self.scale_mode, self.scales["normal"]) + + bg_str = self.custom_bg if self.custom_bg else theme["bg"] + border_str = self.custom_border if self.custom_border else theme["border"] + + bg_color = self._parse_rgba(bg_str) if bg_str.startswith("rgba") else QColor(bg_str) + border_color = self._parse_rgba(border_str) if border_str.startswith("rgba") else QColor(border_str) + + painter.setBrush(QBrush(bg_color)) + painter.setPen(QPen(border_color, 1.5)) + + # Jika is_rounded False, set radius gambar jadi 0 + radius = float(scale["radius"]) if self.is_rounded else 0.0 + rect = QRectF(1.0, 1.0, self.width() - 2.0, self.height() - 2.0) + painter.drawRoundedRect(rect, radius, radius) + + def _parse_rgba(self, rgba_str): + try: + vals = rgba_str.replace("rgba(", "").replace(")", "").split(",") + r, g, b = int(vals[0]), int(vals[1]), int(vals[2]) + a = int(float(vals[3]) * 255) + return QColor(r, g, b, a) + except Exception: + return QColor(30, 30, 46, 235) + + def _get_local_ip(self): + try: + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + s.connect(("8.8.8.8", 80)) + ip = s.getsockname()[0] + s.close() + return ip + except Exception: + return "127.0.0.1" + + def _get_uptime(self): + boot_time = psutil.boot_time() + uptime_sec = time.time() - boot_time + m, s = divmod(uptime_sec, 60) + h, m = divmod(m, 60) + d, h = divmod(h, 24) + if d > 0: + return f"{int(d)}d {int(h)}h" + elif h > 0: + return f"{int(h)}h {int(m)}m" + else: + return f"{int(m)}m {int(s)}s" + + def _async_fetch_loop(self): + loop_count = 0 + while True: + if loop_count % 5 == 0 or self.public_ip in ["Loading...", "Offline"]: + self.local_ip = self._get_local_ip() + try: + req = urllib.request.urlopen("https://api.ipify.org", timeout=2.5) + self.public_ip = req.read().decode('utf-8') + except Exception: + self.public_ip = "Offline" + loop_count += 1 + + try: + start = time.perf_counter() + s = socket.create_connection(("1.1.1.1", 53), timeout=1.5) + s.close() + self.ping_ms = int((time.perf_counter() - start) * 1000) + except Exception: + self.ping_ms = -1 + time.sleep(2.0) + + def force_refresh_ip(self): + self.public_ip = "Refreshing..." + def _fetch(): + self.local_ip = self._get_local_ip() + try: + req = urllib.request.urlopen("https://api.ipify.org", timeout=3.0) + self.public_ip = req.read().decode('utf-8') + except Exception: + self.public_ip = "Offline" + threading.Thread(target=_fetch, daemon=True).start() + + def is_autostart_enabled(self): + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_READ) + winreg.QueryValueEx(key, APP_REG_NAME) + winreg.CloseKey(key) + return True + except Exception: + return False + + def toggle_autostart(self): + enabled = not self.is_autostart_enabled() + try: + key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, REG_PATH, 0, winreg.KEY_ALL_ACCESS) + if enabled: + script_path = os.path.abspath(sys.argv[0]) + cmd = f'"{sys.executable}" "{script_path}"' if script_path.endswith('.py') else f'"{script_path}"' + winreg.SetValueEx(key, APP_REG_NAME, 0, winreg.REG_SZ, cmd) + else: + try: + winreg.DeleteValue(key, APP_REG_NAME) + except FileNotFoundError: + pass + winreg.CloseKey(key) + except Exception: + pass + + def setup_tray(self): + pix = QPixmap(32, 32) + pix.fill(Qt.GlobalColor.transparent) + p = QPainter(pix) + p.setBrush(QColor("#89b4fa")) + p.setPen(Qt.PenStyle.NoPen) + p.drawEllipse(2, 2, 28, 28) + p.end() + + self.tray_icon = QSystemTrayIcon(QIcon(pix), self) + self.tray_icon.setToolTip("LiteMon v1.0") + self.tray_icon.activated.connect(self.on_tray_click) + if self.show_in_tray: + self.tray_icon.show() + else: + self.tray_icon.hide() + + def on_tray_click(self, reason): + if reason == QSystemTrayIcon.ActivationReason.Trigger: + self.showNormal() + self.raise_() + self.activateWindow() + self.force_topmost() + + def _get_net_counters(self): + if self.selected_interface == "Auto (Overall)": + return psutil.net_io_counters() + else: + counters = psutil.net_io_counters(pernic=True) + return counters.get(self.selected_interface, psutil.net_io_counters()) + + def update_window_flags(self): + flags = Qt.WindowType.FramelessWindowHint | Qt.WindowType.WindowStaysOnTopHint | Qt.WindowType.Tool + self.setWindowFlags(flags) + self.show() + self.force_topmost() + + def initUI(self): + self.update_window_flags() + self.setAttribute(Qt.WidgetAttribute.WA_TranslucentBackground, True) + + layout = QVBoxLayout() + layout.setContentsMargins(8, 3, 8, 3) + + self.label = QLabel("Loading LiteMon v1.0...") + self.label.setWordWrap(False) + self.label.setAttribute(Qt.WidgetAttribute.WA_TransparentForMouseEvents) + + layout.addWidget(self.label) + self.setLayout(layout) + self.setWindowTitle("LiteMon v1.0") + self.apply_style() + + def apply_style(self): + theme = self.themes_base.get(self.current_theme, self.themes_base["Catppuccin Dark"]) + scale = self.scales.get(self.scale_mode, self.scales["normal"]) + + fg = self.custom_fg if self.custom_fg else theme["fg"] + font_family = self.custom_font_family + + if self.layout_mode == "horizontal_2": + v_pad = 2 if self.scale_mode == "mini" else (3 if self.scale_mode == "normal" else 4) + h_pad = 6 if self.scale_mode == "mini" else (8 if self.scale_mode == "normal" else 10) + elif self.layout_mode == "horizontal_1": + v_pad = 3 if self.scale_mode == "mini" else (4 if self.scale_mode == "normal" else 5) + h_pad = 6 if self.scale_mode == "mini" else (8 if self.scale_mode == "normal" else 10) + else: + v_pad = 4 if self.scale_mode == "mini" else (6 if self.scale_mode == "normal" else 8) + h_pad = 6 if self.scale_mode == "mini" else (8 if self.scale_mode == "normal" else 10) + + self.layout().setContentsMargins(h_pad, v_pad, h_pad, v_pad) + + style = f""" + QLabel {{ + background-color: transparent; + color: {fg}; + font-family: '{font_family}', Consolas, monospace; + font-size: {scale['font_size']}px; + }} + """ + self.label.setStyleSheet(style) + self.adjustSize() + self.apply_shape_mask() + self.update() + + def toggle_rounded(self): + self.is_rounded = not self.is_rounded + self.save_config() + self.apply_style() + + def reset_custom_style(self): + self.current_theme = "Catppuccin Dark" + self.scale_mode = "normal" + self.layout_mode = "horizontal_2" + self.is_rounded = True + self.custom_bg = "" + self.custom_fg = "" + self.custom_border = "" + self.custom_font_family = "Consolas" + self.dynamic_color = True + self.setWindowOpacity(1.0) + self.save_config() + self.apply_style() + self.update_metrics() + + def format_speed(self, b_sec): + if self.unit_mode == "bits": + bits = b_sec * 8 + if bits < 1_000_000: + return f"{bits / 1000:>3.0f} Kbps".replace(" ", " ") + else: + return f"{bits / 1_000_000:>4.1f} Mbps".replace(" ", " ") + else: + if b_sec < 1048576: + return f"{b_sec / 1024:>3.0f} KB/s".replace(" ", " ") + else: + return f"{b_sec / 1048576:>4.1f} MB/s".replace(" ", " ") + + def _cwrap(self, text, is_warning): + if self.dynamic_color and is_warning: + return f"{text}" + return text + + def update_metrics(self): + cpu = psutil.cpu_percent() + ram = psutil.virtual_memory().percent + uptime_str = self._get_uptime() + + net_now = self._get_net_counters() + time_now = time.time() + dt = max(time_now - self.last_time, 0.001) + + bytes_sent_sec = (net_now.bytes_sent - self.last_net.bytes_sent) / dt + bytes_recv_sec = (net_now.bytes_recv - self.last_net.bytes_recv) / dt + + self.last_net = net_now + self.last_time = time_now + + up_str = self.format_speed(bytes_sent_sec) + down_str = self.format_speed(bytes_recv_sec) + ping_str = f"{self.ping_ms:>3}ms".replace(" ", " ") + + w_cpu = cpu >= 85 + w_ram = ram >= 85 + w_ping = self.ping_ms >= 100 and self.ping_ms > 0 + + metrics_map = { + "cpu": ("CPU", f"{cpu:2.0f}%".replace(" ", " "), w_cpu), + "ram": ("RAM", f"{ram:2.0f}%".replace(" ", " "), w_ram), + "uptime": ("UPTIME", uptime_str, False), + "ping": ("PING", ping_str, w_ping), + "up": ("UP", up_str, False), + "down": ("DOWN", down_str, False), + "local_ip": ("L-IP", self.local_ip, False), + "public_ip": ("P-IP", self.public_ip, False) + } + + r1_keys = ["cpu", "up", "ping", "local_ip"] + r2_keys = ["ram", "down", "uptime", "public_ip"] + + r1_items = [metrics_map[k] for k in r1_keys if self.show_metrics.get(k, False)] + r2_items = [metrics_map[k] for k in r2_keys if self.show_metrics.get(k, False)] + + if len(r1_items) == 0 and len(r2_items) > 1: + mid = (len(r2_items) + 1) // 2 + r1_items = r2_items[:mid] + r2_items = r2_items[mid:] + elif len(r2_items) == 0 and len(r1_items) > 1: + mid = (len(r1_items) + 1) // 2 + r2_items = r1_items[mid:] + r1_items = r1_items[:mid] + + if self.layout_mode == "horizontal_2": + max_cols = max(len(r1_items), len(r2_items)) + if max_cols > 0: + html = "" + html += "" + for c in range(max_cols): + if c > 0: + html += "" + if c < len(r1_items): + lbl, val, warn = r1_items[c] + html += f"" + else: + html += "" + html += "" + + html += "" + for c in range(max_cols): + if c > 0: + html += "" + if c < len(r2_items): + lbl, val, warn = r2_items[c] + html += f"" + else: + html += "" + html += "" + html += "
|{lbl} {self._cwrap(val, warn)}
|{lbl} {self._cwrap(val, warn)}
" + rendered_text = html + else: + rendered_text = "LiteMon v1.0" + + elif self.layout_mode == "horizontal_1": + all_active = r1_items + r2_items + if all_active: + items = [f"{lbl} {self._cwrap(val, warn)}" for lbl, val, warn in all_active] + rendered_text = "  |  ".join(items) + else: + rendered_text = "LiteMon v1.0" + + else: + all_active = r1_items + r2_items + if all_active: + html = "" + for lbl, val, warn in all_active: + html += f"" + html += "
{lbl}{self._cwrap(val, warn)}
" + rendered_text = html + else: + rendered_text = "LiteMon v1.0" + + self.label.setText(rendered_text) + self.adjustSize() + self.apply_shape_mask() + + screen = QApplication.primaryScreen() + if screen: + geom = screen.geometry() + new_x = self.x() + new_y = self.y() + if new_x + self.width() > geom.right(): + new_x = max(geom.left(), geom.right() - self.width()) + if new_y + self.height() > geom.bottom(): + new_y = max(geom.top(), geom.bottom() - self.height()) + if new_x != self.x() or new_y != self.y(): + self.move(new_x, new_y) + + self.update() + + def show_about(self): + t = LANG[self.current_lang] + msg = QMessageBox(self) + msg.setWindowTitle(t["about_title"]) + msg.setStyleSheet(""" + QMessageBox { background-color: #1e1e2e; } + QLabel { color: #cdd6f4; font-family: 'Segoe UI', sans-serif; font-size: 13px; } + QPushButton { background-color: #313244; color: #cdd6f4; border: 1px solid #45475a; border-radius: 6px; padding: 6px 16px; font-weight: bold; } + QPushButton:hover { background-color: #45475a; color: #89b4fa; } + """) + msg.setText( + f"LiteMon Widget v{self.version}
" + f"{t['free_tag']}

" + "Creator: @masbudikusuma
" + "Website: litemon.masbudi.my.id
" + "Donasi / Support: donate.masbudi.my.id/LiteMon

" + "Widget pemantau resource CPU, RAM & Traffic Jaringan Windows." + ) + msg.exec() + + def contextMenuEvent(self, event): + t = LANG[self.current_lang] + menu = QMenu(self) + menu.setStyleSheet(MENU_STYLE) + + layout_menu = menu.addMenu(t["layout"]) + act_h1 = layout_menu.addAction(f"{'๐Ÿ‘‰ ' if self.layout_mode == 'horizontal_1' else ''}{t['horizontal_1']}") + act_h1.triggered.connect(lambda: self.set_layout_mode("horizontal_1")) + + act_h2 = layout_menu.addAction(f"{'๐Ÿ‘‰ ' if self.layout_mode == 'horizontal_2' else ''}{t['horizontal_2']}") + act_h2.triggered.connect(lambda: self.set_layout_mode("horizontal_2")) + + act_v = layout_menu.addAction(f"{'๐Ÿ‘‰ ' if self.layout_mode == 'vertical' else ''}{t['vertical']}") + act_v.triggered.connect(lambda: self.set_layout_mode("vertical")) + + metrics_menu = menu.addMenu(t["metrics"]) + labels_map = { + "cpu": "CPU Usage", + "ram": "RAM Usage", + "uptime": "System Uptime", + "ping": "Ping Latency", + "up": "Upload Speed (UP)", + "down": "Download Speed (DOWN)", + "local_ip": "IP Lokal", + "public_ip": "IP Publik" + } + for key, title in labels_map.items(): + act = metrics_menu.addAction(f"{'โœ…' if self.show_metrics.get(key, False) else 'โŒ'} {title}") + act.triggered.connect(lambda checked, k=key: self.toggle_metric(k)) + + metrics_menu.addSeparator() + act_ref_ip = metrics_menu.addAction(t["ref_ip"]) + act_ref_ip.triggered.connect(self.force_refresh_ip) + + net_menu = menu.addMenu(t["net_if"]) + interfaces = ["Auto (Overall)"] + list(psutil.net_if_addrs().keys()) + for iface in interfaces: + is_active = " (Active)" if iface == self.selected_interface else "" + act = net_menu.addAction(f"{iface}{is_active}") + act.triggered.connect(lambda checked, i=iface: self.set_interface(i)) + + settings_menu = menu.addMenu(t["basic"]) + + unit_menu = settings_menu.addMenu(t["unit"]) + act_bytes = unit_menu.addAction(f"{'๐Ÿ‘‰ ' if self.unit_mode == 'bytes' else ''}Bytes/s (KB/s, MB/s)") + act_bytes.triggered.connect(lambda: self.set_unit_mode("bytes")) + act_bits = unit_menu.addAction(f"{'๐Ÿ‘‰ ' if self.unit_mode == 'bits' else ''}Bits/s (Kbps, Mbps)") + act_bits.triggered.connect(lambda: self.set_unit_mode("bits")) + + refresh_menu = settings_menu.addMenu(t["refresh"]) + for sec in [1, 2, 3, 5]: + act = refresh_menu.addAction(f"{'๐Ÿ‘‰ ' if self.refresh_interval == sec else ''}{sec} s") + act.triggered.connect(lambda checked, s=sec: self.set_refresh_interval(s)) + + autostart_act = settings_menu.addAction(f"{'โœ…' if self.is_autostart_enabled() else 'โŒ'} {t['autostart']}") + autostart_act.triggered.connect(self.toggle_autostart) + + taskbar_act = settings_menu.addAction(f"{'โœ…' if self.show_in_taskbar else 'โŒ'} {t['taskbar']}") + taskbar_act.triggered.connect(self.toggle_taskbar) + + tray_act = settings_menu.addAction(f"{'โœ…' if self.show_in_tray else 'โŒ'} {t['tray']}") + tray_act.triggered.connect(self.toggle_tray) + + lang_menu = menu.addMenu(t["lang"]) + act_id = lang_menu.addAction(f"{'๐Ÿ‘‰ ' if self.current_lang == 'ID' else ''}๐Ÿ‡ฎ๐Ÿ‡ฉ Bahasa Indonesia") + act_id.triggered.connect(lambda: self.set_language("ID")) + act_en = lang_menu.addAction(f"{'๐Ÿ‘‰ ' if self.current_lang == 'EN' else ''}๐Ÿ‡ฌ๐Ÿ‡ง English") + act_en.triggered.connect(lambda: self.set_language("EN")) + + style_menu = menu.addMenu(t["style"]) + + size_menu = style_menu.addMenu(t["preset_size"]) + for s_mode in ["mini", "normal", "large"]: + act = size_menu.addAction(f"{'๐Ÿ‘‰ ' if self.scale_mode == s_mode else ''}{s_mode.capitalize()}") + act.triggered.connect(lambda checked, sm=s_mode: self.set_scale_mode(sm)) + + theme_menu = style_menu.addMenu(t["preset_theme"]) + for t_name in self.themes_base.keys(): + act = theme_menu.addAction(f"{'๐Ÿ‘‰ ' if self.current_theme == t_name else ''}{t_name}") + act.triggered.connect(lambda checked, t=t_name: self.set_theme(t)) + + style_menu.addSeparator() + + # OPSI ROUNDED ON/OFF + act_rounded = style_menu.addAction(f"{'โœ…' if self.is_rounded else 'โŒ'} {t['rounded']}") + act_rounded.triggered.connect(self.toggle_rounded) + + act_dyn_color = style_menu.addAction(f"{'โœ…' if self.dynamic_color else 'โŒ'} {t['dynamic_color']}") + act_dyn_color.triggered.connect(self.toggle_dynamic_color) + + style_menu.addSeparator() + + act_font = style_menu.addAction(t["custom_font"]) + act_font.triggered.connect(self.pick_custom_font) + + act_color_bg = style_menu.addAction(t["custom_bg"]) + act_color_bg.triggered.connect(self.pick_custom_bg) + + act_color_fg = style_menu.addAction(t["custom_fg"]) + act_color_fg.triggered.connect(self.pick_custom_fg) + + act_reset_custom = style_menu.addAction(t["reset_style"]) + act_reset_custom.triggered.connect(self.reset_custom_style) + + opacity_menu = menu.addMenu(t["opacity"]) + for op in [1.0, 0.85, 0.70, 0.50]: + act = opacity_menu.addAction(f"{int(op * 100)}%") + act.triggered.connect(lambda checked, o=op: self.setWindowOpacity(o)) + + tools_menu = menu.addMenu(t["tools"]) + tools_menu.addAction("๐Ÿš€ Task Manager", lambda: run_sys_cmd("taskmgr.exe")) + tools_menu.addAction("๐Ÿ“Š Resource Monitor", lambda: run_sys_cmd("resmon.exe")) + tools_menu.addAction("โš™๏ธ System Configuration (msconfig)", lambda: run_sys_cmd("msconfig.exe")) + tools_menu.addAction("๐Ÿ” DirectX Diagnostics (dxdiag)", lambda: run_sys_cmd("dxdiag.exe")) + tools_menu.addAction("๐Ÿ’ป Device Manager (devmgmt.msc)", lambda: run_sys_cmd("devmgmt.msc")) + tools_menu.addAction("๐ŸŒ Network Connections (ncpa.cpl)", lambda: run_sys_cmd("ncpa.cpl")) + tools_menu.addAction("๐Ÿ› ๏ธ Windows Services (services.msc)", lambda: run_sys_cmd("services.msc")) + tools_menu.addAction("๐Ÿ’ป Command Prompt (CMD)", lambda: run_sys_cmd("cmd.exe")) + tools_menu.addAction("๐ŸŽ›๏ธ Control Panel", lambda: run_sys_cmd("control.exe")) + + menu.addSeparator() + donate_act = menu.addAction(t["donate"]) + donate_act.triggered.connect(lambda: webbrowser.open("https://donate.masbudi.my.id/LiteMon")) + + about_act = menu.addAction(t["about"]) + about_act.triggered.connect(self.show_about) + + menu.addSeparator() + exit_action = menu.addAction(t["exit"]) + exit_action.triggered.connect(QApplication.instance().quit) + + menu.exec(event.globalPos()) + + def toggle_metric(self, key): + self.show_metrics[key] = not self.show_metrics.get(key, False) + self.save_config() + self.update_metrics() + + def set_interface(self, iface): + self.selected_interface = iface + self.last_net = self._get_net_counters() + self.save_config() + self.update_metrics() + + def set_layout_mode(self, mode): + self.layout_mode = mode + self.save_config() + self.apply_style() + self.update_metrics() + + def set_scale_mode(self, mode): + self.scale_mode = mode + self.save_config() + self.apply_style() + + def set_theme(self, theme_name): + self.current_theme = theme_name + self.custom_bg = "" + self.custom_fg = "" + self.custom_border = "" + self.save_config() + self.apply_style() + + def toggle_dynamic_color(self): + self.dynamic_color = not self.dynamic_color + self.save_config() + self.update_metrics() + + def set_unit_mode(self, mode): + self.unit_mode = mode + self.save_config() + self.update_metrics() + + def set_refresh_interval(self, sec): + self.refresh_interval = sec + self.timer.setInterval(sec * 1000) + self.save_config() + + def set_language(self, lang_code): + self.current_lang = lang_code + self.save_config() + self.update_metrics() + + def toggle_taskbar(self): + self.show_in_taskbar = not self.show_in_taskbar + self.update_window_flags() + self.save_config() + + def toggle_tray(self): + self.show_in_tray = not self.show_in_tray + if self.show_in_tray: + self.tray_icon.show() + else: + self.tray_icon.hide() + self.save_config() + + def pick_custom_font(self): + font, ok = QFontDialog.getFont(QFont(self.custom_font_family), self) + if ok: + self.custom_font_family = font.family() + self.save_config() + self.apply_style() + + def pick_custom_bg(self): + color = QColorDialog.getColor( + parent=self, + title="Pilih Warna Background", + options=QColorDialog.ColorDialogOption.ShowAlphaChannel + ) + if color.isValid(): + self.custom_bg = f"rgba({color.red()}, {color.green()}, {color.blue()}, {color.alpha() / 255:.2f})" + self.save_config() + self.apply_style() + + def pick_custom_fg(self): + color = QColorDialog.getColor(parent=self, title="Pilih Warna Teks") + if color.isValid(): + self.custom_fg = color.name() + self.save_config() + self.apply_style() + + def save_config(self): + config = { + "version": self.version, + "current_lang": self.current_lang, + "layout_mode": self.layout_mode, + "scale_mode": self.scale_mode, + "current_theme": self.current_theme, + "selected_interface": self.selected_interface, + "unit_mode": self.unit_mode, + "refresh_interval": self.refresh_interval, + "show_in_taskbar": self.show_in_taskbar, + "show_in_tray": self.show_in_tray, + "dynamic_color": self.dynamic_color, + "is_rounded": self.is_rounded, + "custom_font_family": self.custom_font_family, + "custom_bg": self.custom_bg, + "custom_fg": self.custom_fg, + "custom_border": self.custom_border, + "show_metrics": self.show_metrics, + "pos_x": self.x(), + "pos_y": self.y() + } + try: + with open(CONFIG_FILE, "w") as f: + json.dump(config, f, indent=4) + except Exception: + pass + + def load_config(self): + if os.path.exists(CONFIG_FILE): + try: + with open(CONFIG_FILE, "r") as f: + c = json.load(f) + self.current_lang = c.get("current_lang", self.current_lang) + self.layout_mode = c.get("layout_mode", self.layout_mode) + if self.layout_mode == "grid_2": + self.layout_mode = "horizontal_2" + self.scale_mode = c.get("scale_mode", self.scale_mode) + self.current_theme = c.get("current_theme", self.current_theme) + self.selected_interface = c.get("selected_interface", self.selected_interface) + self.unit_mode = c.get("unit_mode", self.unit_mode) + self.refresh_interval = c.get("refresh_interval", self.refresh_interval) + self.show_in_taskbar = c.get("show_in_taskbar", self.show_in_taskbar) + self.show_in_tray = c.get("show_in_tray", self.show_in_tray) + self.dynamic_color = c.get("dynamic_color", self.dynamic_color) + self.is_rounded = c.get("is_rounded", self.is_rounded) + self.custom_font_family = c.get("custom_font_family", self.custom_font_family) + self.custom_bg = c.get("custom_bg", self.custom_bg) + self.custom_fg = c.get("custom_fg", self.custom_fg) + self.custom_border = c.get("custom_border", self.custom_border) + self.show_metrics.update(c.get("show_metrics", {})) + if "pos_x" in c and "pos_y" in c: + self.move(c["pos_x"], c["pos_y"]) + except Exception: + pass + + def mousePressEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.drag_position = event.globalPosition().toPoint() - self.frameGeometry().topLeft() + event.accept() + + def mouseMoveEvent(self, event): + if event.buttons() == Qt.MouseButton.LeftButton and self.drag_position is not None: + self.move(event.globalPosition().toPoint() - self.drag_position) + event.accept() + + def mouseReleaseEvent(self, event): + if event.button() == Qt.MouseButton.LeftButton: + self.drag_position = None + self.save_config() + event.accept() + +if __name__ == '__main__': + app = QApplication(sys.argv) + widget = SystemWidget() + widget.show() + sys.exit(app.exec())