Spaces:
Runtime error
Runtime error
| 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()) |