891 lines
36 KiB
Python
891 lines
36 KiB
Python
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"<span style='color:#ff5555; font-weight:bold;'>{text}</span>"
|
||
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 = "<table cellpadding='0' cellspacing='0' style='border-collapse: collapse; margin: 0; padding: 0;'>"
|
||
html += "<tr>"
|
||
for c in range(max_cols):
|
||
if c > 0:
|
||
html += "<td width='12' align='center' style='color:#585b70;'>|</td>"
|
||
if c < len(r1_items):
|
||
lbl, val, warn = r1_items[c]
|
||
html += f"<td style='padding: 1px 3px; white-space: nowrap;'><b>{lbl}</b> {self._cwrap(val, warn)}</td>"
|
||
else:
|
||
html += "<td style='padding: 1px 3px;'></td>"
|
||
html += "</tr>"
|
||
|
||
html += "<tr>"
|
||
for c in range(max_cols):
|
||
if c > 0:
|
||
html += "<td width='12' align='center' style='color:#585b70;'>|</td>"
|
||
if c < len(r2_items):
|
||
lbl, val, warn = r2_items[c]
|
||
html += f"<td style='padding: 1px 3px; white-space: nowrap;'><b>{lbl}</b> {self._cwrap(val, warn)}</td>"
|
||
else:
|
||
html += "<td style='padding: 1px 3px;'></td>"
|
||
html += "</tr>"
|
||
html += "</table>"
|
||
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"<b>{lbl}</b> {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 = "<table cellpadding='0' cellspacing='1' style='border-collapse: collapse;'>"
|
||
for lbl, val, warn in all_active:
|
||
html += f"<tr><td align='left' style='padding: 1px 4px;'><b>{lbl}</b></td><td width='12'></td><td align='right' style='padding: 1px 4px;'>{self._cwrap(val, warn)}</td></tr>"
|
||
html += "</table>"
|
||
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"<b>LiteMon Widget v{self.version}</b><br>"
|
||
f"<font color='#a6e3a1'><b>{t['free_tag']}</b></font><br><br>"
|
||
"<b>Creator:</b> @masbudikusuma<br>"
|
||
"<b>Website:</b> <a href='https://litemon.masbudi.my.id' style='color:#89b4fa;'>litemon.masbudi.my.id</a><br>"
|
||
"<b>Donasi / Support:</b> <a href='https://donate.masbudi.my.id/LiteMon' style='color:#f9e2af;'>donate.masbudi.my.id/LiteMon</a><br><br>"
|
||
"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())
|