my-html-app / app.py
chanraphone4's picture
Update app.py
436c8d8 verified
Raw
History Blame Contribute Delete
50.7 kB
import sys
import os
import json
import re
import shutil
import random
import stat
import gc
import threading
import time
from urllib.request import Request, urlopen
# αž–αŸ’αž™αžΆαž™αžΆαž˜αž‘αžΆαž‰αž™αž€αž”αžŽαŸ’αžŽαžΆαž›αŸαž™ psutil αžŸαž˜αŸ’αžšαžΆαž”αŸ‹αžœαžΆαžŸαŸ‹αž‘αž·αž“αŸ’αž“αž“αŸαž™αž˜αŸ‰αžΆαžŸαŸŠαžΈαž“
try:
import psutil
except ImportError:
psutil = None
# =========================================================
# ៑. αž”αŸ’αžšαžΎαž”αŸ’αžšαžΆαžŸαŸ‹αžαŸ‚ Flags αžŠαžΎαž˜αžŠαŸ†αž”αžΌαž„αž”αž„αŸ’αž’αžŸαŸ‹αžŠαŸ‚αž›αž˜αžΆαž“αžŸαŸ’αžαž·αžšαž—αžΆαž– ៑០០%
# =========================================================
os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = (
"--disable-features=FFmpegAllowLists "
"--disable-web-security "
"--allow-running-insecure-content "
"--no-sandbox "
"--ignore-certificate-errors "
"--disable-gpu "
"--disable-gpu-sandbox"
)
from PyQt6.QtCore import QUrl, Qt, QSize, QThread, pyqtSignal, QTimer, QRect, QObject
from PyQt6.QtGui import QColor, QFont, QIcon, QAction, QPainter
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QFrame, QLabel, QPushButton, QLineEdit
from PyQt6.QtWebEngineWidgets import QWebEngineView
from PyQt6.QtWebEngineCore import QWebEngineProfile, QWebEnginePage, QWebEngineScript
from http.server import ThreadingHTTPServer, BaseHTTPRequestHandler
# αž―αž€αžŸαžΆαžš Database
DB_PATH = "./data/instances.json"
STORES_PATH = "./data/stores.json"
URLS_PATH = "./data/urls.json"
CONFIG_PATH = "./data/config.json"
DEFAULT_URL = "https://www.facebook.com"
PORT = 8000
DEVICES = {
"Compact Mobile (360x500)": {
"width": 360, "height": 500,
"ua": "Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
},
"Samsung Galaxy S23 (360x780)": {
"width": 360, "height": 780,
"ua": "Mozilla/5.0 (Linux; Android 13; SM-S911B) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/112.0.0.0 Mobile Safari/537.36"
},
"Google Pixel 8 (412x892)": {
"width": 412, "height": 892,
"ua": "Mozilla/5.0 (Linux; Android 14; Pixel 8) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Mobile Safari/537.36"
},
"iPhone 15 Pro (393x852)": {
"width": 393, "height": 852,
"ua": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
},
"iPad Pro 12.9 (1024x1366)": {
"width": 1024, "height": 1366,
"ua": "Mozilla/5.0 (iPad; CPU OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"
}
}
GLOBAL_PROFILES_CACHE = {}
running_windows = {}
instance_statuses = {}
calculated_sizes = {}
# --- Helper Functions ---
def get_dir_size(path):
total_size = 0
if not os.path.exists(path):
return 0
try:
for dirpath, dirnames, filenames in os.walk(path):
for f in filenames:
fp = os.path.join(dirpath, f)
if not os.path.islink(fp):
total_size += os.path.getsize(fp)
except Exception:
pass
return total_size
def format_size(bytes_size):
if bytes_size <= 0: return "0 KB"
kb_size = bytes_size / 1024
if kb_size < 1024: return f"{kb_size:.1f} KB"
mb_size = kb_size / 1024
if mb_size < 1024: return f"{mb_size:.1f} MB"
return f"{mb_size/1024:.1f} GB"
def load_json(path, default):
if not os.path.exists(path):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "w", encoding="utf-8") as f:
json.dump(default, f, ensure_ascii=False, indent=4)
return default
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return default
def save_json(path, data):
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
# αž•αŸ’αž‘αž»αž€αž‘αž·αž“αŸ’αž“αž“αŸαž™αžŠαŸ†αž”αžΌαž„
instances = load_json(DB_PATH, [{"id": 1, "name": "αž‚αžŽαž“αžΈ #1", "device": "Compact Mobile (360x500)", "url": DEFAULT_URL, "scale": 0.50, "ip": "", "city": "", "note": "", "store": "αž‘αžΌαž‘αŸ… (Default)"}])
stores = load_json(STORES_PATH, ["αž‘αžΌαž‘αŸ… (Default)"])
urls_db = load_json(URLS_PATH, [DEFAULT_URL, "https://www.youtube.com", "https://www.google.com", "https://www.tiktok.com"])
app_config = load_json(CONFIG_PATH, {"chk_auto_scroll": True, "delay_spin": 3, "auto_close_spin": 0, "arrange_type_idx": 0, "cols_spin": 3, "preset_speed": 2, "preset_mode_idx": 1})
def get_or_create_profile(instance_id, parent_widget):
profile_name = f"instance_{instance_id}"
if profile_name in GLOBAL_PROFILES_CACHE:
return GLOBAL_PROFILES_CACHE[profile_name]
data_dir = os.path.abspath("./data")
profile = QWebEngineProfile(profile_name, parent_widget)
profile.setPersistentStoragePath(os.path.join(data_dir, profile_name, "storage"))
profile.setCachePath(os.path.join(data_dir, profile_name, "cache"))
profile.setPersistentCookiesPolicy(QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies)
GLOBAL_PROFILES_CACHE[profile_name] = profile
return profile
# =========================================================
# ្. αž”αŸ’αžšαž–αŸαž“αŸ’αž’αžŸαŸ’αž€αŸαž“ IP αž“αž·αž„αžŸαŸ’αžœαŸ‚αž„αžšαž€αž‘αžΈαžαžΆαŸ†αž„ (Background Thread)
# =========================================================
class IpFetcher(QThread):
ip_fetched = pyqtSignal(str, str)
def run(self):
import urllib.request
opener = urllib.request.build_opener()
try:
req = Request("https://ipapi.co/json/", headers={'User-Agent': 'Mozilla/5.0'})
with opener.open(req, timeout=6) as response:
data = json.loads(response.read().decode())
self.ip_fetched.emit(data.get("ip", "Unknown IP"), data.get("city", "Unknown City"))
return
except Exception: pass
try:
req = Request("https://freeipapi.com/api/json", headers={'User-Agent': 'Mozilla/5.0'})
with opener.open(req, timeout=6) as response:
data = json.loads(response.read().decode())
self.ip_fetched.emit(data.get("ipAddress", "Unknown IP"), data.get("cityName", "Unknown City"))
return
except Exception:
self.ip_fetched.emit("127.0.0.1", "Local Connection")
class CustomWebEnginePage(QWebEnginePage):
console_message_signal = pyqtSignal(int, str, int, str)
def javaScriptConsoleMessage(self, level, message, lineNumber, sourceID):
self.console_message_signal.emit(level, message, lineNumber, sourceID)
super().javaScriptConsoleMessage(level, message, lineNumber, sourceID)
# =========================================================
# ៣. αžαŸ’αž“αžΆαž€αŸ‹αž”αž„αŸ’αž€αžΎαžαž•αŸ’αž‘αžΆαŸ†αž„αž‘αžΌαžšαžŸαž–αŸ’αž‘ (Preserved PyQt6 Mobile Frame)
# =========================================================
class MobileSimulator(QMainWindow):
def __init__(self, instance_id=1, device_name=None, initial_url=DEFAULT_URL, scale_factor=0.75):
super().__init__()
self.instance_id = instance_id
self.current_device_name = device_name if device_name in DEVICES else list(DEVICES.keys())[0]
self.home_url = initial_url if initial_url else DEFAULT_URL
self.scale_factor = scale_factor if scale_factor else 0.75
self.is_landscape = False
self.is_auto_scrolling = False
self.auto_close_on_finish = False
self.is_loading_started = False
# αž”αž„αŸ’αžαŸ†αž±αŸ’αž™ PyQt6 αž€αž˜αŸ’αž‘αŸαž… αž“αž·αž„αžŠαŸ„αŸ‡αž›αŸ‚αž„ Memory αžšαž”αžŸαŸ‹ Window αž“αŸαŸ‡αž—αŸ’αž›αžΆαž˜αŸ—αž“αŸ…αž–αŸαž›αž…αž»αž…αž”αž·αž‘ (Close)
self.setAttribute(Qt.WidgetAttribute.WA_DeleteOnClose)
self.remaining_seconds = 0
self.countdown_timer = QTimer(self)
self.countdown_timer.timeout.connect(self.update_countdown)
self.setWindowTitle(f"αž‘αžΌαžšαžŸαž–αŸ’αž‘αžαŸαžŸαŸ’αž - αž‚αžŽαž“αžΈ #{self.instance_id}")
self.setStyleSheet("background-color: #0f1115;")
self.profile = get_or_create_profile(self.instance_id, self)
self.webpage = CustomWebEnginePage(self.profile, self)
self.webpage.setBackgroundColor(QColor("white"))
self.browser = QWebEngineView()
self.browser.setPage(self.webpage)
self.browser.setStyleSheet("border: none; background-color: white; border-radius: 0px 0px 18px 18px;")
self.browser.loadStarted.connect(self.on_load_started)
self.browser.loadFinished.connect(self.on_load_finished)
self.browser.urlChanged.connect(self.update_url_bar)
main_widget = QWidget()
self.setCentralWidget(main_widget)
self.main_layout = QVBoxLayout(main_widget)
self.main_layout.setContentsMargins(10, 10, 10, 10)
self.main_layout.setSpacing(5)
self.phone_frame = QFrame()
self.phone_frame.setStyleSheet("QFrame { background-color: #020617; border: 8px solid #1e293b; border-radius: 32px; }")
self.phone_inner_layout = QVBoxLayout(self.phone_frame)
self.phone_inner_layout.setContentsMargins(6, 6, 6, 6)
self.phone_inner_layout.setSpacing(6)
self.notch_frame = QFrame()
self.notch_frame.setFixedHeight(22)
self.notch_frame.setStyleSheet("background-color: transparent; border: none;")
notch_layout = QHBoxLayout(self.notch_frame)
notch_layout.setContentsMargins(0, 0, 0, 0)
self.notch_element = QFrame()
self.notch_element.setFixedSize(90, 14)
self.notch_element.setStyleSheet("background-color: #000000; border-radius: 7px; border: none;")
notch_layout.addWidget(self.notch_element, alignment=Qt.AlignmentFlag.AlignCenter)
self.phone_inner_layout.addWidget(self.notch_frame)
browser_bar = QHBoxLayout()
browser_bar.setContentsMargins(4, 0, 4, 0)
browser_bar.setSpacing(6)
self.btn_back = QPushButton("β—€")
self.btn_back.setStyleSheet(self.get_icon_style())
self.btn_back.clicked.connect(self.browser.back)
browser_bar.addWidget(self.btn_back)
self.btn_forward = QPushButton("β–Ά")
self.btn_forward.setStyleSheet(self.get_icon_style())
self.btn_forward.clicked.connect(self.browser.forward)
browser_bar.addWidget(self.btn_forward)
self.btn_home = QPushButton("🏠")
self.btn_home.setStyleSheet(self.get_icon_style())
self.btn_home.clicked.connect(self.go_home)
browser_bar.addWidget(self.btn_home)
self.url_input = QLineEdit()
self.url_input.setText(self.home_url)
self.url_input.setStyleSheet("QLineEdit { background-color: #0f172a; color: #f8fafc; border: 1px solid #334155; border-radius: 12px; padding: 4px 10px; font-size: 11px; } QLineEdit:focus { border-color: #38bdf8; }")
self.url_input.returnPressed.connect(self.load_url)
browser_bar.addWidget(self.url_input)
self.btn_scroll = QPushButton("πŸ“œ")
self.btn_scroll.setStyleSheet(self.get_icon_style())
self.btn_scroll.clicked.connect(lambda: self.toggle_auto_scroll())
browser_bar.addWidget(self.btn_scroll)
self.btn_rotate = QPushButton("πŸ”„")
self.btn_rotate.setStyleSheet(self.get_icon_style())
self.btn_rotate.clicked.connect(self.toggle_rotation)
browser_bar.addWidget(self.btn_rotate)
self.phone_inner_layout.addLayout(browser_bar)
self.phone_inner_layout.addWidget(self.browser, 1)
self.home_bar = QFrame()
self.home_bar.setFixedHeight(12)
self.home_bar.setStyleSheet("background-color: transparent; border: none;")
home_bar_layout = QHBoxLayout(self.home_bar)
indicator = QFrame()
indicator.setFixedSize(110, 4)
indicator.setStyleSheet("background-color: #475569; border-radius: 2px;")
home_bar_layout.addWidget(indicator, alignment=Qt.AlignmentFlag.AlignCenter)
self.phone_inner_layout.addWidget(self.home_bar)
self.main_layout.addWidget(self.phone_frame, alignment=Qt.AlignmentFlag.AlignCenter)
self.scroll_timer = QTimer(self)
self.scroll_timer.timeout.connect(self.scroll_page)
self.apply_device_settings()
self.load_url()
self.ip_fetcher = IpFetcher(self)
self.ip_fetcher.ip_fetched.connect(self.on_ip_fetched)
self.ip_fetcher.start()
def get_icon_style(self):
return "QPushButton { background-color: #1e293b; color: #f8fafc; border: 1px solid #334155; border-radius: 13px; min-width: 26px; min-height: 26px; max-width: 26px; max-height: 26px; font-size: 10px; } QPushButton:hover { background-color: #38bdf8; color: #0f1115; }"
def on_load_started(self):
self.is_loading_started = True
self.update_status("⏳ αž€αŸ†αž–αž»αž„αž•αŸ’αž‘αž»αž€...")
def on_load_finished(self, ok):
self.is_loading_started = False
self.update_status("🟒 Online")
def on_ip_fetched(self, ip, city):
global instances
for inst in instances:
if inst["id"] == self.instance_id:
inst["ip"] = ip
inst["city"] = city
break
save_json(DB_PATH, instances)
def update_status(self, text):
instance_statuses[self.instance_id] = text
def start_countdown(self, duration_min, auto_close=True):
if duration_min > 0:
self.remaining_seconds = duration_min * 60
self.auto_close_on_finish = auto_close
self.countdown_timer.start(1000)
def update_countdown(self):
if self.remaining_seconds > 0:
self.remaining_seconds -= 1
mins = self.remaining_seconds // 60
secs = self.remaining_seconds % 60
self.update_status(f"πŸ“œ αžŸαŸ’αžšαžΌαž› ({mins:02d}:{secs:02d})")
else:
self.countdown_timer.stop()
if self.auto_close_on_finish:
self.close()
def toggle_auto_scroll(self, force_state=None, speed=2, duration_min=0, random_mode=True, auto_close=None):
if force_state is not None:
self.is_auto_scrolling = force_state
else:
self.is_auto_scrolling = not self.is_auto_scrolling
if auto_close is not None:
self.auto_close_on_finish = auto_close
if self.is_auto_scrolling:
self.scroll_speed_base = speed
self.scroll_speed = max(1, speed + random.randint(-1, 1))
self.scroll_direction = 1
self.random_mode = random_mode
self.ticks_since_dir_change = 0
self.next_dir_change_ticks = random.randint(100, 300)
self.scroll_timer.start(random.randint(35, 55))
if duration_min > 0:
self.start_countdown(duration_min, auto_close)
else:
self.update_status("πŸ“œ αž€αŸ†αž–αž»αž„αžŸαŸ’αžšαžΌαž›...")
else:
self.scroll_timer.stop()
self.countdown_timer.stop()
self.update_status("🟒 Online")
def scroll_page(self):
if not hasattr(self, 'browser') or self.browser is None:
self.scroll_timer.stop()
return
try:
if self.random_mode:
self.ticks_since_dir_change += 1
if self.ticks_since_dir_change >= self.next_dir_change_ticks:
self.ticks_since_dir_change = 0
self.next_dir_change_ticks = random.randint(100, 300)
self.scroll_direction = -1 if random.random() < 0.20 else 1
self.scroll_speed = max(1, self.scroll_speed_base + random.randint(-1, 1))
self.browser.page().runJavaScript(f"window.scrollBy(0, {self.scroll_direction * self.scroll_speed});")
except Exception:
self.scroll_timer.stop()
def load_url(self):
url = self.url_input.text().strip()
if not url: return
if not url.startswith("http://") and not url.startswith("https://"):
url = "https://" + url
self.browser.setUrl(QUrl(url))
def update_url_bar(self, qurl):
self.url_input.setText(qurl.toString())
def go_home(self):
self.browser.setUrl(QUrl(self.home_url))
def toggle_rotation(self):
self.is_landscape = not self.is_landscape
self.apply_device_settings()
def apply_device_settings(self):
device = DEVICES[self.current_device_name]
w, h, ua = device["width"], device["height"], device["ua"]
self.profile.setHttpUserAgent(ua)
self.profile.scripts().clear()
js_code = ""
if "Android" in ua:
js_code = "Object.defineProperty(navigator, 'platform', { get: () => 'Linux armv8l' });"
elif "iPhone" in ua:
js_code = "Object.defineProperty(navigator, 'platform', { get: () => 'iPhone' });"
if js_code:
script = QWebEngineScript()
script.setSourceCode(js_code)
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentCreation)
self.profile.scripts().insert(script)
if self.is_landscape:
w, h = h, w
self.notch_frame.hide()
else:
self.notch_frame.show()
w_scaled = int(w * self.scale_factor)
h_scaled = int(h * self.scale_factor)
self.phone_frame.setFixedSize(w_scaled + 24, h_scaled + (60 if self.is_landscape else 85))
self.setFixedSize(w_scaled + 44, h_scaled + (80 if self.is_landscape else 105))
self.browser.setZoomFactor(self.scale_factor)
def closeEvent(self, event):
self.scroll_timer.stop()
self.countdown_timer.stop()
# αž”αž‰αŸ’αžˆαž”αŸ‹ αž“αž·αž„αžŸαž˜αŸ’αž’αžΆαž WebEngine αžŠαžΆαž…αŸ‹αžαžΆαž αžŠαžΎαž˜αŸ’αž”αžΈαžŠαŸ„αŸ‡αžŸαŸ„αžš (Unlock) αž‘αž·αž“αŸ’αž“αž“αŸαž™ Profile αžšαž”αžŸαŸ‹αž‚αžŽαž“αžΈ
if hasattr(self, 'browser') and self.browser is not None:
self.browser.setPage(None)
self.webpage.deleteLater()
self.browser.deleteLater()
self.browser = None
if self.instance_id in running_windows:
del running_windows[self.instance_id]
instance_statuses[self.instance_id] = "πŸ”΄ Offline"
event.accept()
# =========================================================
# ៀ. Web App Control Panel (αž€αŸ‚αž€αŸ‚αž”αŸ’αžšαŸ‚αž€αžΆαžšαžŸαŸαž‘αž·αž…αž‚αžŽαž“αžΈ - Zero Selection Loss)
# =========================================================
HTML_PANEL = """<!DOCTYPE html>
<html lang="km">
<head>
<meta charset="UTF-8">
<title>αž”αŸ’αžšαž–αŸαž“αŸ’αž’αž‚αŸ’αžšαž”αŸ‹αž‚αŸ’αžšαž„αž‚αžŽαž“αžΈ - Web Control Panel</title>
<style>
body {
background-color: #0f1115;
color: #e2e8f0;
font-family: 'Segoe UI', 'Kantumruuy Pro', sans-serif;
font-size: 12px;
margin: 0;
padding: 15px;
}
.top-bar {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 15px;
}
.lbl-total { font-weight: bold; color: #38bdf8; font-size: 13px; }
.main-body { display: flex; gap: 15px; }
.table-container {
flex: 1;
background-color: #161920;
border: 1px solid #1e293b;
border-radius: 8px;
padding: 12px;
}
table { width: 100%; border-collapse: collapse; }
th, td { padding: 8px; text-align: left; border-bottom: 1px solid #1d212a; }
th { background-color: #0f172a; color: #94a3b8; font-weight: bold; }
.sidebar {
width: 240px;
background-color: #161920;
border: 1px solid #1e293b;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 12px;
}
.group-box { border: 1px solid #1e293b; border-radius: 6px; padding: 10px; }
.group-title { font-weight: bold; color: #38bdf8; font-size: 11px; margin-bottom: 6px; }
input[type="text"], select, input[type="number"] {
background-color: #0f1115; color: #f8fafc; border: 1px solid #2d3343;
border-radius: 6px; padding: 5px 8px; width: 100%; box-sizing: border-box;
}
button {
background-color: #1e293b; color: #f8fafc; border: 1px solid #334155;
border-radius: 6px; padding: 6px 12px; font-weight: bold; cursor: pointer;
}
button:hover { background-color: #38bdf8; color: #0f1115; }
.btn-success { background-color: #10b981; color: white; border: none; }
.btn-success:hover { background-color: #059669; }
.btn-danger { background-color: #ef4444; color: white; border: none; }
.btn-danger:hover { background-color: #dc2626; }
.digital-timer { font-size: 24px; font-weight: bold; color: #f97316; font-family: monospace; margin-left: auto; }
.status-badge { font-weight: bold; }
.footer-bar { margin-top: 15px; display: flex; justify-content: space-between; color: #64748b; font-size: 11px; }
.modal {
display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.6); justify-content: center; align-items: center;
}
.modal-content {
background-color: #111317; border: 1px solid #2d3343; border-radius: 12px;
padding: 20px; width: 350px; display: flex; flex-direction: column; gap: 12px;
}
</style>
</head>
<body>
<div class="top-bar">
<div class="lbl-total" id="lbl-total">αž‚αžŽαž“αžΈαžŸαžšαž»αž” (Total Profiles): 0</div>
<button onclick="addInstance()">οΌ‹ αž”αž“αŸ’αžαŸ‚αž˜αž‚αžŽαž“αžΈ</button>
<button class="btn-danger" onclick="deleteSelected()">- αž›αž»αž”αž…αŸαž‰</button>
<button class="btn-success" onclick="startSelected()">β–Ά αž…αžΆαž”αŸ‹αž•αŸ’αžŠαžΎαž˜αžšαžαŸ‹ (START)</button>
<button class="btn-danger" onclick="stopSelected()">⏹ αž”αž‰αŸ’αžˆαž”αŸ‹αžšαžαŸ‹ (STOP)</button>
<div class="digital-timer" id="stopwatch">00:00:00</div>
<input type="text" id="search" placeholder="αžŸαŸ’αžœαŸ‚αž„αžšαž€..." style="width: 150px;" oninput="filterRows()">
<button onclick="document.getElementById('search').value=''; filterRows();">αžŸαž˜αŸ’αž’αžΆαž</button>
</div>
<div class="main-body">
<div class="table-container">
<table>
<thead>
<tr>
<th style="width: 25px;"><input type="checkbox" id="check-all" onchange="selectAll(this)"></th>
<th>αžŸαŸ’αžαžΆαž“αž—αžΆαž– / αžŸαž€αž˜αŸ’αž˜αž—αžΆαž–</th>
<th>αžˆαŸ’αž˜αŸ„αŸ‡αž‚αžŽαž“αžΈ</th>
<th>αž αžΆαž„ / αž€αŸ’αžšαž»αž˜ (Store)</th>
<th>αž§αž”αž€αžšαžŽαŸ (Zoom)</th>
<th>αž‘αŸ†αž αŸ†αž•αŸ’αž‘αž»αž€</th>
<th>αž’αžΆαžŸαž™αžŠαŸ’αž‹αžΆαž“ IP</th>
<th>αž‘αŸ†αž–αŸαžšαžŠαžΎαž˜ (URL)</th>
<th>αž€αŸ†αžŽαžαŸ‹αž…αŸ†αžŽαžΆαŸ†</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<div class="sidebar">
<div class="group-box">
<div class="group-title">αž‚αŸ’αžšαž”αŸ‹αž‚αŸ’αžšαž„αž αžΆαž„/αž€αŸ’αžšαž»αž˜ (STORES)</div>
<select id="store-select" onchange="filterRows()" style="margin-bottom: 6px;"></select>
<div style="display:flex; gap:4px;">
<button style="flex:1;" onclick="addStore()">οΌ‹ αž”αž„αŸ’αž€αžΎαž</button>
<button style="flex:1;" onclick="deleteStore()">πŸ—‘οΈ αž›αž»αž”</button>
</div>
</div>
<div class="group-box">
<div class="group-title">αž‚αŸ’αžšαž”αŸ‹αž‚αŸ’αžšαž„αž›αžΈαž„/αžœαž·αž”αžŸαžΆαž™ (URLS)</div>
<select id="url-select" style="margin-bottom: 6px;"></select>
<div style="display:flex; gap:4px;">
<button style="flex:1;" onclick="addUrl()">οΌ‹ αž”αž“αŸ’αžαŸ‚αž˜</button>
<button style="flex:1;" onclick="deleteUrl()">πŸ—‘οΈ αž›αž»αž”</button>
</div>
</div>
<div class="group-box">
<div class="group-title">αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αž”αŸ’αžšαž–αŸαž“αŸ’αž’ (SYSTEM CONFIG)</div>
<label style="display:flex; align-items:center; gap:6px; margin-bottom: 6px;">
<input type="checkbox" id="cfg-auto-scroll" onchange="toggleScrollOption(this)"> αžŸαŸ’αžšαžΌαž›αž’αžΌαžαžΌαž–αŸαž›αž”αžΎαž€
</label>
<div style="margin-bottom:6px;">
<span>αž…αž“αŸ’αž›αŸ„αŸ‡αž–αŸαž›αž”αžΎαž€ (αžœαž·αž“αžΆαž‘αžΈ):</span>
<input type="number" id="cfg-delay" onchange="saveConfig()">
</div>
<div style="margin-bottom:6px;">
<span>αž€αŸ†αžŽαžαŸ‹αž“αžΆαž‘αžΈαžšαžαŸ‹ (0=∞):</span>
<input type="number" id="cfg-duration" onchange="saveConfig()">
</div>
<div style="margin-bottom:6px;">
<span>αžšαž”αŸ€αž”αžšαŸ€αž”αž…αŸ†:</span>
<select id="cfg-arrange-type" onchange="saveConfig()">
<option value="0">αžšαŸ€αž”αž‡αžΆαž‡αž½αžšαžˆαžš (Columns)</option>
<option value="1">αžšαŸ€αž”αž‡αžΆαž‡αž½αžšαžŠαŸαž€ (Rows)</option>
</select>
</div>
<div style="margin-bottom:6px;">
<span>αž…αŸ†αž“αž½αž“αž‡αž½αžš (Grid Count):</span>
<input type="number" id="cfg-cols" onchange="saveConfig()">
</div>
<button style="width:100%;" class="btn-success" onclick="arrangeWindows()">πŸ—‚ αžšαŸ€αž”αž…αŸ†αž’αŸαž€αŸ’αžšαž„αŸ‹αžŸαŸ’αžœαŸαž™αž”αŸ’αžšαžœαžαŸ’αž</button>
</div>
</div>
</div>
<div class="footer-bar">
<span id="lbl-status">αžαŸ’αžšαŸ€αž˜αžαŸ’αž›αž½αž“αžšαž½αž…αžšαžΆαž›αŸ‹ (Ready)</span>
<span id="lbl-resources">πŸ–₯️ CPU: --% | πŸ’Ύ RAM: --%</span>
</div>
<!-- Active Scroll Setup Modal -->
<div class="modal" id="scroll-modal">
<div class="modal-content">
<h3 style="color:#38bdf8; margin:0;">αž€αžΆαžšαž€αŸ†αžŽαžαŸ‹αž€αžΆαžšαžŸαŸ’αžšαžΌαž›αžŸαŸ’αžœαŸαž™αž”αŸ’αžšαžœαžαŸ’αž</h3>
<label>αž›αŸ’αž”αžΏαž“αžŸαŸ’αžšαžΌαž› (1-10):</label>
<input type="number" id="scroll-speed" min="1" max="10" value="2">
<label>αžšαž”αŸ€αž”αžŸαŸ’αžšαžΌαž›αž’αŸαž€αŸ’αžšαž„αŸ‹:</label>
<select id="scroll-behavior">
<option value="0">αžŸαŸ’αžšαžΌαž›αž…αž»αŸ‡αžαŸ’αžšαž„αŸ‹ (Standard Down)</option>
<option value="1" selected>αžšαŸ‰αŸαž“αžŠαžΉαž˜αž”αŸ‚αž”αž˜αž“αž»αžŸαŸ’αžŸ (Human-Like β‡…)</option>
</select>
<div style="display:flex; gap:10px; margin-top:10px;">
<button class="btn-success" style="flex:1;" onclick="applyScrollSettings()">αžšαž€αŸ’αžŸαžΆαž‘αž»αž€</button>
<button class="btn-danger" style="flex:1;" onclick="closeScrollModal()">αž”αŸ„αŸ‡αž”αž„αŸ‹</button>
</div>
</div>
</div>
<script>
let timerVal = 0;
let stopwatchInterval = null;
function startStopwatch() {
if(stopwatchInterval) clearInterval(stopwatchInterval);
timerVal = 0;
stopwatchInterval = setInterval(() => {
timerVal++;
let h = String(Math.floor(timerVal/3600)).padStart(2,'0');
let m = String(Math.floor((timerVal%3600)/60)).padStart(2,'0');
let s = String(timerVal%60).padStart(2,'0');
document.getElementById("stopwatch").innerText = `${h}:${m}:${s}`;
}, 1000);
}
function stopStopwatch() {
clearInterval(stopwatchInterval);
document.getElementById("stopwatch").innerText = "00:00:00";
}
async function loadUI() {
let resStores = await fetch('/api/get_stores');
let listStores = await resStores.json();
let storeSelect = document.getElementById("store-select");
storeSelect.innerHTML = '<option value="All">αž‘αžΆαŸ†αž„αž’αžŸαŸ‹ (All)</option>';
listStores.forEach(s => storeSelect.innerHTML += `<option value="${s}">${s}</option>`);
let resUrls = await fetch('/api/get_urls');
let listUrls = await resUrls.json();
let urlSelect = document.getElementById("url-select");
urlSelect.innerHTML = '';
listUrls.forEach(u => urlSelect.innerHTML += `<option value="${u}">${u}</option>`);
let resConfig = await fetch('/api/get_config');
let cfg = await resConfig.json();
document.getElementById("cfg-auto-scroll").checked = cfg.chk_auto_scroll;
document.getElementById("cfg-delay").value = cfg.delay_spin;
document.getElementById("cfg-duration").value = cfg.auto_close_spin;
document.getElementById("cfg-arrange-type").value = cfg.arrange_type_idx;
document.getElementById("cfg-cols").value = cfg.cols_spin;
await loadTable();
}
async function loadTable() {
let checkedIds = Array.from(document.querySelectorAll(".row-check:checked")).map(cb => parseInt(cb.value));
let resInst = await fetch('/api/get_instances');
let data = await resInst.json();
document.getElementById("lbl-total").innerText = `αž‚αžŽαž“αžΈαžŸαžšαž»αž” (Total Profiles): ${data.length}`;
let tbody = document.getElementById("table-body");
tbody.innerHTML = "";
data.forEach(inst => {
let statusColor = "color: #ef4444;";
if(inst.status.includes("αžŸαŸ’αžšαžΌαž›")) statusColor = "color: #a855f7;";
else if(inst.status.includes("αž•αŸ’αž‘αž»αž€")) statusColor = "color: #eab308;";
else if(inst.status.includes("Online")) statusColor = "color: #10b981;";
let isChecked = checkedIds.includes(inst.id) ? "checked" : "";
tbody.innerHTML += `
<tr class="profile-row" data-id="${inst.id}" data-name="${inst.name.toLowerCase()}" data-store="${inst.store}">
<td><input type="checkbox" class="row-check" value="${inst.id}" ${isChecked}></td>
<td style="${statusColor} font-weight:bold;">${inst.status}</td>
<td><input type="text" value="${inst.name}" onchange="updateField(${inst.id}, 'name', this.value)"></td>
<td>
<select onchange="updateField(${inst.id}, 'store', this.value)">
${Array.from(document.getElementById("store-select").options)
.filter(o => o.value !== "All")
.map(o => `<option value="${o.value}" ${inst.store === o.value ? 'selected':''}>${o.value}</option>`).join('')}
</select>
</td>
<td>${inst.device} (${Math.round(inst.scale*100)}%)</td>
<td>${inst.size}</td>
<td><input type="text" value="${inst.ip_display}" onchange="updateField(${inst.id}, 'ip', this.value)" placeholder="IP/αž‘αžΈαž€αŸ’αžšαž»αž„"></td>
<td>
<select onchange="updateField(${inst.id}, 'url', this.value)">
${Array.from(document.getElementById("url-select").options)
.map(o => `<option value="${o.value}" ${inst.url === o.value ? 'selected':''}>${o.value}</option>`).join('')}
</select>
</td>
<td><input type="text" value="${inst.note}" onchange="updateField(${inst.id}, 'note', this.value)" placeholder="αž…αŸ†αžŽαžΆαŸ†..."></td>
</tr>
`;
});
filterRows();
}
async function updateField(id, field, value) {
await fetch('/api/update_field', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({id, field, value})
});
document.getElementById("lbl-status").innerText = "αž”αžΆαž“αžšαž€αŸ’αžŸαžΆαž‘αž»αž€αž‘αž·αž“αŸ’αž“αž“αŸαž™αŸ”";
}
function selectAll(master) {
document.querySelectorAll(".row-check").forEach(cb => cb.checked = master.checked);
}
function getSelected() {
return Array.from(document.querySelectorAll(".row-check:checked")).map(cb => parseInt(cb.value));
}
function filterRows() {
let searchVal = document.getElementById("search").value.toLowerCase();
let storeVal = document.getElementById("store-select").value;
document.querySelectorAll(".profile-row").forEach(row => {
let name = row.getAttribute("data-name");
let store = row.getAttribute("data-store");
let matchSearch = name.includes(searchVal);
let matchStore = (storeVal === "All" || store === storeVal);
row.style.display = (matchSearch && matchStore) ? "" : "none";
});
}
async function addInstance() {
await fetch('/api/add_instance', { method: 'POST' });
loadTable();
}
async function deleteSelected() {
let ids = getSelected();
if(!ids.length) return;
if(confirm(`αž…αž„αŸ‹αž›αž»αž”αž‚αžŽαž“αžΈαž‘αžΆαŸ†αž„ ${ids.length} αž“αŸαŸ‡αž˜αŸ‚αž“αž‘αŸ?`)) {
await fetch('/api/delete_instances', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ids})
});
loadTable();
}
}
async function startSelected() {
let ids = getSelected();
if(!ids.length) return alert("αžŸαžΌαž˜αž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸαž‚αžŽαž“αžΈ!");
startStopwatch();
await fetch('/api/start_profiles', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ids})
});
setTimeout(loadTable, 1000);
}
async function stopSelected() {
let ids = getSelected();
if(!ids.length) return alert("αžŸαžΌαž˜αž‡αŸ’αžšαžΎαžŸαžšαžΎαžŸαž‚αžŽαž“αžΈ!");
stopStopwatch();
await fetch('/api/stop_profiles', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ids})
});
setTimeout(loadTable, 1000);
}
function toggleScrollOption(chk) {
if(chk.checked) {
document.getElementById("scroll-modal").style.display = "flex";
} else {
saveConfig();
}
}
function closeScrollModal() {
document.getElementById("cfg-auto-scroll").checked = false;
document.getElementById("scroll-modal").style.display = "none";
saveConfig();
}
async function applyScrollSettings() {
let speed = parseInt(document.getElementById("scroll-speed").value);
let behavior = parseInt(document.getElementById("scroll-behavior").value);
await fetch('/api/save_scroll_preset', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({speed, behavior})
});
document.getElementById("scroll-modal").style.display = "none";
saveConfig();
}
async function saveConfig() {
let cfg = {
chk_auto_scroll: document.getElementById("cfg-auto-scroll").checked,
delay_spin: parseInt(document.getElementById("cfg-delay").value),
auto_close_spin: parseInt(document.getElementById("cfg-duration").value),
arrange_type_idx: parseInt(document.getElementById("cfg-arrange-type").value),
cols_spin: parseInt(document.getElementById("cfg-cols").value)
};
await fetch('/api/save_config', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(cfg)
});
}
async function addStore() {
let name = prompt("αž”αž‰αŸ’αž…αžΌαž›αžˆαŸ’αž˜αŸ„αŸ‡ Store αžαŸ’αž˜αžΈ:");
if(name && name.trim()) {
await fetch('/api/add_store', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: name.trim()})
});
loadUI();
}
}
async function deleteStore() {
let val = document.getElementById("store-select").value;
if(val === "All" || val === "αž‘αžΌαž‘αŸ… (Default)") return;
if(confirm(`αž›αž»αž” Store [${val}] αž˜αŸ‚αž“αž‘αŸ?`)) {
await fetch('/api/delete_store', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({name: val})
});
loadUI();
}
}
async function addUrl() {
let url = prompt("αž”αž‰αŸ’αž…αžΌαž› URL αžαŸ’αž˜αžΈ:");
if(url && url.trim()) {
await fetch('/api/add_url', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url: url.trim()})
});
loadUI();
}
}
async function deleteUrl() {
let val = document.getElementById("url-select").value;
if(!val) return;
if(confirm(`αž›αž»αž” URL [${val}] αž˜αŸ‚αž“αž‘αŸ?`)) {
await fetch('/api/delete_url', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({url: val})
});
loadUI();
}
}
async function arrangeWindows() {
await fetch('/api/arrange_windows', { method: 'POST' });
}
async function updateResources() {
try {
let res = await fetch('/api/get_resources');
let data = await res.json();
document.getElementById("lbl-resources").innerText = `πŸ–₯️ CPU: ${data.cpu}% | πŸ’Ύ RAM: ${data.ram}%`;
} catch {}
}
loadUI();
setInterval(updateResources, 2000);
setInterval(loadTable, 3000);
</script>
</body>
</html>
"""
# =========================================================
# αŸ₯. αž”αŸ’αžšαž–αŸαž“αŸ’αž’αž‘αŸ†αž“αžΆαž€αŸ‹αž‘αŸ†αž“αž„αžšαžœαžΆαž„ Web UI αž“αž·αž„ PyQt6 Threading (Coordinator)
# =========================================================
class Coordinator(QObject):
start_signal = pyqtSignal(int)
stop_signal = pyqtSignal(int)
arrange_signal = pyqtSignal()
def __init__(self):
super().__init__()
self.start_signal.connect(self.on_start)
self.stop_signal.connect(self.on_stop)
self.arrange_signal.connect(self.on_arrange)
def on_start(self, inst_id):
if inst_id not in running_windows:
inst_data = next((x for x in instances if x["id"] == inst_id), None)
if inst_data:
win = MobileSimulator(
instance_id=inst_id,
device_name=inst_data["device"],
initial_url=inst_data["url"],
scale_factor=inst_data.get("scale", 0.75)
)
win.show()
running_windows[inst_id] = win
if app_config.get("chk_auto_scroll", True):
win.toggle_auto_scroll(
force_state=True,
speed=app_config.get("preset_speed", 2),
duration_min=app_config.get("auto_close_spin", 0),
random_mode=(app_config.get("preset_mode_idx", 1) == 1),
auto_close=(app_config.get("auto_close_spin", 0) > 0)
)
def on_stop(self, inst_id):
if inst_id in running_windows:
running_windows[inst_id].close()
def on_arrange(self):
if not running_windows: return
arrange_type = "Columns" if app_config.get("arrange_type_idx", 0) == 0 else "Rows"
count_limit = app_config.get("cols_spin", 3)
margin_x, margin_y, spacing = 20, 50, 15
current_col, current_row = 0, 0
sorted_keys = sorted(running_windows.keys())
for idx in sorted_keys:
win = running_windows[idx]
x = margin_x + current_col * (win.width() + spacing)
y = margin_y + current_row * (win.height() + spacing)
win.move(x, y)
if "Columns" in arrange_type:
current_col += 1
if current_col >= count_limit:
current_col = 0
current_row += 1
else:
current_row += 1
if current_row >= count_limit:
current_row = 0
current_col += 1
coordinator = None
# =========================================================
# ៦. Web App API Server Handler
# =========================================================
class WebUIRequestHandler(BaseHTTPRequestHandler):
def log_message(self, format, *args): return
def do_GET(self):
global instances, stores, urls_db, app_config
if self.path == "/":
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.end_headers()
self.wfile.write(HTML_PANEL.encode("utf-8"))
elif self.path == "/api/get_stores":
self.send_json(stores)
elif self.path == "/api/get_urls":
self.send_json(urls_db)
elif self.path == "/api/get_config":
self.send_json(app_config)
elif self.path == "/api/get_resources":
cpu, ram = 0, 0
if psutil:
try:
cpu = psutil.cpu_percent()
ram = psutil.virtual_memory().percent
except Exception: pass
self.send_json({"cpu": cpu, "ram": ram})
elif self.path == "/api/get_instances":
res = []
for inst in instances:
iid = inst["id"]
status = instance_statuses.get(iid, "πŸ”΄ Offline")
size = calculated_sizes.get(iid, "Calculating...")
ip_city = inst.get("ip", "")
if ip_city and inst.get("city", ""):
ip_city = f"{inst['ip']} ({inst['city']})"
res.append({
"id": iid,
"name": inst.get("name", ""),
"store": inst.get("store", "αž‘αžΌαž‘αŸ… (Default)"),
"device": inst.get("device", ""),
"scale": inst.get("scale", 0.75),
"size": size,
"ip_display": ip_city,
"url": inst.get("url", DEFAULT_URL),
"note": inst.get("note", ""),
"status": status
})
self.send_json(res)
else:
self.send_error(404)
def do_POST(self):
global instances, stores, urls_db, app_config, coordinator
content_length = int(self.headers.get('Content-Length', 0))
post_data = self.rfile.read(content_length) if content_length > 0 else b""
req = json.loads(post_data.decode("utf-8")) if post_data else {}
if self.path == "/api/update_field":
iid, field, value = req["id"], req["field"], req["value"]
for inst in instances:
if inst["id"] == iid:
if field == "ip":
inst["ip"] = value
else:
inst[field] = value
break
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/add_instance":
new_id = max([x["id"] for x in instances]) + 1 if instances else 1
instances.append({
"id": new_id, "name": f"αž‚αžŽαž“αžΈ #{new_id}", "device": "Compact Mobile (360x500)",
"url": DEFAULT_URL, "scale": 0.50, "ip": "", "city": "", "note": "", "store": "αž‘αžΌαž‘αŸ… (Default)"
})
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/delete_instances":
ids = req["ids"]
def handle_remove_readonly(func, path, exc):
try:
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception: pass
for iid in ids:
coordinator.stop_signal.emit(iid)
inst_dir = os.path.abspath(f"./data/instance_{iid}")
if os.path.exists(inst_dir):
shutil.rmtree(inst_dir, onerror=handle_remove_readonly)
instances = [x for x in instances if x["id"] not in ids]
save_json(DB_PATH, instances)
gc.collect()
self.send_success()
elif self.path == "/api/add_store":
stores.append(req["name"])
save_json(STORES_PATH, stores)
self.send_success()
elif self.path == "/api/delete_store":
name = req["name"]
if name in stores:
stores.remove(name)
for inst in instances:
if inst.get("store") == name: inst["store"] = "αž‘αžΌαž‘αŸ… (Default)"
save_json(STORES_PATH, stores)
save_json(DB_PATH, instances)
self.send_success()
elif self.path == "/api/add_url":
new_url = req["url"]
if not new_url.startswith("http://") and not new_url.startswith("https://"):
new_url = "https://" + new_url
urls_db.append(new_url)
save_json(URLS_PATH, urls_db)
self.send_success()
elif self.path == "/api/delete_url":
if req["url"] in urls_db:
urls_db.remove(req["url"])
save_json(URLS_PATH, urls_db)
self.send_success()
elif self.path == "/api/save_scroll_preset":
app_config["preset_speed"] = req["speed"]
app_config["preset_mode_idx"] = req["behavior"]
save_json(CONFIG_PATH, app_config)
self.send_success()
elif self.path == "/api/save_config":
app_config.update(req)
save_json(CONFIG_PATH, app_config)
self.send_success()
elif self.path == "/api/arrange_windows":
coordinator.arrange_signal.emit()
self.send_success()
elif self.path == "/api/start_profiles":
ids = req["ids"]
delay = app_config.get("delay_spin", 3)
def run_queue():
for idx, iid in enumerate(ids):
if idx > 0 and delay > 0:
time.sleep(delay)
coordinator.start_signal.emit(iid)
time.sleep(1)
coordinator.arrange_signal.emit()
threading.Thread(target=run_queue, daemon=True).start()
self.send_success()
elif self.path == "/api/stop_profiles":
for iid in req["ids"]:
coordinator.stop_signal.emit(iid)
self.send_success()
else:
self.send_error(404)
def send_json(self, data):
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.end_headers()
self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8"))
def send_success(self):
self.send_json({"status": "success"})
def run_web_server():
server = ThreadingHTTPServer(("127.0.0.1", PORT), WebUIRequestHandler)
server.serve_forever()
# =========================================================
# ៧. αžαŸ’αž“αžΆαž€αŸ‹αž‚αžŽαž“αžΆαž‘αŸ†αž αŸ† Folder αž“αŸ…αžαžΆαž„αž€αŸ’αžšαŸ„αž™ (Background Thread)
# =========================================================
class SizeWorker(QThread):
def run(self):
while True:
for inst in instances:
iid = inst["id"]
path = os.path.abspath(f"./data/instance_{iid}")
calculated_sizes[iid] = format_size(get_dir_size(path))
time.sleep(5)
# =========================================================
# ៨. αž…αŸ†αžŽαž»αž…αž…αžΆαž”αŸ‹αž•αŸ’αžŠαžΎαž˜αž€αž˜αŸ’αž˜αžœαž·αž’αžΈ (Main Application Entry)
# =========================================================
if __name__ == "__main__":
app = QApplication(sys.argv)
coordinator = Coordinator()
web_thread = threading.Thread(target=run_web_server, daemon=True)
web_thread.start()
size_worker = SizeWorker()
size_worker.start()
webbrowser = __import__("webbrowser")
webbrowser.open(f"http://127.0.0.1:{PORT}")
sys.exit(app.exec())