upgrate PyQt6
This commit is contained in:
parent
1aba2bf683
commit
671fadf645
6
app.py
6
app.py
|
|
@ -1,14 +1,16 @@
|
||||||
import sys
|
import sys
|
||||||
from PyQt5.QtWidgets import QApplication
|
from PyQt6.QtWidgets import QApplication
|
||||||
from gui.main_window import MainWindow
|
from gui.main_window import MainWindow
|
||||||
from database.db import create_tables
|
from database.db import create_tables
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
create_tables() # tạo bảng nếu chưa tồn tại
|
create_tables() # tạo bảng nếu chưa tồn tại
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
window = MainWindow()
|
window = MainWindow()
|
||||||
window.show()
|
window.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
||||||
Binary file not shown.
18
fb_window.py
18
fb_window.py
|
|
@ -2,8 +2,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
from PyQt6.QtCore import Qt, QUrl, QTimer
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
QMainWindow,
|
QMainWindow,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
|
|
@ -12,8 +12,8 @@ from PyQt5.QtWidgets import (
|
||||||
QLabel,
|
QLabel,
|
||||||
QDialog,
|
QDialog,
|
||||||
)
|
)
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
from PyQt5.QtGui import QPixmap, QImage
|
from PyQt6.QtGui import QPixmap, QImage
|
||||||
from services.action_service import ActionService # ✅ JS version (fake click/type)
|
from services.action_service import ActionService # ✅ JS version (fake click/type)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ class FBWindow(QMainWindow):
|
||||||
self.btn_detect.clicked.connect(self.detect_inputs_from_view)
|
self.btn_detect.clicked.connect(self.detect_inputs_from_view)
|
||||||
|
|
||||||
self.status = QLabel("Status: Ready")
|
self.status = QLabel("Status: Ready")
|
||||||
self.status.setAlignment(Qt.AlignLeft)
|
self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
layout.addWidget(self.web)
|
layout.addWidget(self.web)
|
||||||
|
|
@ -195,7 +195,7 @@ class FBWindow(QMainWindow):
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def show_preview(self, bgr_img):
|
def show_preview(self, bgr_img):
|
||||||
"""Hiển thị preview trong PyQt5"""
|
"""Hiển thị preview trong PyQt6"""
|
||||||
rgb = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
|
rgb = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB)
|
||||||
h, w = rgb.shape[:2]
|
h, w = rgb.shape[:2]
|
||||||
qimg = QImage(rgb.data, w, h, rgb.strides[0], QImage.Format.Format_RGB888)
|
qimg = QImage(rgb.data, w, h, rgb.strides[0], QImage.Format.Format_RGB888)
|
||||||
|
|
@ -205,13 +205,13 @@ class FBWindow(QMainWindow):
|
||||||
dlg.setWindowTitle("Detection Preview")
|
dlg.setWindowTitle("Detection Preview")
|
||||||
v_layout = QVBoxLayout(dlg)
|
v_layout = QVBoxLayout(dlg)
|
||||||
lbl = QLabel()
|
lbl = QLabel()
|
||||||
lbl.setPixmap(pix.scaled(800, 600, Qt.KeepAspectRatio))
|
lbl.setPixmap(pix.scaled(800, 600, Qt.AspectRatioMode.KeepAspectRatio))
|
||||||
v_layout.addWidget(lbl)
|
v_layout.addWidget(lbl)
|
||||||
dlg.exec_()
|
dlg.exec()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
win = FBWindow()
|
win = FBWindow()
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec())
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
# gui/core/login_handle_dialog.py
|
# gui/core/login_handle_dialog.py
|
||||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QPushButton, QApplication
|
||||||
from PyQt5.QtCore import QTimer
|
from PyQt6.QtCore import QTimer
|
||||||
from services.core.log_service import log_service
|
from services.core.log_service import log_service
|
||||||
from stores.shared_store import SharedStore
|
from stores.shared_store import SharedStore
|
||||||
from gui.global_signals import global_signals
|
from gui.global_signals import global_signals
|
||||||
|
|
@ -11,7 +11,7 @@ class LoginHandleDialog(QDialog):
|
||||||
dialog_height = 100
|
dialog_height = 100
|
||||||
margin = 10 # khoảng cách giữa các dialog
|
margin = 10 # khoảng cách giữa các dialog
|
||||||
|
|
||||||
# Lưu danh sách dialog đang mở (dùng class variable để chia sẻ giữa tất cả dialog)
|
# Lưu danh sách dialog đang mở (chia sẻ giữa tất cả dialog)
|
||||||
open_dialogs = []
|
open_dialogs = []
|
||||||
|
|
||||||
def __init__(self, account_id: int, listed_id: int):
|
def __init__(self, account_id: int, listed_id: int):
|
||||||
|
|
@ -30,11 +30,12 @@ class LoginHandleDialog(QDialog):
|
||||||
layout.addWidget(self.btn_finish)
|
layout.addWidget(self.btn_finish)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
# --- Tính vị trí để xếp dialog từ góc trái trên cùng sang phải theo hàng ngang ---
|
# --- Vị trí dialog ---
|
||||||
self.move_to_corner()
|
self.move_to_corner()
|
||||||
LoginHandleDialog.open_dialogs.append(self)
|
LoginHandleDialog.open_dialogs.append(self)
|
||||||
|
|
||||||
def move_to_corner(self):
|
def move_to_corner(self):
|
||||||
|
"""Tính vị trí để xếp dialog từ góc trái trên cùng sang phải theo hàng ngang"""
|
||||||
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
screen_geometry = QApplication.primaryScreen().availableGeometry()
|
||||||
start_x = screen_geometry.left() + self.margin
|
start_x = screen_geometry.left() + self.margin
|
||||||
start_y = screen_geometry.top() + self.margin
|
start_y = screen_geometry.top() + self.margin
|
||||||
|
|
@ -66,15 +67,25 @@ class LoginHandleDialog(QDialog):
|
||||||
try:
|
try:
|
||||||
store = SharedStore.get_instance()
|
store = SharedStore.get_instance()
|
||||||
store.remove(self.listed_id)
|
store.remove(self.listed_id)
|
||||||
log_service.info(f"[Dialog] Removed listed_id={self.listed_id} from SharedStore")
|
log_service.info(
|
||||||
|
f"[Dialog] Removed listed_id={self.listed_id} from SharedStore"
|
||||||
|
)
|
||||||
|
|
||||||
# Emit signal để MainWindow biết
|
# Emit signal để MainWindow biết
|
||||||
QTimer.singleShot(0, lambda: global_signals.dialog_finished.emit(self.account_id, self.listed_id))
|
QTimer.singleShot(
|
||||||
|
0,
|
||||||
|
lambda: global_signals.dialog_finished.emit(
|
||||||
|
self.account_id, self.listed_id
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# Close dialog an toàn
|
# Close dialog an toàn
|
||||||
self.hide()
|
self.hide()
|
||||||
self.deleteLater()
|
self.deleteLater()
|
||||||
if self in LoginHandleDialog.open_dialogs:
|
if self in LoginHandleDialog.open_dialogs:
|
||||||
LoginHandleDialog.open_dialogs.remove(self)
|
LoginHandleDialog.open_dialogs.remove(self)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log_service.error(f"[Dialog] Exception in finish_listing for listed_id={self.listed_id}: {e}")
|
log_service.error(
|
||||||
|
f"[Dialog] Exception in finish_listing for listed_id={self.listed_id}: {e}"
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,12 @@
|
||||||
# gui/global_signals.py
|
# gui/global_signals.py
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt6.QtCore import QObject, pyqtSignal
|
||||||
|
|
||||||
|
|
||||||
class GlobalSignals(QObject):
|
class GlobalSignals(QObject):
|
||||||
listed_finished = pyqtSignal()
|
listed_finished = pyqtSignal()
|
||||||
open_login_dialog = pyqtSignal(int, int) # account_id, listed_id
|
open_login_dialog = pyqtSignal(int, int) # account_id, listed_id
|
||||||
dialog_finished = pyqtSignal(int, int) # account_id, listed_id
|
dialog_finished = pyqtSignal(int, int) # account_id, listed_id
|
||||||
|
|
||||||
global_signals = GlobalSignals()
|
|
||||||
|
# Tạo instance toàn cục để các module khác có thể import dùng chung
|
||||||
|
global_signals = GlobalSignals()
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,19 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import cv2
|
import cv2
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from PyQt5.QtCore import Qt, QUrl, QTimer
|
from PyQt6.QtCore import Qt, QUrl, QTimer
|
||||||
from PyQt5.QtWidgets import QApplication, QMainWindow, QVBoxLayout, QWidget, QLabel, QTextEdit, QPushButton
|
from PyQt6.QtWidgets import (
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineProfile
|
QApplication,
|
||||||
from PyQt5.QtGui import QImage
|
QMainWindow,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
QLabel,
|
||||||
|
QTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
)
|
||||||
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
||||||
|
from PyQt6.QtWebEngineCore import QWebEngineProfile
|
||||||
|
from PyQt6.QtGui import QImage
|
||||||
|
|
||||||
from services.action_service import ActionService
|
from services.action_service import ActionService
|
||||||
from services.detect_service import DetectService
|
from services.detect_service import DetectService
|
||||||
|
|
@ -14,41 +23,47 @@ from config import TEMPLATE_DIR
|
||||||
|
|
||||||
|
|
||||||
class LoginFB(QMainWindow):
|
class LoginFB(QMainWindow):
|
||||||
|
"""Cửa sổ tự động đăng nhập Facebook bằng nhận diện hình ảnh."""
|
||||||
|
|
||||||
def __init__(self, account=None, delay=0.3):
|
def __init__(self, account=None, delay=0.3):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.account = account or {}
|
self.account = account or {}
|
||||||
self.template_dir = os.path.abspath(TEMPLATE_DIR)
|
|
||||||
self.delay = delay
|
self.delay = delay
|
||||||
|
self.template_dir = os.path.abspath(TEMPLATE_DIR)
|
||||||
|
|
||||||
# ✅ Lấy tên profile từ email hoặc username
|
# ✅ Lấy tên profile từ email hoặc username
|
||||||
self.profile_name = self.account.get("email") or self.account.get("username") or "default"
|
self.profile_name = (
|
||||||
|
self.account.get("email") or self.account.get("username") or "default"
|
||||||
|
)
|
||||||
|
|
||||||
# --- Detect services ---
|
# --- Detect services ---
|
||||||
self.detector = DetectService(
|
self.detector = DetectService(
|
||||||
template_dir=TEMPLATE_DIR,
|
template_dir=TEMPLATE_DIR, target_labels=["username", "password"]
|
||||||
target_labels=["username", "password"]
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- UI cơ bản ---
|
# --- UI setup ---
|
||||||
self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}")
|
self.setWindowTitle(f"FB Auto Vision Login - {self.profile_name}")
|
||||||
self.setFixedSize(480, 680)
|
self.setFixedSize(480, 680)
|
||||||
|
|
||||||
self.web = QWebEngineView()
|
self.web = QWebEngineView()
|
||||||
|
|
||||||
self.status = QLabel("Status: Ready")
|
self.status = QLabel("Status: Ready")
|
||||||
self.status.setAlignment(Qt.AlignLeft)
|
self.status.setAlignment(Qt.AlignmentFlag.AlignLeft)
|
||||||
self.status.setFixedHeight(20)
|
self.status.setFixedHeight(20)
|
||||||
|
|
||||||
|
# Log area
|
||||||
self.log_area = QTextEdit()
|
self.log_area = QTextEdit()
|
||||||
self.log_area.setReadOnly(True)
|
self.log_area.setReadOnly(True)
|
||||||
self.log_area.setFixedHeight(120)
|
self.log_area.setFixedHeight(120)
|
||||||
self.log_area.setStyleSheet("""
|
self.log_area.setStyleSheet(
|
||||||
|
"""
|
||||||
background-color: #1e1e1e;
|
background-color: #1e1e1e;
|
||||||
color: #dcdcdc;
|
color: #dcdcdc;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
font-family: Consolas, monospace;
|
font-family: Consolas, monospace;
|
||||||
""")
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
# Refresh button
|
||||||
self.btn_refresh = QPushButton("Refresh")
|
self.btn_refresh = QPushButton("Refresh")
|
||||||
self.btn_refresh.setFixedHeight(30)
|
self.btn_refresh.setFixedHeight(30)
|
||||||
self.btn_refresh.clicked.connect(self.refresh_page)
|
self.btn_refresh.clicked.connect(self.refresh_page)
|
||||||
|
|
@ -56,9 +71,14 @@ class LoginFB(QMainWindow):
|
||||||
# --- Profile ---
|
# --- Profile ---
|
||||||
self.profile_service = ProfileService()
|
self.profile_service = ProfileService()
|
||||||
profile_path = self.profile_service.get_profile_path(self.profile_name)
|
profile_path = self.profile_service.get_profile_path(self.profile_name)
|
||||||
|
os.makedirs(profile_path, exist_ok=True)
|
||||||
|
|
||||||
profile = self.web.page().profile()
|
profile = self.web.page().profile()
|
||||||
profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)
|
profile.setPersistentCookiesPolicy(
|
||||||
|
QWebEngineProfile.PersistentCookiesPolicy.ForcePersistentCookies
|
||||||
|
)
|
||||||
profile.setPersistentStoragePath(profile_path)
|
profile.setPersistentStoragePath(profile_path)
|
||||||
|
|
||||||
self.log(f"[INFO] Profile applied at: {profile_path}")
|
self.log(f"[INFO] Profile applied at: {profile_path}")
|
||||||
|
|
||||||
# --- Webview ---
|
# --- Webview ---
|
||||||
|
|
@ -83,15 +103,18 @@ class LoginFB(QMainWindow):
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def log(self, message: str):
|
def log(self, message: str):
|
||||||
|
"""Ghi log vào vùng log và console."""
|
||||||
self.log_area.append(message)
|
self.log_area.append(message)
|
||||||
print(message)
|
print(message)
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def capture_webview(self):
|
def capture_webview(self):
|
||||||
|
"""Chụp hình ảnh nội dung webview dưới dạng numpy array (BGR)."""
|
||||||
pixmap = self.web.grab()
|
pixmap = self.web.grab()
|
||||||
if pixmap.isNull():
|
if pixmap.isNull():
|
||||||
return None
|
return None
|
||||||
qimg = pixmap.toImage().convertToFormat(QImage.Format_RGBA8888)
|
|
||||||
|
qimg = pixmap.toImage().convertToFormat(QImage.Format.Format_RGBA8888)
|
||||||
width, height = qimg.width(), qimg.height()
|
width, height = qimg.width(), qimg.height()
|
||||||
ptr = qimg.bits()
|
ptr = qimg.bits()
|
||||||
ptr.setsize(height * width * 4)
|
ptr.setsize(height * width * 4)
|
||||||
|
|
@ -100,12 +123,13 @@ class LoginFB(QMainWindow):
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def on_web_loaded(self, ok=True):
|
def on_web_loaded(self, ok=True):
|
||||||
|
"""Khi trang web load xong."""
|
||||||
if not ok:
|
if not ok:
|
||||||
self.log("[ERROR] Page failed to load")
|
self.log("[ERROR] Page failed to load")
|
||||||
self.status.setText("Status: Page load failed")
|
self.status.setText("Status: Page load failed")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log("[INFO] Page loaded")
|
self.log("[INFO] Page loaded successfully")
|
||||||
self.status.setText("Status: Page loaded")
|
self.status.setText("Status: Page loaded")
|
||||||
|
|
||||||
# ✅ Lưu profile khi load xong
|
# ✅ Lưu profile khi load xong
|
||||||
|
|
@ -116,7 +140,7 @@ class LoginFB(QMainWindow):
|
||||||
screen = self.capture_webview()
|
screen = self.capture_webview()
|
||||||
if screen is None:
|
if screen is None:
|
||||||
self.status.setText("Status: Unable to capture webview")
|
self.status.setText("Status: Unable to capture webview")
|
||||||
self.log("Status: Unable to capture webview")
|
self.log("[WARN] Unable to capture webview")
|
||||||
return
|
return
|
||||||
|
|
||||||
self.log("[INFO] Detecting email/password fields...")
|
self.log("[INFO] Detecting email/password fields...")
|
||||||
|
|
@ -129,19 +153,25 @@ class LoginFB(QMainWindow):
|
||||||
|
|
||||||
self.status.setText(f"[INFO] Detected {len(regions)} valid regions")
|
self.status.setText(f"[INFO] Detected {len(regions)} valid regions")
|
||||||
self.log(f"[INFO] Detected {len(regions)} valid regions")
|
self.log(f"[INFO] Detected {len(regions)} valid regions")
|
||||||
|
|
||||||
|
# Chờ 500ms trước khi tự điền form
|
||||||
QTimer.singleShot(500, lambda: self.autofill_by_detection(regions))
|
QTimer.singleShot(500, lambda: self.autofill_by_detection(regions))
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def autofill_by_detection(self, regions):
|
def autofill_by_detection(self, regions):
|
||||||
|
"""Tự động điền email và password dựa vào vùng phát hiện."""
|
||||||
email = self.account.get("email", "")
|
email = self.account.get("email", "")
|
||||||
password = self.account.get("password", "")
|
password = self.account.get("password", "")
|
||||||
|
|
||||||
# sắp xếp để điền username trước, password sau
|
# sắp xếp: username trước, password sau
|
||||||
ordered = sorted(regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower()))
|
ordered = sorted(
|
||||||
|
regions, key=lambda r: ("pass" in r[0].lower(), "user" not in r[0].lower())
|
||||||
|
)
|
||||||
|
|
||||||
def do_action(i=0):
|
def do_action(i=0):
|
||||||
if i >= len(ordered):
|
if i >= len(ordered):
|
||||||
return
|
return
|
||||||
|
|
||||||
folder_name, filename, top_left, bottom_right, score = ordered[i]
|
folder_name, filename, top_left, bottom_right, score = ordered[i]
|
||||||
label = folder_name.lower()
|
label = folder_name.lower()
|
||||||
|
|
||||||
|
|
@ -161,13 +191,15 @@ class LoginFB(QMainWindow):
|
||||||
|
|
||||||
# ----------------------------------------------------
|
# ----------------------------------------------------
|
||||||
def refresh_page(self):
|
def refresh_page(self):
|
||||||
|
"""Tải lại trang."""
|
||||||
self.log("[INFO] Refreshing page...")
|
self.log("[INFO] Refreshing page...")
|
||||||
self.web.reload()
|
self.web.reload()
|
||||||
|
|
||||||
|
|
||||||
|
# ----------------------------------------------------
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
fake_account = {"email": "test@example.com", "password": "123456"}
|
fake_account = {"email": "test@example.com", "password": "123456"}
|
||||||
win = LoginFB(account=fake_account, delay=0.5)
|
win = LoginFB(account=fake_account, delay=0.5)
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec_())
|
sys.exit(app.exec())
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from PyQt5.QtWidgets import QMainWindow, QTabWidget, QMessageBox
|
from PyQt6.QtWidgets import QMainWindow, QTabWidget, QMessageBox
|
||||||
from gui.tabs.accounts.account_tab import AccountTab
|
from gui.tabs.accounts.account_tab import AccountTab
|
||||||
from gui.tabs.products.product_tab import ProductTab
|
from gui.tabs.products.product_tab import ProductTab
|
||||||
from gui.tabs.import_tab import ImportTab
|
from gui.tabs.import_tab import ImportTab
|
||||||
|
|
@ -34,51 +34,62 @@ class MainWindow(QMainWindow):
|
||||||
self.setCentralWidget(self.tabs)
|
self.setCentralWidget(self.tabs)
|
||||||
self.on_tab_changed(0)
|
self.on_tab_changed(0)
|
||||||
|
|
||||||
# # --- Signals ---
|
# --- Signals ---
|
||||||
# global_signals.listed_finished.connect(self.on_listed_task_finished)
|
# global_signals.listed_finished.connect(self.on_listed_task_finished)
|
||||||
# global_signals.dialog_finished.connect(self.on_dialog_finished)
|
# global_signals.dialog_finished.connect(self.on_dialog_finished)
|
||||||
# global_signals.open_login_dialog.connect(self.show_login_dialog)
|
# global_signals.open_login_dialog.connect(self.show_login_dialog)
|
||||||
|
|
||||||
# # --- Start background ---
|
# --- Background tasks ---
|
||||||
# start_background_listed()
|
# start_background_listed()
|
||||||
|
|
||||||
# --- Store opened dialogs ---
|
# --- Store opened dialogs ---
|
||||||
self.login_dialogs = []
|
self.login_dialogs = []
|
||||||
|
|
||||||
def show_login_dialog(self, account_id, listed_id):
|
# ----------------------------------------------------------------------
|
||||||
print(account_id, listed_id)
|
def show_login_dialog(self, account_id: int, listed_id: int):
|
||||||
|
"""Hiển thị dialog xử lý login (modeless)"""
|
||||||
|
print(f"[INFO] Open LoginHandleDialog for acc={account_id}, listed={listed_id}")
|
||||||
dialog = LoginHandleDialog(account_id=account_id, listed_id=listed_id)
|
dialog = LoginHandleDialog(account_id=account_id, listed_id=listed_id)
|
||||||
dialog.show() # modeless
|
dialog.setParent(self) # đảm bảo dialog đóng theo MainWindow
|
||||||
|
dialog.show()
|
||||||
self.login_dialogs.append(dialog)
|
self.login_dialogs.append(dialog)
|
||||||
|
|
||||||
# Khi dialog đóng, remove khỏi list và SharedStore
|
# Cleanup khi dialog đóng
|
||||||
def on_dialog_close():
|
dialog.finished.connect(
|
||||||
if dialog in self.login_dialogs:
|
lambda: self._on_dialog_closed(dialog, account_id, listed_id)
|
||||||
self.login_dialogs.remove(dialog)
|
)
|
||||||
self.on_dialog_finished(account_id, listed_id)
|
|
||||||
|
|
||||||
dialog.finished.connect(on_dialog_close)
|
# ----------------------------------------------------------------------
|
||||||
|
def _on_dialog_closed(self, dialog, account_id, listed_id):
|
||||||
|
"""Xử lý khi dialog đóng"""
|
||||||
|
if dialog in self.login_dialogs:
|
||||||
|
self.login_dialogs.remove(dialog)
|
||||||
|
self.on_dialog_finished(account_id, listed_id)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
def on_dialog_finished(self, account_id, listed_id):
|
def on_dialog_finished(self, account_id, listed_id):
|
||||||
"""Dialog xong, remove khỏi store"""
|
"""Khi dialog xử lý xong"""
|
||||||
store = SharedStore.get_instance()
|
store = SharedStore.get_instance()
|
||||||
store.remove(listed_id)
|
store.remove(listed_id)
|
||||||
|
|
||||||
def on_tab_changed(self, index):
|
# ----------------------------------------------------------------------
|
||||||
|
def on_tab_changed(self, index: int):
|
||||||
|
"""Lazy-load dữ liệu khi đổi tab"""
|
||||||
tab = self.tabs.widget(index)
|
tab = self.tabs.widget(index)
|
||||||
if hasattr(tab, "load_data") and not getattr(tab, "is_loaded", False):
|
if hasattr(tab, "load_data") and not getattr(tab, "is_loaded", False):
|
||||||
tab.load_data()
|
tab.load_data()
|
||||||
tab.is_loaded = True
|
tab.is_loaded = True
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
def on_listed_task_finished(self):
|
def on_listed_task_finished(self):
|
||||||
|
"""Khi task nền hoàn tất"""
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self, "Auto Listing", "All pending listed items have been processed!"
|
||||||
"Auto Listing",
|
|
||||||
"All pending listed items have been processed!"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
def closeEvent(self, event):
|
def closeEvent(self, event):
|
||||||
"""Khi MainWindow đóng, đóng hết tất cả dialog đang mở"""
|
"""Khi đóng MainWindow -> đóng hết dialog con"""
|
||||||
for dialog in self.login_dialogs[:]: # copy list để tránh lỗi khi remove
|
for dialog in self.login_dialogs[:]: # copy để tránh modify khi lặp
|
||||||
dialog.close()
|
dialog.close()
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
import os
|
import os
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QWidget,
|
||||||
QPushButton, QHBoxLayout, QDialog, QLabel, QLineEdit, QComboBox, QMessageBox,
|
QVBoxLayout,
|
||||||
QMenu, QAction, QSizePolicy
|
QTableWidget,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QPushButton,
|
||||||
|
QHBoxLayout,
|
||||||
|
QMessageBox,
|
||||||
|
QMenu,
|
||||||
|
QSizePolicy,
|
||||||
|
QHeaderView,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtGui import QAction, QFont
|
||||||
from PyQt5.QtGui import QFont
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt5.QtWidgets import QHeaderView
|
from PyQt6.QtGui import QFont
|
||||||
from database.models import Account
|
from database.models import Account
|
||||||
from .forms.account_form import AccountForm
|
from .forms.account_form import AccountForm
|
||||||
from config import PROFILES_DIR
|
from config import PROFILES_DIR
|
||||||
|
|
||||||
# 👇 import cửa sổ login FB
|
# 👇 import cửa sổ login FB
|
||||||
from gui.handle.login_fb import LoginFB # chỉnh path này theo project của bạn
|
from gui.handle.login_fb import LoginFB # chỉnh path này theo project của bạn
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
class AccountTab(QWidget):
|
class AccountTab(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -31,7 +39,9 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
# 🆕 Action menu
|
# 🆕 Action menu
|
||||||
self.options_btn = QPushButton("Action")
|
self.options_btn = QPushButton("Action")
|
||||||
self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
self.options_btn.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed
|
||||||
|
)
|
||||||
self.options_btn.setMinimumWidth(80)
|
self.options_btn.setMinimumWidth(80)
|
||||||
self.options_btn.setMaximumWidth(120)
|
self.options_btn.setMaximumWidth(120)
|
||||||
top_layout.addWidget(self.options_btn)
|
top_layout.addWidget(self.options_btn)
|
||||||
|
|
@ -40,7 +50,9 @@ class AccountTab(QWidget):
|
||||||
# Table
|
# Table
|
||||||
self.table = QTableWidget()
|
self.table = QTableWidget()
|
||||||
self.table.verticalHeader().setDefaultSectionSize(28) # row gọn
|
self.table.verticalHeader().setDefaultSectionSize(28) # row gọn
|
||||||
self.table.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
self.table.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding
|
||||||
|
)
|
||||||
layout.addWidget(self.table)
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
|
|
@ -68,16 +80,22 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
self.table.setRowCount(len(page_items))
|
self.table.setRowCount(len(page_items))
|
||||||
self.table.setColumnCount(6)
|
self.table.setColumnCount(6)
|
||||||
self.table.setHorizontalHeaderLabels([
|
self.table.setHorizontalHeaderLabels(
|
||||||
"ID", "Email", "Status", "Profile Exists", "Login At", "Actions"
|
["ID", "Email", "Status", "Profile Exists", "Login At", "Actions"]
|
||||||
])
|
)
|
||||||
|
|
||||||
for i, acc in enumerate(page_items):
|
for i, acc in enumerate(page_items):
|
||||||
acc_dict = {k: acc[k] for k in acc.keys()}
|
acc_dict = {k: acc[k] for k in acc.keys()}
|
||||||
|
|
||||||
self.table.setItem(i, 0, QTableWidgetItem(str(acc_dict["id"])))
|
self.table.setItem(i, 0, QTableWidgetItem(str(acc_dict["id"])))
|
||||||
self.table.setItem(i, 1, QTableWidgetItem(acc_dict["email"]))
|
self.table.setItem(i, 1, QTableWidgetItem(acc_dict["email"]))
|
||||||
self.table.setItem(i, 2, QTableWidgetItem("Active" if acc_dict["is_active"] == 1 else "Inactive"))
|
self.table.setItem(
|
||||||
|
i,
|
||||||
|
2,
|
||||||
|
QTableWidgetItem(
|
||||||
|
"Active" if acc_dict["is_active"] == 1 else "Inactive"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
# ✅ Check profile folder
|
# ✅ Check profile folder
|
||||||
folder_name = acc_dict["email"]
|
folder_name = acc_dict["email"]
|
||||||
|
|
@ -95,7 +113,9 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
# 🆕 Login action
|
# 🆕 Login action
|
||||||
action_login = QAction("Login", btn_menu)
|
action_login = QAction("Login", btn_menu)
|
||||||
action_login.triggered.connect(lambda _, a=acc_dict: self.open_login_window(a))
|
action_login.triggered.connect(
|
||||||
|
lambda _, a=acc_dict: self.open_login_window(a)
|
||||||
|
)
|
||||||
menu.addAction(action_login)
|
menu.addAction(action_login)
|
||||||
|
|
||||||
# Edit
|
# Edit
|
||||||
|
|
@ -105,7 +125,9 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
# Delete
|
# Delete
|
||||||
action_delete = QAction("Delete", btn_menu)
|
action_delete = QAction("Delete", btn_menu)
|
||||||
action_delete.triggered.connect(lambda _, a=acc_dict: self.delete_account(a))
|
action_delete.triggered.connect(
|
||||||
|
lambda _, a=acc_dict: self.delete_account(a)
|
||||||
|
)
|
||||||
menu.addAction(action_delete)
|
menu.addAction(action_delete)
|
||||||
|
|
||||||
btn_menu.setMenu(menu)
|
btn_menu.setMenu(menu)
|
||||||
|
|
@ -113,12 +135,12 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
# Column sizing
|
# Column sizing
|
||||||
header = self.table.horizontalHeader()
|
header = self.table.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(1, QHeaderView.Stretch)
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(2, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(3, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(4, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(4, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(5, QHeaderView.Fixed)
|
header.setSectionResizeMode(5, QHeaderView.ResizeMode.Fixed)
|
||||||
self.table.setColumnWidth(5, 100)
|
self.table.setColumnWidth(5, 100)
|
||||||
|
|
||||||
self.prev_btn.setEnabled(self.current_page > 0)
|
self.prev_btn.setEnabled(self.current_page > 0)
|
||||||
|
|
@ -134,21 +156,23 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
def add_account(self):
|
def add_account(self):
|
||||||
form = AccountForm(self)
|
form = AccountForm(self)
|
||||||
if form.exec_():
|
if form.exec():
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def edit_account(self, account):
|
def edit_account(self, account):
|
||||||
form = AccountForm(self, account)
|
form = AccountForm(self, account)
|
||||||
if form.exec_():
|
if form.exec():
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def delete_account(self, account):
|
def delete_account(self, account):
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm", f"Delete account {account['email']}?",
|
self,
|
||||||
QMessageBox.Yes | QMessageBox.No
|
"Confirm",
|
||||||
|
f"Delete account {account['email']}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm == QMessageBox.Yes:
|
if confirm == QMessageBox.StandardButton.Yes:
|
||||||
Account.delete(account["id"])
|
Account.delete(account["id"])
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
|
|
@ -162,5 +186,7 @@ class AccountTab(QWidget):
|
||||||
|
|
||||||
# 🆕 Mở cửa sổ login FB
|
# 🆕 Mở cửa sổ login FB
|
||||||
def open_login_window(self, account):
|
def open_login_window(self, account):
|
||||||
self.login_window = LoginFB(account) # truyền account vào nếu class LoginFB có nhận
|
self.login_window = LoginFB(
|
||||||
|
account
|
||||||
|
) # truyền account vào nếu class LoginFB có nhận
|
||||||
self.login_window.show()
|
self.login_window.show()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,18 @@
|
||||||
from PyQt5.QtWidgets import (
|
# gui/dialogs/account_form.py
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QComboBox,
|
from PyQt6.QtWidgets import (
|
||||||
QPushButton, QMessageBox
|
QDialog,
|
||||||
|
QVBoxLayout,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QComboBox,
|
||||||
|
QPushButton,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
from PyQt6.QtCore import Qt
|
||||||
from database.models.account import Account
|
from database.models.account import Account
|
||||||
|
|
||||||
|
|
||||||
class AccountForm(QDialog):
|
class AccountForm(QDialog):
|
||||||
def __init__(self, parent=None, account=None):
|
def __init__(self, parent=None, account=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -23,7 +32,7 @@ class AccountForm(QDialog):
|
||||||
layout.addWidget(QLabel("Password"))
|
layout.addWidget(QLabel("Password"))
|
||||||
pw_layout = QHBoxLayout()
|
pw_layout = QHBoxLayout()
|
||||||
self.password_input = QLineEdit()
|
self.password_input = QLineEdit()
|
||||||
self.password_input.setEchoMode(QLineEdit.Password)
|
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.password_input.setMinimumWidth(200)
|
self.password_input.setMinimumWidth(200)
|
||||||
|
|
||||||
self.toggle_btn = QPushButton("Show")
|
self.toggle_btn = QPushButton("Show")
|
||||||
|
|
@ -50,7 +59,7 @@ class AccountForm(QDialog):
|
||||||
|
|
||||||
self.cancel_btn = QPushButton("Cancel")
|
self.cancel_btn = QPushButton("Cancel")
|
||||||
self.cancel_btn.setMinimumWidth(80)
|
self.cancel_btn.setMinimumWidth(80)
|
||||||
self.cancel_btn.clicked.connect(self.close)
|
self.cancel_btn.clicked.connect(self.reject)
|
||||||
btn_layout.addWidget(self.cancel_btn)
|
btn_layout.addWidget(self.cancel_btn)
|
||||||
|
|
||||||
layout.addLayout(btn_layout)
|
layout.addLayout(btn_layout)
|
||||||
|
|
@ -60,21 +69,29 @@ class AccountForm(QDialog):
|
||||||
if account:
|
if account:
|
||||||
self.email_input.setText(account.get("email", ""))
|
self.email_input.setText(account.get("email", ""))
|
||||||
self.password_input.setText(account.get("password", ""))
|
self.password_input.setText(account.get("password", ""))
|
||||||
self.active_input.setCurrentText("Active" if account.get("is_active", 1) == 1 else "Inactive")
|
self.active_input.setCurrentText(
|
||||||
|
"Active" if account.get("is_active", 1) == 1 else "Inactive"
|
||||||
|
)
|
||||||
|
|
||||||
def toggle_password(self):
|
def toggle_password(self):
|
||||||
"""Ẩn/hiện password khi nhấn nút"""
|
"""Ẩn/hiện password khi nhấn nút"""
|
||||||
if self.toggle_btn.isChecked():
|
if self.toggle_btn.isChecked():
|
||||||
self.password_input.setEchoMode(QLineEdit.Normal)
|
self.password_input.setEchoMode(QLineEdit.EchoMode.Normal)
|
||||||
self.toggle_btn.setText("Hide")
|
self.toggle_btn.setText("Hide")
|
||||||
else:
|
else:
|
||||||
self.password_input.setEchoMode(QLineEdit.Password)
|
self.password_input.setEchoMode(QLineEdit.EchoMode.Password)
|
||||||
self.toggle_btn.setText("Show")
|
self.toggle_btn.setText("Show")
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
email = self.email_input.text()
|
"""Lưu hoặc cập nhật account"""
|
||||||
password = self.password_input.text()
|
email = self.email_input.text().strip()
|
||||||
|
password = self.password_input.text().strip()
|
||||||
is_active = 1 if self.active_input.currentText() == "Active" else 0
|
is_active = 1 if self.active_input.currentText() == "Active" else 0
|
||||||
|
|
||||||
|
if not email or not password:
|
||||||
|
QMessageBox.warning(self, "Error", "Email và Password không được để trống.")
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.account and "id" in self.account:
|
if self.account and "id" in self.account:
|
||||||
Account.update(self.account["id"], email, password, is_active)
|
Account.update(self.account["id"], email, password, is_active)
|
||||||
|
|
@ -82,4 +99,4 @@ class AccountForm(QDialog):
|
||||||
Account.create(email, password, is_active)
|
Account.create(email, password, is_active)
|
||||||
self.accept()
|
self.accept()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.warning(self, "Error", str(e))
|
QMessageBox.critical(self, "Error", f"Failed to save account:\n{e}")
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,15 @@ import time
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from database.models.product import Product
|
from database.models.product import Product
|
||||||
|
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QPushButton, QFileDialog,
|
QWidget,
|
||||||
QTableWidget, QTableWidgetItem, QHBoxLayout, QMessageBox
|
QVBoxLayout,
|
||||||
|
QPushButton,
|
||||||
|
QFileDialog,
|
||||||
|
QTableWidget,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QHBoxLayout,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -40,13 +46,15 @@ class ImportTab(QWidget):
|
||||||
self.preview_data = [] # store imported data
|
self.preview_data = [] # store imported data
|
||||||
|
|
||||||
def import_csv(self):
|
def import_csv(self):
|
||||||
file_path, _ = QFileDialog.getOpenFileName(self, "Select CSV File", "", "CSV Files (*.csv)")
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
|
self, "Select CSV File", "", "CSV Files (*.csv)"
|
||||||
|
)
|
||||||
if not file_path:
|
if not file_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# đọc CSV chuẩn với DictReader
|
# đọc CSV chuẩn với DictReader
|
||||||
with open(file_path, newline='', encoding="utf-8-sig") as csvfile:
|
with open(file_path, newline="", encoding="utf-8-sig") as csvfile:
|
||||||
reader = csv.DictReader(csvfile)
|
reader = csv.DictReader(csvfile)
|
||||||
headers = reader.fieldnames
|
headers = reader.fieldnames
|
||||||
rows = list(reader)
|
rows = list(reader)
|
||||||
|
|
@ -75,7 +83,9 @@ class ImportTab(QWidget):
|
||||||
QMessageBox.critical(self, "Error", f"Failed to read CSV: {e}")
|
QMessageBox.critical(self, "Error", f"Failed to read CSV: {e}")
|
||||||
|
|
||||||
def import_api(self):
|
def import_api(self):
|
||||||
QMessageBox.information(self, "Info", "API import feature will be developed later 😉")
|
QMessageBox.information(
|
||||||
|
self, "Info", "API import feature will be developed later 😉"
|
||||||
|
)
|
||||||
|
|
||||||
def save_to_db(self):
|
def save_to_db(self):
|
||||||
if not self.preview_data:
|
if not self.preview_data:
|
||||||
|
|
@ -87,14 +97,13 @@ class ImportTab(QWidget):
|
||||||
self,
|
self,
|
||||||
"Confirm Import",
|
"Confirm Import",
|
||||||
f"Are you sure you want to import {len(self.preview_data)} rows?",
|
f"Are you sure you want to import {len(self.preview_data)} rows?",
|
||||||
QMessageBox.Yes | QMessageBox.No
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if reply != QMessageBox.Yes:
|
if reply != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
def handler(item):
|
def handler(item):
|
||||||
try:
|
try:
|
||||||
# time.sleep(0.05) # có thể bỏ nếu không cần debug progress
|
|
||||||
Product.insert_from_import(item)
|
Product.insert_from_import(item)
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -102,16 +111,13 @@ class ImportTab(QWidget):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
success, fail = run_with_progress(
|
success, fail = run_with_progress(
|
||||||
self.preview_data,
|
self.preview_data, handler=handler, message="Importing data...", parent=self
|
||||||
handler=handler,
|
|
||||||
message="Importing data...",
|
|
||||||
parent=self
|
|
||||||
)
|
)
|
||||||
|
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self,
|
self,
|
||||||
"Import Completed",
|
"Import Completed",
|
||||||
f"Successfully imported {success}/{len(self.preview_data)} rows.\nFailed: {fail} rows."
|
f"Successfully imported {success}/{len(self.preview_data)} rows.\nFailed: {fail} rows.",
|
||||||
)
|
)
|
||||||
|
|
||||||
# ✅ Clear preview sau khi import xong
|
# ✅ Clear preview sau khi import xong
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QLineEdit, QDialogButtonBox,
|
QDialog,
|
||||||
QVBoxLayout, QWidget, QComboBox
|
QFormLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
|
QComboBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class ListedFilterDialog(QDialog):
|
class ListedFilterDialog(QDialog):
|
||||||
STATUS_OPTIONS = ["Any", "pending", "listed"] # ✅ thêm 'Any' để không lọc theo trạng thái
|
STATUS_OPTIONS = ["Any", "pending", "listed"]
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -27,15 +33,17 @@ class ListedFilterDialog(QDialog):
|
||||||
self.account_input = QLineEdit()
|
self.account_input = QLineEdit()
|
||||||
form_layout.addRow("Account Email:", self.account_input)
|
form_layout.addRow("Account Email:", self.account_input)
|
||||||
|
|
||||||
# Status (dropdown)
|
# Status
|
||||||
self.status_input = QComboBox()
|
self.status_input = QComboBox()
|
||||||
self.status_input.addItems(self.STATUS_OPTIONS)
|
self.status_input.addItems(self.STATUS_OPTIONS)
|
||||||
form_layout.addRow("Status:", self.status_input)
|
form_layout.addRow("Status:", self.status_input)
|
||||||
|
|
||||||
main_layout.addWidget(form_widget)
|
main_layout.addWidget(form_widget)
|
||||||
|
|
||||||
# Buttons
|
# Buttons (PyQt6 khác cú pháp một chút)
|
||||||
self.btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
self.btn_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
self.btn_box.accepted.connect(self.accept)
|
self.btn_box.accepted.connect(self.accept)
|
||||||
self.btn_box.rejected.connect(self.reject)
|
self.btn_box.rejected.connect(self.reject)
|
||||||
main_layout.addWidget(self.btn_box)
|
main_layout.addWidget(self.btn_box)
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
|
# gui/tabs/listeds/listed_tab.py
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import json
|
import json
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QWidget,
|
||||||
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView,
|
QVBoxLayout,
|
||||||
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton,
|
QTableWidget,
|
||||||
QStyle, QStylePainter, QLineEdit, QProgressBar
|
QTableWidgetItem,
|
||||||
|
QPushButton,
|
||||||
|
QHBoxLayout,
|
||||||
|
QMenu,
|
||||||
|
QHeaderView,
|
||||||
|
QLabel,
|
||||||
|
QCheckBox,
|
||||||
|
QSizePolicy,
|
||||||
|
QMessageBox,
|
||||||
|
QStyleOptionButton,
|
||||||
|
QStyle,
|
||||||
|
QStylePainter,
|
||||||
|
QLineEdit,
|
||||||
|
QProgressBar,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QRect, pyqtSignal, QTimer
|
from PyQt6.QtGui import QAction
|
||||||
|
from PyQt6.QtCore import Qt, QRect, pyqtSignal, QTimer
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from services.image_service import ImageService
|
from services.image_service import ImageService
|
||||||
from database.models.listed import Listed
|
from database.models.listed import Listed
|
||||||
|
|
@ -15,6 +30,7 @@ from gui.tabs.listeds.dialogs.listed_filter_dialog import ListedFilterDialog
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
# --- Header Checkbox ---
|
# --- Header Checkbox ---
|
||||||
class CheckBoxHeader(QHeaderView):
|
class CheckBoxHeader(QHeaderView):
|
||||||
select_all_changed = pyqtSignal(bool)
|
select_all_changed = pyqtSignal(bool)
|
||||||
|
|
@ -33,9 +49,11 @@ class CheckBoxHeader(QHeaderView):
|
||||||
x = rect.x() + (rect.width() - size) // 2
|
x = rect.x() + (rect.width() - size) // 2
|
||||||
y = rect.y() + (rect.height() - size) // 2
|
y = rect.y() + (rect.height() - size) // 2
|
||||||
option.rect = QRect(x, y, size, size)
|
option.rect = QRect(x, y, size, size)
|
||||||
option.state = QStyle.State_Enabled | (QStyle.State_On if self.isOn else QStyle.State_Off)
|
option.state = QStyle.StateFlag.State_Enabled | (
|
||||||
|
QStyle.StateFlag.State_On if self.isOn else QStyle.StateFlag.State_Off
|
||||||
|
)
|
||||||
painter2 = QStylePainter(self.viewport())
|
painter2 = QStylePainter(self.viewport())
|
||||||
painter2.drawControl(QStyle.CE_CheckBox, option)
|
painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option)
|
||||||
|
|
||||||
def handle_section_pressed(self, logicalIndex):
|
def handle_section_pressed(self, logicalIndex):
|
||||||
if logicalIndex == 0:
|
if logicalIndex == 0:
|
||||||
|
|
@ -46,11 +64,7 @@ class CheckBoxHeader(QHeaderView):
|
||||||
|
|
||||||
# --- ListedTab ---
|
# --- ListedTab ---
|
||||||
class ListedTab(QWidget):
|
class ListedTab(QWidget):
|
||||||
SORTABLE_COLUMNS = {
|
SORTABLE_COLUMNS = {1: "id", 3: "product_name", 5: "listed_at"}
|
||||||
1: "id",
|
|
||||||
3: "product_name",
|
|
||||||
5: "listed_at"
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -63,43 +77,47 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Top menu
|
# --- Top layout ---
|
||||||
top_layout = QHBoxLayout()
|
top_layout = QHBoxLayout()
|
||||||
top_layout.addStretch() # placeholder stretch
|
top_layout.addStretch()
|
||||||
|
|
||||||
# --- Progress bar ---
|
# Progress bar
|
||||||
self.progress_bar = QProgressBar()
|
self.progress_bar = QProgressBar()
|
||||||
self.progress_bar.setMinimum(0)
|
self.progress_bar.setMinimum(0)
|
||||||
self.progress_bar.setMaximum(100)
|
self.progress_bar.setMaximum(100)
|
||||||
self.progress_bar.setValue(0)
|
self.progress_bar.setValue(0)
|
||||||
self.progress_bar.setTextVisible(True)
|
self.progress_bar.setTextVisible(True)
|
||||||
self.progress_bar.setMinimumWidth(150)
|
self.progress_bar.setMinimumWidth(150)
|
||||||
self.progress_bar.setMinimumHeight(25) # thêm chiều cao đủ lớn
|
self.progress_bar.setMinimumHeight(25)
|
||||||
self.progress_bar.setAlignment(Qt.AlignCenter) # căn giữa text
|
self.progress_bar.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
top_layout.insertWidget(0, self.progress_bar)
|
top_layout.insertWidget(0, self.progress_bar)
|
||||||
top_layout.addStretch() # đẩy Action button sang phải
|
top_layout.addStretch()
|
||||||
|
|
||||||
# --- Action button ---
|
# Action button
|
||||||
self.options_btn = QPushButton("Action")
|
self.options_btn = QPushButton("Action")
|
||||||
self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
# QSizePolicy.Policy for PyQt6
|
||||||
|
self.options_btn.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed
|
||||||
|
)
|
||||||
self.options_btn.setMinimumWidth(50)
|
self.options_btn.setMinimumWidth(50)
|
||||||
self.options_btn.setMaximumWidth(120)
|
self.options_btn.setMaximumWidth(120)
|
||||||
top_layout.addWidget(self.options_btn)
|
top_layout.addWidget(self.options_btn)
|
||||||
|
|
||||||
layout.addLayout(top_layout)
|
layout.addLayout(top_layout)
|
||||||
|
|
||||||
# Table
|
# --- Table ---
|
||||||
self.table = QTableWidget()
|
self.table = QTableWidget()
|
||||||
self.table.verticalHeader().setDefaultSectionSize(60)
|
self.table.verticalHeader().setDefaultSectionSize(60)
|
||||||
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||||
header = CheckBoxHeader(Qt.Horizontal, self.table)
|
|
||||||
|
header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table)
|
||||||
self.table.setHorizontalHeader(header)
|
self.table.setHorizontalHeader(header)
|
||||||
header.select_all_changed.connect(self.select_all_rows)
|
header.select_all_changed.connect(self.select_all_rows)
|
||||||
header.sectionClicked.connect(self.handle_header_click)
|
header.sectionClicked.connect(self.handle_header_click)
|
||||||
layout.addWidget(self.table)
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
# Pagination
|
# --- Pagination ---
|
||||||
pag_layout = QHBoxLayout()
|
pag_layout = QHBoxLayout()
|
||||||
self.prev_btn = QPushButton("Previous")
|
self.prev_btn = QPushButton("Previous")
|
||||||
self.prev_btn.clicked.connect(self.prev_page)
|
self.prev_btn.clicked.connect(self.prev_page)
|
||||||
|
|
@ -117,14 +135,14 @@ class ListedTab(QWidget):
|
||||||
self.next_btn = QPushButton("Next")
|
self.next_btn = QPushButton("Next")
|
||||||
self.next_btn.clicked.connect(self.next_page)
|
self.next_btn.clicked.connect(self.next_page)
|
||||||
pag_layout.addWidget(self.next_btn)
|
pag_layout.addWidget(self.next_btn)
|
||||||
layout.addLayout(pag_layout)
|
|
||||||
|
|
||||||
|
layout.addLayout(pag_layout)
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
# --- Timer để update progress ---
|
# --- Timer ---
|
||||||
self.listing_timer = QTimer()
|
self.listing_timer = QTimer()
|
||||||
self.listing_timer.timeout.connect(self.update_listed_progress)
|
self.listing_timer.timeout.connect(self.update_listed_progress)
|
||||||
self.listing_timer.start(1000) # mỗi giây update
|
self.listing_timer.start(1000)
|
||||||
|
|
||||||
# --- Load Data ---
|
# --- Load Data ---
|
||||||
def load_data(self, show_progress=True):
|
def load_data(self, show_progress=True):
|
||||||
|
|
@ -133,20 +151,34 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
offset = self.current_page * PAGE_SIZE
|
offset = self.current_page * PAGE_SIZE
|
||||||
page_items, total_count = Listed.get_paginated(
|
page_items, total_count = Listed.get_paginated(
|
||||||
offset, PAGE_SIZE, self.filters,
|
offset,
|
||||||
sort_by=self.sort_by, sort_order=self.sort_order
|
PAGE_SIZE,
|
||||||
|
self.filters,
|
||||||
|
sort_by=self.sort_by,
|
||||||
|
sort_order=self.sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.total_count = total_count
|
self.total_count = total_count
|
||||||
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
|
||||||
self.table.setColumnCount(9)
|
self.table.setColumnCount(9)
|
||||||
columns = ["", "Image", "SKU", "Product Name", "Account", "Listed At", "Condition", "Status", "Actions"]
|
columns = [
|
||||||
|
"",
|
||||||
|
"Image",
|
||||||
|
"SKU",
|
||||||
|
"Product Name",
|
||||||
|
"Account",
|
||||||
|
"Listed At",
|
||||||
|
"Condition",
|
||||||
|
"Status",
|
||||||
|
"Actions",
|
||||||
|
]
|
||||||
self.table.setHorizontalHeaderLabels(columns)
|
self.table.setHorizontalHeaderLabels(columns)
|
||||||
self.table.setRowCount(len(page_items))
|
self.table.setRowCount(len(page_items))
|
||||||
|
|
||||||
def handler(item, i_row):
|
def handler(item, i_row):
|
||||||
listed_id = item.get("id")
|
listed_id = item.get("id")
|
||||||
|
|
||||||
# Checkbox
|
# Checkbox
|
||||||
cb = QCheckBox()
|
cb = QCheckBox()
|
||||||
cb.setProperty("listed_id", listed_id)
|
cb.setProperty("listed_id", listed_id)
|
||||||
|
|
@ -160,32 +192,41 @@ class ListedTab(QWidget):
|
||||||
if pixmap:
|
if pixmap:
|
||||||
lbl = QLabel()
|
lbl = QLabel()
|
||||||
lbl.setPixmap(pixmap)
|
lbl.setPixmap(pixmap)
|
||||||
lbl.setAlignment(Qt.AlignCenter)
|
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.table.setCellWidget(i_row, 1, lbl)
|
self.table.setCellWidget(i_row, 1, lbl)
|
||||||
else:
|
else:
|
||||||
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
||||||
else:
|
else:
|
||||||
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
||||||
|
|
||||||
# SKU, Product Name, Account
|
# Basic info
|
||||||
self.table.setItem(i_row, 2, QTableWidgetItem(item.get("sku") or ""))
|
self.table.setItem(i_row, 2, QTableWidgetItem(item.get("sku") or ""))
|
||||||
self.table.setItem(i_row, 3, QTableWidgetItem(item.get("product_name") or ""))
|
self.table.setItem(
|
||||||
self.table.setItem(i_row, 4, QTableWidgetItem(item.get("account_email") or ""))
|
i_row, 3, QTableWidgetItem(item.get("product_name") or "")
|
||||||
|
)
|
||||||
|
self.table.setItem(
|
||||||
|
i_row, 4, QTableWidgetItem(item.get("account_email") or "")
|
||||||
|
)
|
||||||
|
|
||||||
# Listed At
|
# Listed At
|
||||||
listed_str = ""
|
listed_str = ""
|
||||||
ts = item.get("listed_at")
|
ts = item.get("listed_at")
|
||||||
if ts:
|
if ts:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
listed_str = datetime.fromtimestamp(int(ts)).strftime("%Y-%m-%d %H:%M")
|
listed_str = datetime.fromtimestamp(int(ts)).strftime(
|
||||||
|
"%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
listed_str = str(ts)
|
listed_str = str(ts)
|
||||||
self.table.setItem(i_row, 5, QTableWidgetItem(listed_str))
|
self.table.setItem(i_row, 5, QTableWidgetItem(listed_str))
|
||||||
|
|
||||||
# Condition, Status
|
# Condition, Status
|
||||||
self.table.setItem(i_row, 6, QTableWidgetItem(item.get("condition") or ""))
|
self.table.setItem(i_row, 6, QTableWidgetItem(item.get("condition") or ""))
|
||||||
self.table.setItem(i_row, 7, QTableWidgetItem(item.get("status") or "pending"))
|
self.table.setItem(
|
||||||
|
i_row, 7, QTableWidgetItem(item.get("status") or "pending")
|
||||||
|
)
|
||||||
|
|
||||||
# Actions
|
# Actions
|
||||||
btn_menu = QPushButton("Actions")
|
btn_menu = QPushButton("Actions")
|
||||||
|
|
@ -198,25 +239,32 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
items_with_index = [(p, i) for i, p in enumerate(page_items)]
|
items_with_index = [(p, i) for i, p in enumerate(page_items)]
|
||||||
if show_progress:
|
if show_progress:
|
||||||
run_with_progress(items_with_index, handler=lambda x: handler(*x), message="Loading listed...", parent=self)
|
run_with_progress(
|
||||||
|
items_with_index,
|
||||||
|
handler=lambda x: handler(*x),
|
||||||
|
message="Loading listed...",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for item in items_with_index:
|
for item in items_with_index:
|
||||||
handler(*item)
|
handler(*item)
|
||||||
|
|
||||||
# Header sizing
|
# Header resize
|
||||||
header = self.table.horizontalHeader()
|
header = self.table.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
self.table.setColumnWidth(1, 60)
|
self.table.setColumnWidth(1, 60)
|
||||||
for idx in range(2, 8):
|
for idx in range(2, 8):
|
||||||
header.setSectionResizeMode(idx, QHeaderView.Stretch)
|
header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(8, QHeaderView.Fixed)
|
header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed)
|
||||||
self.table.setColumnWidth(8, 100)
|
self.table.setColumnWidth(8, 100)
|
||||||
|
|
||||||
# Pagination
|
# Pagination info
|
||||||
self.prev_btn.setEnabled(self.current_page > 0)
|
self.prev_btn.setEnabled(self.current_page > 0)
|
||||||
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
||||||
self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)")
|
self.page_info_label.setText(
|
||||||
|
f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)"
|
||||||
|
)
|
||||||
|
|
||||||
# Reset header checkbox
|
# Reset header checkbox
|
||||||
if isinstance(header, CheckBoxHeader):
|
if isinstance(header, CheckBoxHeader):
|
||||||
|
|
@ -247,39 +295,59 @@ class ListedTab(QWidget):
|
||||||
menu.addAction(action_clear)
|
menu.addAction(action_clear)
|
||||||
|
|
||||||
# Toggle Auto Listing
|
# Toggle Auto Listing
|
||||||
auto_setting = Setting.get_by_key(Setting.AUTO_LISTING)
|
auto_setting = None
|
||||||
|
try:
|
||||||
|
auto_setting = Setting.get_by_key(Setting.AUTO_LISTING)
|
||||||
|
except Exception:
|
||||||
|
auto_setting = None
|
||||||
|
|
||||||
if auto_setting:
|
if auto_setting:
|
||||||
setting_id = auto_setting["id"]
|
setting_id = auto_setting.get("id")
|
||||||
auto_listing_val = auto_setting["value"].lower() == "true"
|
auto_listing_val = str(auto_setting.get("value", "")).lower() == "true"
|
||||||
else:
|
else:
|
||||||
auto_setting = Setting.create(Setting.AUTO_LISTING, "false")
|
# create default if missing
|
||||||
setting_id = auto_setting["id"]
|
try:
|
||||||
auto_listing_val = False
|
created = Setting.create(Setting.AUTO_LISTING, "false")
|
||||||
|
setting_id = created.get("id")
|
||||||
|
auto_listing_val = False
|
||||||
|
except Exception:
|
||||||
|
setting_id = None
|
||||||
|
auto_listing_val = False
|
||||||
|
|
||||||
action_toggle_auto = QAction(
|
action_toggle_auto = QAction(
|
||||||
"Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON",
|
"Turn Auto Listing OFF" if auto_listing_val else "Turn Auto Listing ON",
|
||||||
menu
|
menu,
|
||||||
)
|
)
|
||||||
|
|
||||||
def toggle_auto_listing():
|
def toggle_auto_listing():
|
||||||
|
if setting_id is None:
|
||||||
|
QMessageBox.critical(
|
||||||
|
self, "Error", "AUTO_LISTING setting not available"
|
||||||
|
)
|
||||||
|
return
|
||||||
new_val = "false" if auto_listing_val else "true"
|
new_val = "false" if auto_listing_val else "true"
|
||||||
try:
|
try:
|
||||||
Setting.update_value(setting_id, new_val)
|
Setting.update_value(setting_id, new_val)
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "Auto Listing",
|
self, "Auto Listing", f"AUTO_LISTING set to {new_val.upper()}"
|
||||||
f"AUTO_LISTING set to {new_val.upper()}"
|
|
||||||
)
|
)
|
||||||
|
# refresh menu and progress
|
||||||
self.update_options_menu()
|
self.update_options_menu()
|
||||||
self.update_listed_progress()
|
self.update_listed_progress()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to update AUTO_LISTING: {e}")
|
QMessageBox.critical(
|
||||||
|
self, "Error", f"Failed to update AUTO_LISTING: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
action_toggle_auto.triggered.connect(toggle_auto_listing)
|
action_toggle_auto.triggered.connect(toggle_auto_listing)
|
||||||
menu.addAction(action_toggle_auto)
|
menu.addAction(action_toggle_auto)
|
||||||
|
|
||||||
# Delete Selected
|
# Delete Selected
|
||||||
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked()
|
if any(
|
||||||
for i in range(self.table.rowCount())):
|
isinstance(self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and self.table.cellWidget(i, 0).isChecked()
|
||||||
|
for i in range(self.table.rowCount())
|
||||||
|
):
|
||||||
action_delete_selected = QAction("Delete Selected", menu)
|
action_delete_selected = QAction("Delete Selected", menu)
|
||||||
action_delete_selected.triggered.connect(self.delete_selected)
|
action_delete_selected.triggered.connect(self.delete_selected)
|
||||||
menu.addAction(action_delete_selected)
|
menu.addAction(action_delete_selected)
|
||||||
|
|
@ -287,7 +355,9 @@ class ListedTab(QWidget):
|
||||||
self.options_btn.setMenu(menu)
|
self.options_btn.setMenu(menu)
|
||||||
|
|
||||||
def update_listed_progress(self):
|
def update_listed_progress(self):
|
||||||
auto_listing_val = Setting.get(Setting.AUTO_LISTING, "false").lower() == "true"
|
auto_listing_val = (
|
||||||
|
str(Setting.get(Setting.AUTO_LISTING, "false")).lower() == "true"
|
||||||
|
)
|
||||||
self.progress_bar.setVisible(auto_listing_val)
|
self.progress_bar.setVisible(auto_listing_val)
|
||||||
if not auto_listing_val:
|
if not auto_listing_val:
|
||||||
return
|
return
|
||||||
|
|
@ -311,11 +381,10 @@ class ListedTab(QWidget):
|
||||||
self.progress_bar.setTextVisible(True)
|
self.progress_bar.setTextVisible(True)
|
||||||
self.progress_bar.setFormat("0/0 listed")
|
self.progress_bar.setFormat("0/0 listed")
|
||||||
|
|
||||||
|
|
||||||
# --- Filter ---
|
# --- Filter ---
|
||||||
def open_filter_dialog(self):
|
def open_filter_dialog(self):
|
||||||
dialog = ListedFilterDialog(self)
|
dialog = ListedFilterDialog(self)
|
||||||
if dialog.exec_():
|
if dialog.exec(): # PyQt6: exec()
|
||||||
self.filters = dialog.get_filters()
|
self.filters = dialog.get_filters()
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
@ -366,29 +435,48 @@ class ListedTab(QWidget):
|
||||||
|
|
||||||
# --- Delete ---
|
# --- Delete ---
|
||||||
def delete_selected(self):
|
def delete_selected(self):
|
||||||
ids = [int(cb.property("listed_id")) for i in range(self.table.rowCount())
|
ids = [
|
||||||
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked()]
|
int(cb.property("listed_id"))
|
||||||
|
for i in range(self.table.rowCount())
|
||||||
|
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and cb.isChecked()
|
||||||
|
]
|
||||||
if not ids:
|
if not ids:
|
||||||
QMessageBox.information(self, "Info", "No listed selected")
|
QMessageBox.information(self, "Info", "No listed selected")
|
||||||
return
|
return
|
||||||
|
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm Delete", f"Delete {len(ids)} selected listed items?",
|
self,
|
||||||
QMessageBox.Yes | QMessageBox.No
|
"Confirm Delete",
|
||||||
|
f"Delete {len(ids)} selected listed items?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm != QMessageBox.Yes:
|
if confirm != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
run_with_progress(ids, handler=lambda x: Listed.bulk_delete([x]), message="Deleting listed...", parent=self)
|
run_with_progress(
|
||||||
|
ids,
|
||||||
|
handler=lambda x: Listed.bulk_delete([x]),
|
||||||
|
message="Deleting listed...",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def delete_listed(self, listed_id):
|
def delete_listed(self, listed_id):
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm Delete", f"Delete listed ID {listed_id}?", QMessageBox.Yes | QMessageBox.No
|
self,
|
||||||
|
"Confirm Delete",
|
||||||
|
f"Delete listed ID {listed_id}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm != QMessageBox.Yes:
|
if confirm != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
run_with_progress([listed_id], handler=lambda x: Listed.bulk_delete([x]), message="Deleting listed...", parent=self)
|
run_with_progress(
|
||||||
|
[listed_id],
|
||||||
|
handler=lambda x: Listed.bulk_delete([x]),
|
||||||
|
message="Deleting listed...",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
from PyQt5.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton, QMessageBox
|
from PyQt6.QtWidgets import QDialog, QVBoxLayout, QComboBox, QPushButton, QMessageBox
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from database.models.listed import Listed
|
from database.models.listed import Listed
|
||||||
from database.models.account import Account # import model
|
from database.models.account import Account # import model
|
||||||
|
|
||||||
|
|
||||||
class AddListedDialog(QDialog):
|
class AddListedDialog(QDialog):
|
||||||
def __init__(self, product_ids, parent=None):
|
def __init__(self, product_ids, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
@ -27,6 +28,7 @@ class AddListedDialog(QDialog):
|
||||||
btn_ok = QPushButton("Add Listed")
|
btn_ok = QPushButton("Add Listed")
|
||||||
btn_ok.clicked.connect(self.process_add_listed)
|
btn_ok.clicked.connect(self.process_add_listed)
|
||||||
layout.addWidget(btn_ok)
|
layout.addWidget(btn_ok)
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
def process_add_listed(self):
|
def process_add_listed(self):
|
||||||
|
|
@ -40,16 +42,20 @@ class AddListedDialog(QDialog):
|
||||||
return
|
return
|
||||||
|
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm Add Listed",
|
self,
|
||||||
|
"Confirm Add Listed",
|
||||||
f"Add {len(self.product_ids)} product(s) to listed under selected account?",
|
f"Add {len(self.product_ids)} product(s) to listed under selected account?",
|
||||||
QMessageBox.Yes | QMessageBox.No
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm != QMessageBox.Yes:
|
|
||||||
|
if confirm != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
def handler(product_id):
|
def handler(product_id):
|
||||||
try:
|
try:
|
||||||
Listed.bulk_create([{"product_id": product_id, "account_id": self.selected_account}])
|
Listed.bulk_create(
|
||||||
|
[{"product_id": product_id, "account_id": self.selected_account}]
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error adding listed for product {product_id}: {e}")
|
print(f"Error adding listed for product {product_id}: {e}")
|
||||||
|
|
||||||
|
|
@ -57,8 +63,10 @@ class AddListedDialog(QDialog):
|
||||||
self.product_ids,
|
self.product_ids,
|
||||||
handler=handler,
|
handler=handler,
|
||||||
message="Adding listed...",
|
message="Adding listed...",
|
||||||
parent=self
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
QMessageBox.information(self, "Success", f"Added {len(self.product_ids)} product(s) to listed.")
|
QMessageBox.information(
|
||||||
|
self, "Success", f"Added {len(self.product_ids)} product(s) to listed."
|
||||||
|
)
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QFormLayout, QLineEdit, QDateEdit, QComboBox,
|
QDialog,
|
||||||
QDialogButtonBox, QPushButton, QHBoxLayout, QVBoxLayout, QWidget
|
QFormLayout,
|
||||||
|
QLineEdit,
|
||||||
|
QDateEdit,
|
||||||
|
QComboBox,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QHBoxLayout,
|
||||||
|
QVBoxLayout,
|
||||||
|
QWidget,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import QDate
|
from PyQt6.QtCore import QDate
|
||||||
import json
|
|
||||||
|
|
||||||
class FilterDialog(QDialog):
|
class FilterDialog(QDialog):
|
||||||
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
|
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
|
||||||
|
|
@ -62,11 +69,12 @@ class FilterDialog(QDialog):
|
||||||
right_form.addRow("Location:", self.location_input)
|
right_form.addRow("Location:", self.location_input)
|
||||||
|
|
||||||
columns_layout.addWidget(right_widget)
|
columns_layout.addWidget(right_widget)
|
||||||
|
|
||||||
main_layout.addLayout(columns_layout)
|
main_layout.addLayout(columns_layout)
|
||||||
|
|
||||||
# --- Button Box ---
|
# --- Button Box ---
|
||||||
self.btn_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
self.btn_box = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
self.btn_box.accepted.connect(self.accept)
|
self.btn_box.accepted.connect(self.accept)
|
||||||
self.btn_box.rejected.connect(self.reject)
|
self.btn_box.rejected.connect(self.reject)
|
||||||
main_layout.addWidget(self.btn_box)
|
main_layout.addWidget(self.btn_box)
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,30 @@
|
||||||
import json
|
import json
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit,
|
QDialog,
|
||||||
QComboBox, QTextEdit, QPushButton, QGridLayout,
|
QVBoxLayout,
|
||||||
QFileDialog, QMessageBox
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QComboBox,
|
||||||
|
QTextEdit,
|
||||||
|
QPushButton,
|
||||||
|
QGridLayout,
|
||||||
|
QFileDialog,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from database.models.product import Product
|
from database.models.product import Product
|
||||||
|
|
||||||
|
|
||||||
class ProductForm(QDialog):
|
class ProductForm(QDialog):
|
||||||
SAMPLE_NAMES = [
|
SAMPLE_NAMES = [
|
||||||
"Apple iPhone 15", "Samsung Galaxy S23", "Sony Headphones",
|
"Apple iPhone 15",
|
||||||
"Dell Laptop", "Canon Camera", "Nike Shoes", "Adidas T-Shirt"
|
"Samsung Galaxy S23",
|
||||||
|
"Sony Headphones",
|
||||||
|
"Dell Laptop",
|
||||||
|
"Canon Camera",
|
||||||
|
"Nike Shoes",
|
||||||
|
"Adidas T-Shirt",
|
||||||
]
|
]
|
||||||
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
|
SAMPLE_CATEGORIES = ["Electronics", "Clothing", "Shoes", "Accessories", "Home"]
|
||||||
SAMPLE_CONDITIONS = ["New", "Used", "Refurbished"]
|
SAMPLE_CONDITIONS = ["New", "Used", "Refurbished"]
|
||||||
|
|
@ -116,7 +129,11 @@ class ProductForm(QDialog):
|
||||||
self, "Select Image Files", "", "Images (*.png *.jpg *.jpeg *.bmp)"
|
self, "Select Image Files", "", "Images (*.png *.jpg *.jpeg *.bmp)"
|
||||||
)
|
)
|
||||||
if files:
|
if files:
|
||||||
existing = [img.strip() for img in self.images_input.toPlainText().split(",") if img.strip()]
|
existing = [
|
||||||
|
img.strip()
|
||||||
|
for img in self.images_input.toPlainText().split(",")
|
||||||
|
if img.strip()
|
||||||
|
]
|
||||||
self.images_input.setText(",".join(existing + files))
|
self.images_input.setText(",".join(existing + files))
|
||||||
|
|
||||||
def save(self):
|
def save(self):
|
||||||
|
|
@ -131,35 +148,61 @@ class ProductForm(QDialog):
|
||||||
QMessageBox.warning(self, "Error", "Price must be a valid number")
|
QMessageBox.warning(self, "Error", "Price must be a valid number")
|
||||||
return
|
return
|
||||||
|
|
||||||
category = self.category_input.currentText() if self.category_input.currentText() != "None" else None
|
category = (
|
||||||
condition = self.condition_input.currentText() if self.condition_input.currentText() != "None" else None
|
self.category_input.currentText()
|
||||||
|
if self.category_input.currentText() != "None"
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
condition = (
|
||||||
|
self.condition_input.currentText()
|
||||||
|
if self.condition_input.currentText() != "None"
|
||||||
|
else None
|
||||||
|
)
|
||||||
brand = self.brand_input.text().strip() or None
|
brand = self.brand_input.text().strip() or None
|
||||||
description = self.description_input.toPlainText().strip() or None
|
description = self.description_input.toPlainText().strip() or None
|
||||||
tags = [t.strip() for t in self.tags_input.text().split(",") if t.strip()] or None
|
tags = [
|
||||||
|
t.strip() for t in self.tags_input.text().split(",") if t.strip()
|
||||||
|
] or None
|
||||||
sku = self.sku_input.text().strip() or None
|
sku = self.sku_input.text().strip() or None
|
||||||
location = self.location_input.text().strip() or None
|
location = self.location_input.text().strip() or None
|
||||||
|
|
||||||
# Xử lý images: remove dấu ngoặc và quote nếu copy từ JSON
|
|
||||||
raw_text = self.images_input.toPlainText()
|
raw_text = self.images_input.toPlainText()
|
||||||
raw_text = raw_text.replace('[', '').replace(']', '').replace('"', '').replace("'", '')
|
raw_text = (
|
||||||
|
raw_text.replace("[", "").replace("]", "").replace('"', "").replace("'", "")
|
||||||
|
)
|
||||||
images = [img.strip() for img in raw_text.split(",") if img.strip()]
|
images = [img.strip() for img in raw_text.split(",") if img.strip()]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if self.product and "id" in self.product:
|
if self.product and "id" in self.product:
|
||||||
Product.update(
|
Product.update(
|
||||||
self.product["id"], name, price,
|
self.product["id"],
|
||||||
|
name,
|
||||||
|
price,
|
||||||
images=images,
|
images=images,
|
||||||
url=None, status="draft",
|
url=None,
|
||||||
category=category, condition=condition, brand=brand,
|
status="draft",
|
||||||
description=description, tags=tags, sku=sku, location=location
|
category=category,
|
||||||
|
condition=condition,
|
||||||
|
brand=brand,
|
||||||
|
description=description,
|
||||||
|
tags=tags,
|
||||||
|
sku=sku,
|
||||||
|
location=location,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
Product.create(
|
Product.create(
|
||||||
name, price,
|
name,
|
||||||
|
price,
|
||||||
images=images,
|
images=images,
|
||||||
url=None, status="draft",
|
url=None,
|
||||||
category=category, condition=condition, brand=brand,
|
status="draft",
|
||||||
description=description, tags=tags, sku=sku, location=location
|
category=category,
|
||||||
|
condition=condition,
|
||||||
|
brand=brand,
|
||||||
|
description=description,
|
||||||
|
tags=tags,
|
||||||
|
sku=sku,
|
||||||
|
location=location,
|
||||||
)
|
)
|
||||||
self.accept()
|
self.accept()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,27 @@
|
||||||
import json
|
import json
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QWidget,
|
||||||
QPushButton, QHBoxLayout, QMenu, QAction, QHeaderView,
|
QVBoxLayout,
|
||||||
QLabel, QCheckBox, QSizePolicy, QMessageBox, QStyleOptionButton, QStyle, QStylePainter, QLineEdit
|
QTableWidget,
|
||||||
|
QTableWidgetItem,
|
||||||
|
QPushButton,
|
||||||
|
QHBoxLayout,
|
||||||
|
QMenu,
|
||||||
|
QHeaderView,
|
||||||
|
QLabel,
|
||||||
|
QCheckBox,
|
||||||
|
QSizePolicy,
|
||||||
|
QMessageBox,
|
||||||
|
QStyleOptionButton,
|
||||||
|
QStyle,
|
||||||
|
QStylePainter,
|
||||||
|
QLineEdit,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt, QRect, pyqtSignal
|
from PyQt6.QtGui import QAction
|
||||||
|
from PyQt6.QtCore import Qt, QRect, pyqtSignal
|
||||||
from database.models.product import Product
|
from database.models.product import Product
|
||||||
from database.models.listed import Listed
|
|
||||||
from services.image_service import ImageService
|
from services.image_service import ImageService
|
||||||
from gui.tabs.products.forms.product_form import ProductForm
|
from gui.tabs.products.forms.product_form import ProductForm
|
||||||
from gui.tabs.products.dialogs.product_filter_dialog import FilterDialog
|
from gui.tabs.products.dialogs.product_filter_dialog import FilterDialog
|
||||||
|
|
@ -16,6 +29,7 @@ from gui.tabs.products.dialogs.add_listed_dialog import AddListedDialog
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
|
|
||||||
# --- Header Checkbox ---
|
# --- Header Checkbox ---
|
||||||
class CheckBoxHeader(QHeaderView):
|
class CheckBoxHeader(QHeaderView):
|
||||||
select_all_changed = pyqtSignal(bool)
|
select_all_changed = pyqtSignal(bool)
|
||||||
|
|
@ -34,9 +48,14 @@ class CheckBoxHeader(QHeaderView):
|
||||||
x = rect.x() + (rect.width() - size) // 2
|
x = rect.x() + (rect.width() - size) // 2
|
||||||
y = rect.y() + (rect.height() - size) // 2
|
y = rect.y() + (rect.height() - size) // 2
|
||||||
option.rect = QRect(x, y, size, size)
|
option.rect = QRect(x, y, size, size)
|
||||||
option.state = QStyle.State_Enabled | (QStyle.State_On if self.isOn else QStyle.State_Off)
|
# PyQt6: use QStyle.StateFlag and QStyle.ControlElement
|
||||||
|
state = QStyle.StateFlag.State_Enabled
|
||||||
|
state = state | (
|
||||||
|
QStyle.StateFlag.State_On if self.isOn else QStyle.StateFlag.State_Off
|
||||||
|
)
|
||||||
|
option.state = state
|
||||||
painter2 = QStylePainter(self.viewport())
|
painter2 = QStylePainter(self.viewport())
|
||||||
painter2.drawControl(QStyle.CE_CheckBox, option)
|
painter2.drawControl(QStyle.ControlElement.CE_CheckBox, option)
|
||||||
|
|
||||||
def handle_section_pressed(self, logicalIndex):
|
def handle_section_pressed(self, logicalIndex):
|
||||||
if logicalIndex == 0:
|
if logicalIndex == 0:
|
||||||
|
|
@ -47,12 +66,7 @@ class CheckBoxHeader(QHeaderView):
|
||||||
|
|
||||||
# --- ProductTab ---
|
# --- ProductTab ---
|
||||||
class ProductTab(QWidget):
|
class ProductTab(QWidget):
|
||||||
SORTABLE_COLUMNS = {
|
SORTABLE_COLUMNS = {1: "id", 3: "name", 4: "price", 7: "created_at"}
|
||||||
1: "id",
|
|
||||||
3: "name",
|
|
||||||
4: "price",
|
|
||||||
7: "created_at"
|
|
||||||
}
|
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -72,7 +86,9 @@ class ProductTab(QWidget):
|
||||||
top_layout.addWidget(self.add_btn)
|
top_layout.addWidget(self.add_btn)
|
||||||
|
|
||||||
self.options_btn = QPushButton("Action")
|
self.options_btn = QPushButton("Action")
|
||||||
self.options_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
self.options_btn.setSizePolicy(
|
||||||
|
QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Fixed
|
||||||
|
)
|
||||||
self.options_btn.setMinimumWidth(50)
|
self.options_btn.setMinimumWidth(50)
|
||||||
self.options_btn.setMaximumWidth(120)
|
self.options_btn.setMaximumWidth(120)
|
||||||
top_layout.addWidget(self.options_btn)
|
top_layout.addWidget(self.options_btn)
|
||||||
|
|
@ -81,10 +97,10 @@ class ProductTab(QWidget):
|
||||||
# Table
|
# Table
|
||||||
self.table = QTableWidget()
|
self.table = QTableWidget()
|
||||||
self.table.verticalHeader().setDefaultSectionSize(60)
|
self.table.verticalHeader().setDefaultSectionSize(60)
|
||||||
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||||
|
|
||||||
header = CheckBoxHeader(Qt.Horizontal, self.table)
|
header = CheckBoxHeader(Qt.Orientation.Horizontal, self.table)
|
||||||
self.table.setHorizontalHeader(header)
|
self.table.setHorizontalHeader(header)
|
||||||
header.select_all_changed.connect(self.select_all_rows)
|
header.select_all_changed.connect(self.select_all_rows)
|
||||||
header.sectionClicked.connect(self.handle_header_click)
|
header.sectionClicked.connect(self.handle_header_click)
|
||||||
|
|
@ -141,16 +157,22 @@ class ProductTab(QWidget):
|
||||||
action_clear = QAction("Clear Filter", menu)
|
action_clear = QAction("Clear Filter", menu)
|
||||||
action_clear.triggered.connect(self.clear_filters)
|
action_clear.triggered.connect(self.clear_filters)
|
||||||
menu.addAction(action_clear)
|
menu.addAction(action_clear)
|
||||||
|
|
||||||
# --- Thêm Add Listed Selected ---
|
# --- Thêm Add Listed Selected ---
|
||||||
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked()
|
if any(
|
||||||
for i in range(self.table.rowCount())):
|
isinstance(self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and self.table.cellWidget(i, 0).isChecked()
|
||||||
|
for i in range(self.table.rowCount())
|
||||||
|
):
|
||||||
action_add_listed_selected = QAction("Add Listed Selected", menu)
|
action_add_listed_selected = QAction("Add Listed Selected", menu)
|
||||||
action_add_listed_selected.triggered.connect(self.add_listed_selected)
|
action_add_listed_selected.triggered.connect(self.add_listed_selected)
|
||||||
menu.addAction(action_add_listed_selected)
|
menu.addAction(action_add_listed_selected)
|
||||||
|
|
||||||
if any(isinstance(self.table.cellWidget(i, 0), QCheckBox) and self.table.cellWidget(i, 0).isChecked()
|
if any(
|
||||||
for i in range(self.table.rowCount())):
|
isinstance(self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and self.table.cellWidget(i, 0).isChecked()
|
||||||
|
for i in range(self.table.rowCount())
|
||||||
|
):
|
||||||
action_delete_selected = QAction("Delete Selected", menu)
|
action_delete_selected = QAction("Delete Selected", menu)
|
||||||
action_delete_selected.triggered.connect(self.delete_selected)
|
action_delete_selected.triggered.connect(self.delete_selected)
|
||||||
menu.addAction(action_delete_selected)
|
menu.addAction(action_delete_selected)
|
||||||
|
|
@ -160,7 +182,7 @@ class ProductTab(QWidget):
|
||||||
# --- Filter ---
|
# --- Filter ---
|
||||||
def open_filter_dialog(self):
|
def open_filter_dialog(self):
|
||||||
dialog = FilterDialog(self)
|
dialog = FilterDialog(self)
|
||||||
if dialog.exec_():
|
if dialog.exec(): # PyQt6: exec()
|
||||||
self.filters = dialog.get_filters()
|
self.filters = dialog.get_filters()
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
@ -182,15 +204,28 @@ class ProductTab(QWidget):
|
||||||
offset = self.current_page * PAGE_SIZE
|
offset = self.current_page * PAGE_SIZE
|
||||||
# Lấy toàn bộ dữ liệu cần load
|
# Lấy toàn bộ dữ liệu cần load
|
||||||
page_items, total_count = Product.get_paginated(
|
page_items, total_count = Product.get_paginated(
|
||||||
offset, PAGE_SIZE, self.filters,
|
offset,
|
||||||
sort_by=self.sort_by, sort_order=self.sort_order
|
PAGE_SIZE,
|
||||||
|
self.filters,
|
||||||
|
sort_by=self.sort_by,
|
||||||
|
sort_order=self.sort_order,
|
||||||
)
|
)
|
||||||
|
|
||||||
self.total_count = total_count
|
self.total_count = total_count
|
||||||
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
self.total_pages = max(1, (total_count + PAGE_SIZE - 1) // PAGE_SIZE)
|
||||||
|
|
||||||
self.table.setColumnCount(9)
|
self.table.setColumnCount(9)
|
||||||
columns = ["", "Image", "SKU", "Name", "Price", "Condition", "Brand", "Created At", "Actions"]
|
columns = [
|
||||||
|
"",
|
||||||
|
"Image",
|
||||||
|
"SKU",
|
||||||
|
"Name",
|
||||||
|
"Price",
|
||||||
|
"Condition",
|
||||||
|
"Brand",
|
||||||
|
"Created At",
|
||||||
|
"Actions",
|
||||||
|
]
|
||||||
self.table.setHorizontalHeaderLabels(columns)
|
self.table.setHorizontalHeaderLabels(columns)
|
||||||
self.table.setRowCount(len(page_items))
|
self.table.setRowCount(len(page_items))
|
||||||
|
|
||||||
|
|
@ -210,7 +245,7 @@ class ProductTab(QWidget):
|
||||||
if pixmap:
|
if pixmap:
|
||||||
lbl = QLabel()
|
lbl = QLabel()
|
||||||
lbl.setPixmap(pixmap)
|
lbl.setPixmap(pixmap)
|
||||||
lbl.setAlignment(Qt.AlignCenter)
|
lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||||
self.table.setCellWidget(i_row, 1, lbl)
|
self.table.setCellWidget(i_row, 1, lbl)
|
||||||
else:
|
else:
|
||||||
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
self.table.setItem(i_row, 1, QTableWidgetItem("None"))
|
||||||
|
|
@ -228,8 +263,11 @@ class ProductTab(QWidget):
|
||||||
created_ts = p.get("created_at")
|
created_ts = p.get("created_at")
|
||||||
if created_ts:
|
if created_ts:
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
try:
|
try:
|
||||||
created_str = datetime.fromtimestamp(int(created_ts)).strftime("%Y-%m-%d %H:%M")
|
created_str = datetime.fromtimestamp(int(created_ts)).strftime(
|
||||||
|
"%Y-%m-%d %H:%M"
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
created_str = str(created_ts)
|
created_str = str(created_ts)
|
||||||
self.table.setItem(i_row, 7, QTableWidgetItem(created_str))
|
self.table.setItem(i_row, 7, QTableWidgetItem(created_str))
|
||||||
|
|
@ -240,11 +278,11 @@ class ProductTab(QWidget):
|
||||||
act_edit = QAction("Edit", btn_menu)
|
act_edit = QAction("Edit", btn_menu)
|
||||||
act_edit.triggered.connect(partial(self.edit_product, p))
|
act_edit.triggered.connect(partial(self.edit_product, p))
|
||||||
menu.addAction(act_edit)
|
menu.addAction(act_edit)
|
||||||
|
|
||||||
act_add_listed = QAction("Add Listed", btn_menu) # <-- thêm action này
|
act_add_listed = QAction("Add Listed", btn_menu) # <-- thêm action này
|
||||||
act_add_listed.triggered.connect(partial(self.add_listed_row, product_id))
|
act_add_listed.triggered.connect(partial(self.add_listed_row, product_id))
|
||||||
menu.addAction(act_add_listed)
|
menu.addAction(act_add_listed)
|
||||||
|
|
||||||
act_del = QAction("Delete", btn_menu)
|
act_del = QAction("Delete", btn_menu)
|
||||||
act_del.triggered.connect(partial(self.delete_product, product_id))
|
act_del.triggered.connect(partial(self.delete_product, product_id))
|
||||||
menu.addAction(act_del)
|
menu.addAction(act_del)
|
||||||
|
|
@ -258,7 +296,7 @@ class ProductTab(QWidget):
|
||||||
items_with_index,
|
items_with_index,
|
||||||
handler=lambda x: handler(*x),
|
handler=lambda x: handler(*x),
|
||||||
message="Loading products...",
|
message="Loading products...",
|
||||||
parent=self
|
parent=self,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
for item in items_with_index:
|
for item in items_with_index:
|
||||||
|
|
@ -266,18 +304,20 @@ class ProductTab(QWidget):
|
||||||
|
|
||||||
# Header sizing
|
# Header sizing
|
||||||
header = self.table.horizontalHeader()
|
header = self.table.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents)
|
||||||
header.setSectionResizeMode(1, QHeaderView.Fixed)
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Fixed)
|
||||||
self.table.setColumnWidth(1, 60)
|
self.table.setColumnWidth(1, 60)
|
||||||
for idx in range(2, 8):
|
for idx in range(2, 8):
|
||||||
header.setSectionResizeMode(idx, QHeaderView.Stretch)
|
header.setSectionResizeMode(idx, QHeaderView.ResizeMode.Stretch)
|
||||||
header.setSectionResizeMode(8, QHeaderView.Fixed)
|
header.setSectionResizeMode(8, QHeaderView.ResizeMode.Fixed)
|
||||||
self.table.setColumnWidth(8, 100)
|
self.table.setColumnWidth(8, 100)
|
||||||
|
|
||||||
# Pagination
|
# Pagination
|
||||||
self.prev_btn.setEnabled(self.current_page > 0)
|
self.prev_btn.setEnabled(self.current_page > 0)
|
||||||
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
||||||
self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)")
|
self.page_info_label.setText(
|
||||||
|
f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)"
|
||||||
|
)
|
||||||
|
|
||||||
# Reset header checkbox
|
# Reset header checkbox
|
||||||
if isinstance(header, CheckBoxHeader):
|
if isinstance(header, CheckBoxHeader):
|
||||||
|
|
@ -286,8 +326,6 @@ class ProductTab(QWidget):
|
||||||
|
|
||||||
self.update_options_menu()
|
self.update_options_menu()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# --- Go to page ---
|
# --- Go to page ---
|
||||||
def go_to_page(self):
|
def go_to_page(self):
|
||||||
try:
|
try:
|
||||||
|
|
@ -310,70 +348,72 @@ class ProductTab(QWidget):
|
||||||
ids = [
|
ids = [
|
||||||
int(cb.property("product_id"))
|
int(cb.property("product_id"))
|
||||||
for i in range(self.table.rowCount())
|
for i in range(self.table.rowCount())
|
||||||
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked()
|
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and cb.isChecked()
|
||||||
]
|
]
|
||||||
if not ids:
|
if not ids:
|
||||||
QMessageBox.information(self, "Info", "No product selected")
|
QMessageBox.information(self, "Info", "No product selected")
|
||||||
return
|
return
|
||||||
|
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm Delete", f"Delete {len(ids)} selected products?",
|
self,
|
||||||
QMessageBox.Yes | QMessageBox.No
|
"Confirm Delete",
|
||||||
|
f"Delete {len(ids)} selected products?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm != QMessageBox.Yes:
|
if confirm != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
# --- dùng run_with_progress ---
|
# --- dùng run_with_progress ---
|
||||||
run_with_progress(
|
run_with_progress(
|
||||||
ids,
|
ids, handler=Product.delete, message="Deleting products...", parent=self
|
||||||
handler=Product.delete,
|
|
||||||
message="Deleting products...",
|
|
||||||
parent=self
|
|
||||||
)
|
)
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
|
|
||||||
# --- Product Actions ---
|
# --- Product Actions ---
|
||||||
def add_product(self):
|
def add_product(self):
|
||||||
form = ProductForm(self)
|
form = ProductForm(self)
|
||||||
if form.exec_():
|
if form.exec(): # PyQt6: exec()
|
||||||
self.current_page = 0
|
self.current_page = 0
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def edit_product(self, product):
|
def edit_product(self, product):
|
||||||
form = ProductForm(self, product)
|
form = ProductForm(self, product)
|
||||||
if form.exec_():
|
if form.exec(): # PyQt6: exec()
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def delete_product(self, product_id):
|
def delete_product(self, product_id):
|
||||||
confirm = QMessageBox.question(
|
confirm = QMessageBox.question(
|
||||||
self, "Confirm Delete", f"Delete product ID {product_id}?", QMessageBox.Yes | QMessageBox.No
|
self,
|
||||||
|
"Confirm Delete",
|
||||||
|
f"Delete product ID {product_id}?",
|
||||||
|
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
|
||||||
)
|
)
|
||||||
if confirm != QMessageBox.Yes:
|
if confirm != QMessageBox.StandardButton.Yes:
|
||||||
return
|
return
|
||||||
|
|
||||||
run_with_progress(
|
run_with_progress(
|
||||||
[product_id],
|
[product_id],
|
||||||
handler=Product.delete,
|
handler=Product.delete,
|
||||||
message="Deleting product...",
|
message="Deleting product...",
|
||||||
parent=self
|
parent=self,
|
||||||
)
|
)
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
|
|
||||||
def add_listed_selected(self):
|
def add_listed_selected(self):
|
||||||
selected_ids = [
|
selected_ids = [
|
||||||
int(cb.property("product_id"))
|
int(cb.property("product_id"))
|
||||||
for i in range(self.table.rowCount())
|
for i in range(self.table.rowCount())
|
||||||
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox) and cb.isChecked()
|
if isinstance(cb := self.table.cellWidget(i, 0), QCheckBox)
|
||||||
|
and cb.isChecked()
|
||||||
]
|
]
|
||||||
if not selected_ids:
|
if not selected_ids:
|
||||||
QMessageBox.information(self, "Info", "No products selected")
|
QMessageBox.information(self, "Info", "No products selected")
|
||||||
return
|
return
|
||||||
|
|
||||||
dialog = AddListedDialog(selected_ids, parent=self)
|
dialog = AddListedDialog(selected_ids, parent=self)
|
||||||
dialog.exec_()
|
dialog.exec() # PyQt6
|
||||||
|
|
||||||
# --- Clear row checkboxes ---
|
# --- Clear row checkboxes ---
|
||||||
for i in range(self.table.rowCount()):
|
for i in range(self.table.rowCount()):
|
||||||
|
|
@ -387,7 +427,6 @@ class ProductTab(QWidget):
|
||||||
# --- Update menu ---
|
# --- Update menu ---
|
||||||
self.update_options_menu()
|
self.update_options_menu()
|
||||||
|
|
||||||
|
|
||||||
def reset_header_checkbox(self):
|
def reset_header_checkbox(self):
|
||||||
header = self.table.horizontalHeader()
|
header = self.table.horizontalHeader()
|
||||||
if isinstance(header, CheckBoxHeader):
|
if isinstance(header, CheckBoxHeader):
|
||||||
|
|
@ -401,8 +440,7 @@ class ProductTab(QWidget):
|
||||||
return
|
return
|
||||||
|
|
||||||
dialog = AddListedDialog([product_id], parent=self)
|
dialog = AddListedDialog([product_id], parent=self)
|
||||||
dialog.exec_()
|
dialog.exec() # PyQt6
|
||||||
|
|
||||||
|
|
||||||
# --- Pagination ---
|
# --- Pagination ---
|
||||||
def next_page(self):
|
def next_page(self):
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,15 @@
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt6.QtWidgets import (
|
||||||
QDialog, QVBoxLayout, QLabel, QLineEdit,
|
QDialog,
|
||||||
QComboBox, QDialogButtonBox, QMessageBox
|
QVBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QLineEdit,
|
||||||
|
QComboBox,
|
||||||
|
QDialogButtonBox,
|
||||||
|
QMessageBox,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from PyQt5.QtGui import QIntValidator
|
from PyQt6.QtGui import QIntValidator
|
||||||
|
|
||||||
|
|
||||||
class SettingForm(QDialog):
|
class SettingForm(QDialog):
|
||||||
def __init__(self, key: str, value: str, type_: str = "text", parent=None):
|
def __init__(self, key: str, value: str, type_: str = "text", parent=None):
|
||||||
|
|
@ -28,7 +34,9 @@ class SettingForm(QDialog):
|
||||||
if self.type_ == "boolean" or val_lower in ("true", "false"):
|
if self.type_ == "boolean" or val_lower in ("true", "false"):
|
||||||
self.input_widget = QComboBox()
|
self.input_widget = QComboBox()
|
||||||
self.input_widget.addItems(["true", "false"])
|
self.input_widget.addItems(["true", "false"])
|
||||||
self.input_widget.setCurrentText(val_lower if val_lower in ("true", "false") else "true")
|
self.input_widget.setCurrentText(
|
||||||
|
val_lower if val_lower in ("true", "false") else "true"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.input_widget = QLineEdit()
|
self.input_widget = QLineEdit()
|
||||||
self.input_widget.setText(value or "")
|
self.input_widget.setText(value or "")
|
||||||
|
|
@ -38,18 +46,23 @@ class SettingForm(QDialog):
|
||||||
self.layout.addWidget(self.input_widget)
|
self.layout.addWidget(self.input_widget)
|
||||||
|
|
||||||
# --- Buttons ---
|
# --- Buttons ---
|
||||||
buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
buttons = QDialogButtonBox(
|
||||||
|
QDialogButtonBox.StandardButton.Ok | QDialogButtonBox.StandardButton.Cancel
|
||||||
|
)
|
||||||
buttons.accepted.connect(self.on_accept)
|
buttons.accepted.connect(self.on_accept)
|
||||||
buttons.rejected.connect(self.reject)
|
buttons.rejected.connect(self.reject)
|
||||||
self.layout.addWidget(buttons)
|
self.layout.addWidget(buttons)
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
def on_accept(self):
|
def on_accept(self):
|
||||||
|
"""Xử lý khi người dùng nhấn OK"""
|
||||||
if isinstance(self.input_widget, QLineEdit):
|
if isinstance(self.input_widget, QLineEdit):
|
||||||
val = self.input_widget.text()
|
val = self.input_widget.text()
|
||||||
if self.type_ == "number":
|
if self.type_ == "number" and not val.isdigit():
|
||||||
if not val.isdigit():
|
QMessageBox.warning(
|
||||||
QMessageBox.warning(self, "Invalid input", "Please enter a valid number.")
|
self, "Invalid input", "Please enter a valid number."
|
||||||
return
|
)
|
||||||
|
return
|
||||||
elif isinstance(self.input_widget, QComboBox):
|
elif isinstance(self.input_widget, QComboBox):
|
||||||
val = self.input_widget.currentText()
|
val = self.input_widget.currentText()
|
||||||
else:
|
else:
|
||||||
|
|
@ -62,10 +75,12 @@ class SettingForm(QDialog):
|
||||||
self.new_value = val
|
self.new_value = val
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_new_value(key: str, value: str, type_: str = "text", parent=None):
|
def get_new_value(key: str, value: str, type_: str = "text", parent=None):
|
||||||
|
"""Hiển thị dialog và trả về giá trị mới"""
|
||||||
dialog = SettingForm(key, value, type_=type_, parent=parent)
|
dialog = SettingForm(key, value, type_=type_, parent=parent)
|
||||||
result = dialog.exec_()
|
result = dialog.exec() # ✅ PyQt6 dùng exec() thay vì exec_()
|
||||||
if result == QDialog.Accepted:
|
if result == QDialog.DialogCode.Accepted:
|
||||||
return dialog.new_value
|
return dialog.new_value
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,25 @@
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import json
|
from PyQt6.QtWidgets import (
|
||||||
from PyQt5.QtWidgets import (
|
QWidget,
|
||||||
QWidget, QVBoxLayout, QTableWidget, QTableWidgetItem,
|
QVBoxLayout,
|
||||||
QHBoxLayout, QLabel, QPushButton, QMessageBox, QMenu,
|
QTableWidget,
|
||||||
QAction, QHeaderView, QSizePolicy, QLineEdit
|
QTableWidgetItem,
|
||||||
|
QHBoxLayout,
|
||||||
|
QLabel,
|
||||||
|
QPushButton,
|
||||||
|
QMessageBox,
|
||||||
|
QMenu,
|
||||||
|
QHeaderView,
|
||||||
|
QLineEdit,
|
||||||
)
|
)
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
from services.core.loading_service import run_with_progress
|
from services.core.loading_service import run_with_progress
|
||||||
from database.models.setting import Setting
|
from database.models.setting import Setting
|
||||||
from .forms.setting_form import SettingForm
|
from .forms.setting_form import SettingForm
|
||||||
|
|
||||||
PAGE_SIZE = 10
|
PAGE_SIZE = 10
|
||||||
|
|
||||||
# --- Settings Tab ---
|
|
||||||
class SettingsTab(QWidget):
|
class SettingsTab(QWidget):
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
@ -24,18 +31,18 @@ class SettingsTab(QWidget):
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
layout = QVBoxLayout()
|
||||||
|
|
||||||
# Top Layout
|
# --- Top layout (placeholder cho search/filter nếu cần) ---
|
||||||
top_layout = QHBoxLayout()
|
top_layout = QHBoxLayout()
|
||||||
top_layout.addStretch()
|
top_layout.addStretch()
|
||||||
layout.addLayout(top_layout)
|
layout.addLayout(top_layout)
|
||||||
|
|
||||||
# Table
|
# --- Table ---
|
||||||
self.table = QTableWidget()
|
self.table = QTableWidget()
|
||||||
self.table.setEditTriggers(QTableWidget.NoEditTriggers)
|
self.table.setEditTriggers(QTableWidget.EditTrigger.NoEditTriggers)
|
||||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
self.table.setSelectionBehavior(QTableWidget.SelectionBehavior.SelectRows)
|
||||||
layout.addWidget(self.table)
|
layout.addWidget(self.table)
|
||||||
|
|
||||||
# Pagination
|
# --- Pagination ---
|
||||||
pag_layout = QHBoxLayout()
|
pag_layout = QHBoxLayout()
|
||||||
self.prev_btn = QPushButton("Previous")
|
self.prev_btn = QPushButton("Previous")
|
||||||
self.prev_btn.clicked.connect(self.prev_page)
|
self.prev_btn.clicked.connect(self.prev_page)
|
||||||
|
|
@ -57,8 +64,9 @@ class SettingsTab(QWidget):
|
||||||
|
|
||||||
self.setLayout(layout)
|
self.setLayout(layout)
|
||||||
|
|
||||||
# --- Load Data ---
|
# ----------------------------------------------------------------------
|
||||||
def load_data(self, show_progress=True):
|
def load_data(self, show_progress=True):
|
||||||
|
"""Nạp dữ liệu setting vào bảng"""
|
||||||
self.table.clearContents()
|
self.table.clearContents()
|
||||||
self.table.setRowCount(0)
|
self.table.setRowCount(0)
|
||||||
|
|
||||||
|
|
@ -82,12 +90,12 @@ class SettingsTab(QWidget):
|
||||||
|
|
||||||
# ID
|
# ID
|
||||||
id_item = QTableWidgetItem(str(setting_id))
|
id_item = QTableWidgetItem(str(setting_id))
|
||||||
id_item.setFlags(id_item.flags() & ~Qt.ItemIsEditable)
|
id_item.setFlags(id_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
self.table.setItem(i_row, 0, id_item)
|
self.table.setItem(i_row, 0, id_item)
|
||||||
|
|
||||||
# Key
|
# Key
|
||||||
key_item = QTableWidgetItem(key)
|
key_item = QTableWidgetItem(key)
|
||||||
key_item.setFlags(key_item.flags() & ~Qt.ItemIsEditable)
|
key_item.setFlags(key_item.flags() & ~Qt.ItemFlag.ItemIsEditable)
|
||||||
self.table.setItem(i_row, 1, key_item)
|
self.table.setItem(i_row, 1, key_item)
|
||||||
|
|
||||||
# Value
|
# Value
|
||||||
|
|
@ -99,32 +107,42 @@ class SettingsTab(QWidget):
|
||||||
btn_menu.setMaximumWidth(120)
|
btn_menu.setMaximumWidth(120)
|
||||||
menu = QMenu(btn_menu)
|
menu = QMenu(btn_menu)
|
||||||
act_edit = menu.addAction("Edit")
|
act_edit = menu.addAction("Edit")
|
||||||
act_edit.triggered.connect(partial(self.edit_setting, setting_id, key, i_row, type_))
|
act_edit.triggered.connect(
|
||||||
|
partial(self.edit_setting, setting_id, key, i_row, type_)
|
||||||
|
)
|
||||||
btn_menu.setMenu(menu)
|
btn_menu.setMenu(menu)
|
||||||
self.table.setCellWidget(i_row, 3, btn_menu)
|
self.table.setCellWidget(i_row, 3, btn_menu)
|
||||||
|
|
||||||
items_with_index = [(s, i) for i, s in enumerate(settings)]
|
items_with_index = [(s, i) for i, s in enumerate(settings)]
|
||||||
if show_progress:
|
if show_progress:
|
||||||
run_with_progress(items_with_index, handler=lambda x: handler(*x), message="Loading settings...", parent=self)
|
run_with_progress(
|
||||||
|
items_with_index,
|
||||||
|
handler=lambda x: handler(*x),
|
||||||
|
message="Loading settings...",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
for item in items_with_index:
|
for item in items_with_index:
|
||||||
handler(*item)
|
handler(*item)
|
||||||
|
|
||||||
# Resize header
|
# --- Resize header ---
|
||||||
header = self.table.horizontalHeader()
|
header = self.table.horizontalHeader()
|
||||||
header.setSectionResizeMode(0, QHeaderView.ResizeToContents) # ID
|
header.setSectionResizeMode(0, QHeaderView.ResizeMode.ResizeToContents) # ID
|
||||||
header.setSectionResizeMode(1, QHeaderView.Stretch) # Key
|
header.setSectionResizeMode(1, QHeaderView.ResizeMode.Stretch) # Key
|
||||||
header.setSectionResizeMode(2, QHeaderView.Stretch) # Value
|
header.setSectionResizeMode(2, QHeaderView.ResizeMode.Stretch) # Value
|
||||||
header.setSectionResizeMode(3, QHeaderView.Fixed) # Action
|
header.setSectionResizeMode(3, QHeaderView.ResizeMode.Fixed) # Action
|
||||||
self.table.setColumnWidth(3, 120)
|
self.table.setColumnWidth(3, 120)
|
||||||
|
|
||||||
# Pagination
|
# --- Pagination ---
|
||||||
self.prev_btn.setEnabled(self.current_page > 0)
|
self.prev_btn.setEnabled(self.current_page > 0)
|
||||||
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
self.next_btn.setEnabled(self.current_page < self.total_pages - 1)
|
||||||
self.page_info_label.setText(f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)")
|
self.page_info_label.setText(
|
||||||
|
f"Page {self.current_page + 1} / {self.total_pages} ({self.total_count} items)"
|
||||||
|
)
|
||||||
|
|
||||||
# --- Edit setting ---
|
# ----------------------------------------------------------------------
|
||||||
def edit_setting(self, setting_id, key, row, type_):
|
def edit_setting(self, setting_id, key, row, type_):
|
||||||
|
"""Chỉnh sửa giá trị setting"""
|
||||||
old_value = self.table.item(row, 2).text()
|
old_value = self.table.item(row, 2).text()
|
||||||
new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self)
|
new_value = SettingForm.get_new_value(key, old_value, type_=type_, parent=self)
|
||||||
if new_value is not None and new_value != old_value:
|
if new_value is not None and new_value != old_value:
|
||||||
|
|
@ -134,8 +152,9 @@ class SettingsTab(QWidget):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", f"Failed to update setting: {e}")
|
QMessageBox.critical(self, "Error", f"Failed to update setting: {e}")
|
||||||
|
|
||||||
# --- Pagination ---
|
# ----------------------------------------------------------------------
|
||||||
def go_to_page(self):
|
def go_to_page(self):
|
||||||
|
"""Nhảy đến trang cụ thể"""
|
||||||
try:
|
try:
|
||||||
page = int(self.page_input.text()) - 1
|
page = int(self.page_input.text()) - 1
|
||||||
if 0 <= page < self.total_pages:
|
if 0 <= page < self.total_pages:
|
||||||
|
|
@ -145,11 +164,13 @@ class SettingsTab(QWidget):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def next_page(self):
|
def next_page(self):
|
||||||
|
"""Trang kế"""
|
||||||
if self.current_page < self.total_pages - 1:
|
if self.current_page < self.total_pages - 1:
|
||||||
self.current_page += 1
|
self.current_page += 1
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
def prev_page(self):
|
def prev_page(self):
|
||||||
|
"""Trang trước"""
|
||||||
if self.current_page > 0:
|
if self.current_page > 0:
|
||||||
self.current_page -= 1
|
self.current_page -= 1
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
PyQt5==5.15.11
|
PyQt6==6.7.0
|
||||||
PyQtWebEngine==5.15.6
|
PyQt6-WebEngine==6.7.0
|
||||||
opencv-python==4.10.0.84
|
opencv-python==4.10.0.84
|
||||||
numpy==1.26.4
|
numpy==1.26.4
|
||||||
SQLAlchemy==2.0.22
|
SQLAlchemy==2.0.22
|
||||||
requests
|
requests
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
from PyQt5.QtCore import Qt, QTimer
|
from PyQt6.QtCore import Qt, QTimer
|
||||||
|
|
||||||
|
|
||||||
class ActionService:
|
class ActionService:
|
||||||
|
|
@ -16,7 +16,7 @@ class ActionService:
|
||||||
self.delay = delay
|
self.delay = delay
|
||||||
|
|
||||||
# ----------------------------------------------------------------------
|
# ----------------------------------------------------------------------
|
||||||
def _run_js(self, script):
|
def _run_js(self, script: str):
|
||||||
"""Chạy JavaScript trên webview"""
|
"""Chạy JavaScript trên webview"""
|
||||||
if not self.webview:
|
if not self.webview:
|
||||||
print("[WARN] Không có webview để chạy JS.")
|
print("[WARN] Không có webview để chạy JS.")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,10 @@
|
||||||
from PyQt5.QtWidgets import QProgressDialog
|
from PyQt6.QtWidgets import QProgressDialog
|
||||||
from PyQt5.QtCore import Qt, QCoreApplication
|
from PyQt6.QtCore import Qt, QCoreApplication
|
||||||
|
|
||||||
def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel", parent=None):
|
|
||||||
|
def run_with_progress(
|
||||||
|
items, handler, message="Loading...", cancel_text="Cancel", parent=None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Run the handler for each item in `items` and display a loading progress bar.
|
Run the handler for each item in `items` and display a loading progress bar.
|
||||||
- items: iterable (list, tuple, ...)
|
- items: iterable (list, tuple, ...)
|
||||||
|
|
@ -15,7 +18,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel"
|
||||||
return 0, 0
|
return 0, 0
|
||||||
|
|
||||||
progress = QProgressDialog(message, cancel_text, 0, total, parent)
|
progress = QProgressDialog(message, cancel_text, 0, total, parent)
|
||||||
progress.setWindowModality(Qt.WindowModal)
|
progress.setWindowModality(Qt.WindowModality.WindowModal) # PyQt6: enum thay đổi
|
||||||
progress.setMinimumDuration(0)
|
progress.setMinimumDuration(0)
|
||||||
progress.setValue(0)
|
progress.setValue(0)
|
||||||
|
|
||||||
|
|
@ -33,7 +36,7 @@ def run_with_progress(items, handler, message="Loading...", cancel_text="Cancel"
|
||||||
fail_count += 1
|
fail_count += 1
|
||||||
|
|
||||||
progress.setValue(i + 1)
|
progress.setValue(i + 1)
|
||||||
QCoreApplication.processEvents() # Prevent UI freezing
|
QCoreApplication.processEvents() # Giữ UI không bị treo
|
||||||
|
|
||||||
progress.close()
|
progress.close()
|
||||||
return success_count, fail_count
|
return success_count, fail_count
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
# services/image_service.py
|
# services/image_service.py
|
||||||
import os
|
import os
|
||||||
import requests
|
import requests
|
||||||
from PyQt5.QtGui import QPixmap
|
from PyQt6.QtGui import QPixmap
|
||||||
from PyQt5.QtCore import Qt
|
from PyQt6.QtCore import Qt
|
||||||
|
|
||||||
|
|
||||||
class ImageService:
|
class ImageService:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -35,7 +36,13 @@ class ImageService:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if not pixmap.isNull():
|
if not pixmap.isNull():
|
||||||
pixmap = pixmap.scaled(size, size, Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
# PyQt6: enum cần gọi qua Qt.AspectRatioMode và Qt.TransformationMode
|
||||||
|
pixmap = pixmap.scaled(
|
||||||
|
size,
|
||||||
|
size,
|
||||||
|
Qt.AspectRatioMode.KeepAspectRatio,
|
||||||
|
Qt.TransformationMode.SmoothTransformation,
|
||||||
|
)
|
||||||
return pixmap
|
return pixmap
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ class ProfileService:
|
||||||
Service để quản lý thư mục profiles.
|
Service để quản lý thư mục profiles.
|
||||||
Mặc định root folder lấy từ config.PROFILES_DIR.
|
Mặc định root folder lấy từ config.PROFILES_DIR.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_dir = PROFILES_DIR
|
base_dir = PROFILES_DIR
|
||||||
|
|
||||||
def __init__(self, profiles_root: Optional[str] = None):
|
def __init__(self, profiles_root: Optional[str] = None):
|
||||||
|
|
@ -47,7 +47,7 @@ class ProfileService:
|
||||||
def get_profile_dirname(self, name: str) -> str:
|
def get_profile_dirname(self, name: str) -> str:
|
||||||
"""Tên folder đã sanitize (chỉ tên folder, không có path)"""
|
"""Tên folder đã sanitize (chỉ tên folder, không có path)"""
|
||||||
return _sanitize_name(name)
|
return _sanitize_name(name)
|
||||||
|
|
||||||
def save_profile(self, key: str):
|
def save_profile(self, key: str):
|
||||||
# ở đây có thể không cần làm gì nhiều vì Qt tự lưu cookie
|
# ở đây có thể không cần làm gì nhiều vì Qt tự lưu cookie
|
||||||
# nhưng bạn có thể log hoặc thêm custom logic
|
# nhưng bạn có thể log hoặc thêm custom logic
|
||||||
|
|
@ -62,7 +62,9 @@ class ProfileService:
|
||||||
"""Check folder có tồn tại không"""
|
"""Check folder có tồn tại không"""
|
||||||
return os.path.isdir(self.get_profile_path(name))
|
return os.path.isdir(self.get_profile_path(name))
|
||||||
|
|
||||||
def create(self, name: str, copy_from: Optional[str] = None, exist_ok: bool = True) -> str:
|
def create(
|
||||||
|
self, name: str, copy_from: Optional[str] = None, exist_ok: bool = True
|
||||||
|
) -> str:
|
||||||
"""
|
"""
|
||||||
Tạo folder profile.
|
Tạo folder profile.
|
||||||
- name: email/username
|
- name: email/username
|
||||||
|
|
@ -118,19 +120,21 @@ class ProfileService:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
# ----------------- Optional: QWebEngineProfile creator -----------------
|
# ----------------- Optional: QWebEngineProfile creator -----------------
|
||||||
def create_qwebengine_profile(self, name: str, parent=None, profile_id: Optional[str] = None):
|
def create_qwebengine_profile(
|
||||||
|
self, name: str, parent=None, profile_id: Optional[str] = None
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Tạo và trả về QWebEngineProfile đã cấu hình persistent storage (cookies, local storage, cache).
|
Tạo và trả về QWebEngineProfile đã cấu hình persistent storage (cookies, local storage, cache).
|
||||||
Yêu cầu PyQt5.QtWebEngineWidgets được cài.
|
Yêu cầu PyQt6.QtWebEngineWidgets được cài.
|
||||||
- name: email/username để đặt thư mục lưu
|
- name: email/username để đặt thư mục lưu
|
||||||
- parent: parent QObject cho QWebEngineProfile (thường là self)
|
- parent: parent QObject cho QWebEngineProfile (thường là self)
|
||||||
- profile_id: tên id cho profile (tùy chọn)
|
- profile_id: tên id cho profile (tùy chọn)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
from PyQt5.QtWebEngineWidgets import QWebEngineProfile
|
from PyQt6.QtWebEngineWidgets import QWebEngineProfile
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise RuntimeError(
|
raise RuntimeError(
|
||||||
"PyQt5.QtWebEngineWidgets không khả dụng. "
|
"PyQt6.QtWebEngineWidgets không khả dụng. "
|
||||||
"Không thể tạo QWebEngineProfile."
|
"Không thể tạo QWebEngineProfile."
|
||||||
) from e
|
) from e
|
||||||
|
|
||||||
|
|
@ -143,8 +147,13 @@ class ProfileService:
|
||||||
profile.setCachePath(profile_path)
|
profile.setCachePath(profile_path)
|
||||||
# Force lưu cookie persist
|
# Force lưu cookie persist
|
||||||
try:
|
try:
|
||||||
profile.setPersistentCookiesPolicy(QWebEngineProfile.ForcePersistentCookies)
|
from PyQt6.QtWebEngineCore import QWebEngineProfile as CoreProfile
|
||||||
|
|
||||||
|
profile.setPersistentCookiesPolicy(
|
||||||
|
CoreProfile.PersistentCookiesPolicy.ForcePersistentCookies
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path)
|
logger.info("Created QWebEngineProfile for %s -> %s", name, profile_path)
|
||||||
return profile
|
return profile
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from PyQt5.QtCore import QObject
|
from PyQt6.QtCore import QObject
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
|
|
||||||
class SharedStore(QObject):
|
class SharedStore(QObject):
|
||||||
_instance = None
|
_instance = None
|
||||||
_lock = threading.Lock()
|
_lock = threading.Lock()
|
||||||
|
|
@ -24,7 +25,7 @@ class SharedStore(QObject):
|
||||||
|
|
||||||
def remove(self, listed_id: int):
|
def remove(self, listed_id: int):
|
||||||
with self._items_lock:
|
with self._items_lock:
|
||||||
self._items = [i for i in self._items if i["listed_id"] != listed_id]
|
self._items = [i for i in self._items if i.get("listed_id") != listed_id]
|
||||||
|
|
||||||
def size(self) -> int:
|
def size(self) -> int:
|
||||||
with self._items_lock:
|
with self._items_lock:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue