Upload files to "/"
This commit is contained in:
+890
@@ -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"<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())
|
||||
Reference in New Issue
Block a user