From 24b2290636e19fd4a347081c86b8bcca07b05557 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= <168283529+elilef@users.noreply.github.com> Date: Tue, 5 May 2026 22:04:49 +0300 Subject: [PATCH 01/14] Commit for project SWE in README.md This project was set up and tested locally by Berre for the SWE Spring 2026 project. --- docs/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/README.md b/docs/README.md index a0ef095108..4725409f45 100644 --- a/docs/README.md +++ b/docs/README.md @@ -113,3 +113,4 @@ Creator - [Siddharth Dushantha](https://github.com/sdushantha) [ext_pypi]: https://pypi.org/project/sherlock-project/ [ext_brew]: https://formulae.brew.sh/formula/sherlock + From 86704e2f2657f6d9ec8ba2d9d6ffc4e313028f5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Mon, 25 May 2026 21:05:13 +0300 Subject: [PATCH 02/14] feat: Add initial PyQt5 GUI skeleton and mock search thread --- sherlock_project/gui_app.py | 201 ++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 sherlock_project/gui_app.py diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py new file mode 100644 index 0000000000..7cccd59ce5 --- /dev/null +++ b/sherlock_project/gui_app.py @@ -0,0 +1,201 @@ +import sys +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, + QHBoxLayout, QLineEdit, QPushButton, QTableWidget, + QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView) +from PyQt5.QtCore import Qt + +class SherlockGUI(QMainWindow): + def __init__(self): + super().__init__() + self.initUI() + self.apply_styles() + + def initUI(self): + # Main window settings + self.setWindowTitle("Sherlock OSINT Dashboard") + self.resize(800, 600) + + # Central widget and main layout + central_widget = QWidget() + self.setCentralWidget(central_widget) + main_layout = QVBoxLayout() + main_layout.setContentsMargins(30, 30, 30, 30) + main_layout.setSpacing(20) + + # Top section: Heading + title_label = QLabel("🕵️‍♂️Sherlock Username Search") + title_label.setAlignment(Qt.AlignCenter) + title_label.setObjectName("TitleLabel") + main_layout.addWidget(title_label) + + # Search bar and button for horizontal layout + search_layout = QHBoxLayout() + search_layout.setSpacing(15) + + self.username_input = QLineEdit() + self.username_input.setPlaceholderText("Enter the username to search (e.g., elif123)") + + self.search_button = QPushButton("Search") + self.search_button.setCursor(Qt.PointingHandCursor) + # We are attaching the function that will run when the button is clicked (Signal-Slot logic). + self.search_button.clicked.connect(self.start_search_mock) + + search_layout.addWidget(self.username_input) + search_layout.addWidget(self.search_button) + main_layout.addLayout(search_layout) + + # Bottom section: Results table + self.result_table = QTableWidget() + self.result_table.setColumnCount(3) + self.result_table.setHorizontalHeaderLabels(["Platform", "Status", "URL"]) + + # Resize table columns proportionally to the window + self.result_table.setEditTriggers(QAbstractItemView.NoEditTriggers) + self.result_table.setSelectionBehavior(QAbstractItemView.SelectRows) + self.result_table.setShowGrid(False) + self.result_table.verticalHeader().setVisible(False) + + + header = self.result_table.horizontalHeader() + header.setSectionResizeMode(0, QHeaderView.ResizeToContents) + header.setSectionResizeMode(1, QHeaderView.ResizeToContents) + header.setSectionResizeMode(2, QHeaderView.Stretch) + + main_layout.addWidget(self.result_table) + + # Assign main layout to central widget + central_widget.setLayout(main_layout) + + + def apply_styles(self): + self.setStyleSheet(""" + /* Main Background */ + QMainWindow { + background-color: #1e1e2e; + } + /* General Font and Color */ + QWidget { + font-family: 'Segoe UI', Arial, sans-serif; + color: #cdd6f4; + } + /* Title Customization */ + #titleLabel { + font-size: 28px; + font-weight: bold; + color: #89b4fa; + margin-bottom: 15px; + } + /* Text Input Field */ + QLineEdit { + background-color: #313244; + border: 2px solid #45475a; + border-radius: 8px; + padding: 12px 15px; + font-size: 15px; + color: #cdd6f4; + } + QLineEdit:focus { + border: 2px solid #89b4fa; + background-color: #1e1e2e; + } + /* Search Button */ + QPushButton { + background-color: #89b4fa; + color: #1e1e2e; + border: none; + border-radius: 8px; + padding: 12px 25px; + font-size: 15px; + font-weight: bold; + } + QPushButton:hover { + background-color: #b4befe; + } + QPushButton:pressed { + background-color: #74c7ec; + } + /* Table General Settings */ + QTableWidget { + background-color: #1e1e2e; + border: 1px solid #45475a; + border-radius: 8px; + font-size: 14px; + outline: none; + } + /* Table Rows */ + QTableWidget::item { + padding: 10px; + border-bottom: 1px solid #313244; + } + QTableWidget::item:selected { + background-color: #313244; + color: #89b4fa; + } + /* Table Header */ + QHeaderView::section { + background-color: #313244; + color: #a6adc8; + padding: 12px; + border: none; + font-weight: bold; + font-size: 15px; + text-align: left; + } + /* Scrollbar Aesthetics */ + QScrollBar:vertical { + border: none; + background: #1e1e2e; + width: 12px; + border-radius: 6px; + } + QScrollBar::handle:vertical { + background: #45475a; + min-height: 30px; + border-radius: 6px; + } + QScrollBar::handle:vertical:hover { + background: #585b70; + } + """) + + + + + def start_search_mock(self): + """ + This function is for testing purposes only. + Later, we will connect it to the original sherlock.py. + """ + username = self.username_input.text() + + if not username: + return + + + # Clean the table and add 2 rows of fake data for testing. + self.result_table.setRowCount(0) + + mock_results = [ + {"site": "GitHub", "status": "Found", "url": f"https://github.com/{username}"}, + {"site": "Instagram", "status": "Not Found", "url": "-"} + ] + + for row_idx, result in enumerate(mock_results): + self.result_table.insertRow(row_idx) + + # Durum kısmına daha estetik görünmesi için emoji ekledik + status_text = "✅ Found" if result["status"] == "Found" else "❌ Not Found" + + self.result_table.setItem(row_idx, 0, QTableWidgetItem(result["site"])) + self.result_table.setItem(row_idx, 1, QTableWidgetItem(status_text)) + self.result_table.setItem(row_idx, 2, QTableWidgetItem(result["url"])) + +def run_gui(): + app = QApplication(sys.argv) + window = SherlockGUI() + window.show() + sys.exit(app.exec_()) + +# If this file is executed directly, the GUI should open. +if __name__ == "__main__": + run_gui() \ No newline at end of file From 52253598b8227d20d96ba2453bf2b8c6258427c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Mon, 25 May 2026 21:56:53 +0300 Subject: [PATCH 03/14] feat: Add PyQt5 GUI integration and dynamic sorting algorithm for search results --- sherlock_project/gui_app.py | 113 ++++++++++++++++++++++++++++-------- sherlock_project/notify.py | 24 ++++++++ 2 files changed, 114 insertions(+), 23 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 7cccd59ce5..38e447e445 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -1,8 +1,20 @@ import sys +import os +import json + +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from sherlock_project.notify import QueryNotifyGUI +from sherlock_project.sherlock import sherlock +from sherlock_project.sites import SitesInformation + from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView) -from PyQt5.QtCore import Qt + + +from PyQt5.QtCore import Qt, QThread, pyqtSignal + class SherlockGUI(QMainWindow): def __init__(self): @@ -38,7 +50,7 @@ def initUI(self): self.search_button = QPushButton("Search") self.search_button.setCursor(Qt.PointingHandCursor) # We are attaching the function that will run when the button is clicked (Signal-Slot logic). - self.search_button.clicked.connect(self.start_search_mock) + self.search_button.clicked.connect(self.start_search) search_layout.addWidget(self.username_input) search_layout.addWidget(self.search_button) @@ -160,35 +172,90 @@ def apply_styles(self): - - def start_search_mock(self): - """ - This function is for testing purposes only. - Later, we will connect it to the original sherlock.py. - """ - username = self.username_input.text() - + def start_search(self): + username = self.username_input.text().strip() if not username: return - - # Clean the table and add 2 rows of fake data for testing. self.result_table.setRowCount(0) + self.search_button.setEnabled(False) + self.search_button.setText("Searching...") + + self.worker = SherlockWorker(username) + self.worker.result_signal.connect(self.add_result_to_table) + self.worker.finished_signal.connect(self.search_finished) + self.worker.start() + + def add_result_to_table(self, site, status, url): + status_text = "✅ Found" if status == "Found" else "❌ Not Found" + + insert_row = self.result_table.rowCount() - mock_results = [ - {"site": "GitHub", "status": "Found", "url": f"https://github.com/{username}"}, - {"site": "Instagram", "status": "Not Found", "url": "-"} - ] + for i in range(self.result_table.rowCount()): + current_site = self.result_table.item(i, 0).text() + current_status = self.result_table.item(i, 1).text() + + if status_text == "✅ Found": + # Rule 1: The newly entered "Found" data should be placed above the first "Not Found" row in the table. + if current_status == "❌ Not Found": + insert_row = i + break + # Rule 2: Claimed sites should be sorted alphabetically (A-Z) within the found section + elif current_site.lower() > site.lower(): + insert_row = i + break + else: + # Rule 3: Newly entered "Not Found" entries should skip over existing "Found" rows - for row_idx, result in enumerate(mock_results): - self.result_table.insertRow(row_idx) + if current_status == "✅ Found": + continue + # Rule 4: "Not Found" sites should be sorted alphabetically (A-Z) within their own section + if current_site.lower() > site.lower(): + insert_row = i + break + + self.result_table.insertRow(insert_row) + self.result_table.setItem(insert_row, 0, QTableWidgetItem(site)) + self.result_table.setItem(insert_row, 1, QTableWidgetItem(status_text)) + self.result_table.setItem(insert_row, 2, QTableWidgetItem(url)) + self.result_table.scrollToBottom() + + def search_finished(self): + self.search_button.setEnabled(True) + self.search_button.setText("Search") + print("[*] Search completed.") + +class SherlockWorker(QThread): + result_signal = pyqtSignal(str, str, str) + finished_signal = pyqtSignal() + + def __init__(self, username): + super().__init__() + self.username = username + + def run(self): + print(f"[*] Running background search for {self.username}...") + + gui_notifier = QueryNotifyGUI(self.result_signal) + + try: + with open("sherlock_project/resources/data.json", "r", encoding="utf-8") as f: + site_data = json.load(f) - # Durum kısmına daha estetik görünmesi için emoji ekledik - status_text = "✅ Found" if result["status"] == "Found" else "❌ Not Found" + if "$schema" in site_data: + del site_data["$schema"] + + except Exception as e: + print(f"Site data could not be read: {e}") + self.finished_signal.emit() + return - self.result_table.setItem(row_idx, 0, QTableWidgetItem(result["site"])) - self.result_table.setItem(row_idx, 1, QTableWidgetItem(status_text)) - self.result_table.setItem(row_idx, 2, QTableWidgetItem(result["url"])) + # We are triggering the main search engine. + sherlock(self.username, site_data, gui_notifier, timeout=60) + + self.finished_signal.emit() + + def run_gui(): app = QApplication(sys.argv) diff --git a/sherlock_project/notify.py b/sherlock_project/notify.py index ab6f5a3866..df6d1e4b82 100644 --- a/sherlock_project/notify.py +++ b/sherlock_project/notify.py @@ -277,3 +277,27 @@ def __str__(self): Nicely formatted string to get information about this object. """ return str(self.result) + + +class QueryNotifyGUI: + """ + Instead of printing to the terminal, a notification class that sends data to the desktop interface via PyQt signals (QThread). + """ + def __init__(self, result_signal): + # We receive the communication cable (signal) from the interface. + self.result_signal = result_signal + + def start(self, message): + pass + + def update(self, result): + # This function is automatically triggered when Sherlock scans a site. + # result is an object of type QueryResult() containing results for this query. + + status_str = "Found" if result.status.name == "CLAIMED" else "Not Found" + + # Instead of printing to the terminal, we emit a signal to the interface's table. + self.result_signal.emit(result.site_name, status_str, result.site_url_user) + + def finish(self, message): + pass From 4e141ec744fdd1f27697bab916533ab0ebf1eb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Thu, 28 May 2026 15:36:01 +0300 Subject: [PATCH 04/14] feat: Implement CLI/GUI routing logic and version check in __main__.py --- sherlock_project/__main__.py | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/sherlock_project/__main__.py b/sherlock_project/__main__.py index a252de0fc1..95f3d0bd78 100644 --- a/sherlock_project/__main__.py +++ b/sherlock_project/__main__.py @@ -8,15 +8,33 @@ """ import sys +import os +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) -if __name__ == "__main__": - # Check if the user is using the correct version of Python +# First, we check the Python version so that the program gives a warning from the start if the system is incompatible. +if sys.version_info < (3, 9): python_version = sys.version.split()[0] + print(f"Sherlock requires Python 3.9+\nYou are using Python {python_version}, which is not supported by Sherlock.") + sys.exit(1) + +from sherlock_project import sherlock + +if __name__ == "__main__": + # If the user did not enter any parameters or typed --gui, then start the GUI + if len(sys.argv) == 1 or "--gui" in sys.argv: + print("Starting Sherlock GUI...") - if sys.version_info < (3, 9): - print(f"Sherlock requires Python 3.9+\nYou are using Python {python_version}, which is not supported by Sherlock.") - sys.exit(1) + # To prevent PyQt from giving an error, we delete the --gui parameter that we added ourselves. + if "--gui" in sys.argv: + sys.argv.remove("--gui") + + # We call the function that will launch the GUI. + # (We specifically included the import process here so as not to slow down those who only want to use the terminal.) + from sherlock_project.gui_app import run_gui + run_gui() - from sherlock_project import sherlock - sherlock.main() + else: + # We execute the original Sherlock code if the user enters standard arguments from the command line. + sys.exit(sherlock.main()) + From 67fa9f7fca0a2947646a0fdd3586d5f4c153be4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Sat, 30 May 2026 16:11:40 +0300 Subject: [PATCH 05/14] feat: Add progress_callback parameter to core engine for real-time GUI tracking This commit updates the inner iteration loop in sherlock.py to trigger an optional callback function upon completing each site check. This allows the PyQt5 GUI to update its progress bar synchronously without breaking the traditional CLI execution mode. --- sherlock_project/gui_app.py | 33 ++++++++++++++++++++++++--------- sherlock_project/sherlock.py | 7 +++++++ 2 files changed, 31 insertions(+), 9 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 38e447e445..6cb5b6b9b9 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -10,7 +10,7 @@ from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLineEdit, QPushButton, QTableWidget, - QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView) + QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView, QProgressBar) from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -75,6 +75,13 @@ def initUI(self): main_layout.addWidget(self.result_table) + # Progress Bar Settings + self.progress_bar = QProgressBar() + self.progress_bar.setValue(0) + self.progress_bar.setTextVisible(True) + self.progress_bar.hide() + main_layout.addWidget(self.progress_bar) + # Assign main layout to central widget central_widget.setLayout(main_layout) @@ -184,6 +191,8 @@ def start_search(self): self.worker = SherlockWorker(username) self.worker.result_signal.connect(self.add_result_to_table) self.worker.finished_signal.connect(self.search_finished) + self.progress_bar.show() + self.worker.progress_signal.connect(self.update_progress) self.worker.start() def add_result_to_table(self, site, status, url): @@ -224,39 +233,45 @@ def search_finished(self): self.search_button.setEnabled(True) self.search_button.setText("Search") print("[*] Search completed.") + + def update_progress(self, current, total): + self.progress_bar.setMaximum(total) + self.progress_bar.setValue(current) class SherlockWorker(QThread): result_signal = pyqtSignal(str, str, str) finished_signal = pyqtSignal() + progress_signal = pyqtSignal(int, int) def __init__(self, username): super().__init__() self.username = username + self.checked_count = 0 def run(self): - print(f"[*] Running background search for {self.username}...") - gui_notifier = QueryNotifyGUI(self.result_signal) try: with open("sherlock_project/resources/data.json", "r", encoding="utf-8") as f: site_data = json.load(f) - if "$schema" in site_data: del site_data["$schema"] - except Exception as e: - print(f"Site data could not be read: {e}") self.finished_signal.emit() return - # We are triggering the main search engine. - sherlock(self.username, site_data, gui_notifier, timeout=60) + total_sites = len(site_data) + + # The Callback function that Sherlock.py will call at the end of each site visit. + def progress_callback(): + self.checked_count += 1 + self.progress_signal.emit(self.checked_count, total_sites) + # We are sending the callback as a parameter. + sherlock(self.username, site_data, gui_notifier, timeout=60, progress_callback=progress_callback) self.finished_signal.emit() - def run_gui(): app = QApplication(sys.argv) window = SherlockGUI() diff --git a/sherlock_project/sherlock.py b/sherlock_project/sherlock.py index e037d39458..af0d93499e 100644 --- a/sherlock_project/sherlock.py +++ b/sherlock_project/sherlock.py @@ -177,6 +177,9 @@ def sherlock( dump_response: bool = False, proxy: Optional[str] = None, timeout: int = 60, + # Callback function that Sherlock.py will call at the end of each site visit. This is used to update the progress bar in the GUI. + progress_callback=None, + ) -> dict[str, dict[str, str | QueryResult]]: """Run Sherlock Analysis. @@ -502,6 +505,10 @@ def sherlock( # Add this site's results into final dictionary with all of the other results. results_total[social_network] = results_site + # We send a signal to the interface when each site's cycle ends. + if progress_callback: + progress_callback() + return results_total From e101c3ba29ab409714ddc1faabdbfeef74cc7604 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Sun, 31 May 2026 16:07:50 +0300 Subject: [PATCH 06/14] refactor: Update QueryNotifyGUI to inherit from base QueryNotify and fix parameter scope This ensures compatibility with the core sherlock engine by properly inheriting the notification base class and adding optional parameters to prevent runtime TypeErrors. --- sherlock_project/notify.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/sherlock_project/notify.py b/sherlock_project/notify.py index df6d1e4b82..39c5754fcf 100644 --- a/sherlock_project/notify.py +++ b/sherlock_project/notify.py @@ -278,26 +278,27 @@ def __str__(self): """ return str(self.result) - -class QueryNotifyGUI: +class QueryNotifyGUI(QueryNotify): """ - Instead of printing to the terminal, a notification class that sends data to the desktop interface via PyQt signals (QThread). + Instead of printing to the terminal, a notification class that sends data + to the desktop interface via PyQt signals (QThread). """ def __init__(self, result_signal): + super().__init__() # We receive the communication cable (signal) from the interface. self.result_signal = result_signal - def start(self, message): + def start(self, message=None): pass def update(self, result): # This function is automatically triggered when Sherlock scans a site. # result is an object of type QueryResult() containing results for this query. - status_str = "Found" if result.status.name == "CLAIMED" else "Not Found" + status_str = "Found" if result.status == QueryStatus.CLAIMED else "Not Found" # Instead of printing to the terminal, we emit a signal to the interface's table. self.result_signal.emit(result.site_name, status_str, result.site_url_user) - def finish(self, message): + def finish(self, message=None): pass From 4886d2fcfa1c4b915300acfa5b76cbbed0452a0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Sun, 31 May 2026 16:17:59 +0300 Subject: [PATCH 07/14] fix: Force progress bar to 100% on search completion This commit resolves a visual bug where the progress bar would not fully complete if certain target sites were skipped by the core engine's regex checks. The search_finished method now explicitly sets the progress bar to its maximum value upon receiving the completion signal. --- sherlock_project/gui_app.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 6cb5b6b9b9..3f9ef80a6f 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -232,6 +232,7 @@ def add_result_to_table(self, site, status, url): def search_finished(self): self.search_button.setEnabled(True) self.search_button.setText("Search") + self.progress_bar.setValue(self.progress_bar.maximum()) print("[*] Search completed.") def update_progress(self, current, total): From aa68601d9be8627762fca15b8c43067f3cc49f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 15:11:14 +0300 Subject: [PATCH 08/14] feat: Add GUI-friendly data formatting to QueryResult in result.py Implemented to_dict() and get_ui_color_code() methods to safely export query results as structured dictionaries with status-based hex color codes for the PyQt5 interface. This reduces frontend parsing logic and satisfies the existing file contribution requirement. --- sherlock_project/result.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sherlock_project/result.py b/sherlock_project/result.py index c4d68b1c88..35a1b389c3 100644 --- a/sherlock_project/result.py +++ b/sherlock_project/result.py @@ -87,3 +87,33 @@ def __str__(self): status += f" ({self.context})" return status + + def to_dict(self): + """ + Converts the QueryResult object to a dictionary format suitable for GUI parsing. + This prevents the GUI from needing to access raw object attributes directly. + """ + return { + "username": self.username, + "site_name": self.site_name, + "site_url_user": self.site_url_user, + "status": str(self.status), + "query_time": self.query_time, + "context": self.context, + "ui_color_code": self.get_ui_color_code() + } + + def get_ui_color_code(self): + """ + Returns a hex color code based on the query status for UI representation. + Green for Found, Red for Not Found, Yellow for Unknown/Errors. + """ + + if self.status.name == "CLAIMED": + return "#28a745" # Success Green + elif self.status.name == "AVAILABLE": + return "#dc3545" # Danger Red + elif self.status.name == "UNKNOWN": + return "#ffc107" # Warning Yellow + else: + return "#6c757d" # Secondary Gray for other statuses (ILLEGAL, WAF, etc.) From ed168e7be0dc4616c6ce25a36e15dbda931fe0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 15:26:44 +0300 Subject: [PATCH 09/14] feat: Integrate dynamic UI color codes from QueryResult to GUI table Updated QueryNotifyGUI to utilize the to_dict() method for extracting hex color codes. Modified the PyQt5 result_signal and add_result_to_table functions to parse these color codes and dynamically paint the table rows (Green for Found, Red for Not Found). --- sherlock_project/gui_app.py | 25 +++++++++++++++++++------ sherlock_project/notify.py | 12 ++++++++++++ 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 3f9ef80a6f..6f41c92484 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -12,7 +12,7 @@ QHBoxLayout, QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView, QProgressBar) - +from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -195,7 +195,7 @@ def start_search(self): self.worker.progress_signal.connect(self.update_progress) self.worker.start() - def add_result_to_table(self, site, status, url): + def add_result_to_table(self, site, status, url, color_code): status_text = "✅ Found" if status == "Found" else "❌ Not Found" insert_row = self.result_table.rowCount() @@ -224,9 +224,22 @@ def add_result_to_table(self, site, status, url): break self.result_table.insertRow(insert_row) - self.result_table.setItem(insert_row, 0, QTableWidgetItem(site)) - self.result_table.setItem(insert_row, 1, QTableWidgetItem(status_text)) - self.result_table.setItem(insert_row, 2, QTableWidgetItem(url)) + + # 1. We create cell objects. + site_item = QTableWidgetItem(site) + status_item = QTableWidgetItem(status_text) + url_item = QTableWidgetItem(url) + + # 2. We apply color coding to the text based on the status (green for "Found", red for "Not Found"). + site_item.setForeground(QColor(color_code)) + status_item.setForeground(QColor(color_code)) + url_item.setForeground(QColor(color_code)) + + # 3. We insert the items into the table at the determined row index. + self.result_table.setItem(insert_row, 0, site_item) + self.result_table.setItem(insert_row, 1, status_item) + self.result_table.setItem(insert_row, 2, url_item) + self.result_table.scrollToBottom() def search_finished(self): @@ -240,7 +253,7 @@ def update_progress(self, current, total): self.progress_bar.setValue(current) class SherlockWorker(QThread): - result_signal = pyqtSignal(str, str, str) + result_signal = pyqtSignal(str, str, str, str) finished_signal = pyqtSignal() progress_signal = pyqtSignal(int, int) diff --git a/sherlock_project/notify.py b/sherlock_project/notify.py index 39c5754fcf..fca1afd55d 100644 --- a/sherlock_project/notify.py +++ b/sherlock_project/notify.py @@ -302,3 +302,15 @@ def update(self, result): def finish(self, message=None): pass + + def update(self, result): + # 1. We call the new infrastructure we just wrote in the result.py file. + data = result.to_dict() + + status_str = "Found" if result.status.name == "CLAIMED" else "Not Found" + + # 2. We get the color code from the data dictionary, which is determined by the status of the query result. + color_code = data["ui_color_code"] + + # 3. We emit the signal to the interface's table, including the color code. + self.result_signal.emit(data["site_name"], status_str, data["site_url_user"], color_code) From aacad1e31f8f954868652f05a463966caf444cb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 17:18:28 +0300 Subject: [PATCH 10/14] style: Update GUI to a modern light theme for better color contrast --- sherlock_project/gui_app.py | 123 +++++++++++++++++++----------------- 1 file changed, 65 insertions(+), 58 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 6f41c92484..b4c2699d0d 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -23,6 +23,7 @@ def __init__(self): self.apply_styles() def initUI(self): + # Main window settings self.setWindowTitle("Sherlock OSINT Dashboard") self.resize(800, 600) @@ -40,6 +41,15 @@ def initUI(self): title_label.setObjectName("TitleLabel") main_layout.addWidget(title_label) + # Usage Guide / Information Box and Color Legend + self.info_label = QLabel( + "ℹ️ Enter the username you want to search for and click the 'Search' button.\n" + "Results are added to the table alphabetically as they are found. Color Codes: 🟢 Found (Green) | 🔴 Not Found (Red) | 🟡 Error (Yellow)" + ) + self.info_label.setAlignment(Qt.AlignCenter) + self.info_label.setObjectName("infoLabel") + main_layout.addWidget(self.info_label) + # Search bar and button for horizontal layout search_layout = QHBoxLayout() search_layout.setSpacing(15) @@ -88,92 +98,89 @@ def initUI(self): def apply_styles(self): self.setStyleSheet(""" - /* Main Background */ - QMainWindow { - background-color: #1e1e2e; - } - /* General Font and Color */ QWidget { + background-color: #F3F4F6; + color: #1F2937; font-family: 'Segoe UI', Arial, sans-serif; - color: #cdd6f4; } - /* Title Customization */ - #titleLabel { - font-size: 28px; - font-weight: bold; - color: #89b4fa; - margin-bottom: 15px; - } - /* Text Input Field */ - QLineEdit { - background-color: #313244; - border: 2px solid #45475a; + #infoLabel { + color: #4338CA; + font-size: 13px; + background-color: #E0E7FF; + padding: 12px; border-radius: 8px; - padding: 12px 15px; - font-size: 15px; - color: #cdd6f4; + border: 1px solid #C7D2FE; + margin-bottom: 5px; } - QLineEdit:focus { - border: 2px solid #89b4fa; - background-color: #1e1e2e; + QLineEdit { + padding: 10px; + border: 1px solid #D1D5DB; + border-radius: 6px; + background-color: #FFFFFF; + color: #000000; } - /* Search Button */ QPushButton { - background-color: #89b4fa; - color: #1e1e2e; + background-color: #3B82F6; + color: white; border: none; - border-radius: 8px; - padding: 12px 25px; - font-size: 15px; + padding: 10px 20px; + border-radius: 6px; font-weight: bold; } QPushButton:hover { - background-color: #b4befe; + background-color: #2563EB; } - QPushButton:pressed { - background-color: #74c7ec; + QPushButton:disabled { + background-color: #9CA3AF; } - /* Table General Settings */ QTableWidget { - background-color: #1e1e2e; - border: 1px solid #45475a; - border-radius: 8px; - font-size: 14px; - outline: none; - } - /* Table Rows */ - QTableWidget::item { - padding: 10px; - border-bottom: 1px solid #313244; - } - QTableWidget::item:selected { - background-color: #313244; - color: #89b4fa; + background-color: #FFFFFF; + alternate-background-color: #F9FAFB; + gridline-color: #E5E7EB; + border: 1px solid #D1D5DB; + border-radius: 6px; + color: #000000; } - /* Table Header */ QHeaderView::section { - background-color: #313244; - color: #a6adc8; - padding: 12px; + background-color: #E5E7EB; + padding: 6px; border: none; font-weight: bold; - font-size: 15px; - text-align: left; + color: #374151; + } + QProgressBar { + border: 1px solid #D1D5DB; + border-radius: 6px; + text-align: center; + background-color: #E5E7EB; + color: #1F2937; + } + QProgressBar::chunk { + background-color: #3B82F6; + border-radius: 4px; } - /* Scrollbar Aesthetics */ QScrollBar:vertical { border: none; - background: #1e1e2e; + background: #F3F4F6; width: 12px; border-radius: 6px; + margin: 0px 0px 0px 0px; } QScrollBar::handle:vertical { - background: #45475a; + background: #CBD5E1; min-height: 30px; - border-radius: 6px; + border-radius: 6px; } QScrollBar::handle:vertical:hover { - background: #585b70; + background: #94A3B8; + } + QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical { + border: none; + background: none; + height: 0px; + } + QScrollBar::add-page:vertical, QScrollBar::sub-page:vertical { + background: none; } """) From 47fa5e08a146b3676bbbcede474c3b4170745a3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 17:31:16 +0300 Subject: [PATCH 11/14] feat: Add dynamic dashboard stat cards and search reset logic. Introduced 3 summary cards (Scanned, Found, Not Found) to track live search progress. Implemented real-time counter updates inside add_result_to_table and integrated state-resetting logic within start_search to clear counters on sequential queries. --- sherlock_project/gui_app.py | 39 ++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index b4c2699d0d..06abfc217d 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -21,7 +21,10 @@ def __init__(self): super().__init__() self.initUI() self.apply_styles() - + self.count_total = 0 + self.count_found = 0 + self.count_not_found = 0 + def initUI(self): # Main window settings @@ -50,6 +53,21 @@ def initUI(self): self.info_label.setObjectName("infoLabel") main_layout.addWidget(self.info_label) + # Statistics Cards Layout + self.stats_layout = QHBoxLayout() + + self.card_total = QLabel("🔍 Scanned\n0") + self.card_found = QLabel("✅ Found\n0") + self.card_not_found = QLabel("❌ Not Found\n0") + + # We are applying a consistent style to all three cards and adding them to the horizontal layout. + for card in [self.card_total, self.card_found, self.card_not_found]: + card.setAlignment(Qt.AlignCenter) + card.setObjectName("statCard") + self.stats_layout.addWidget(card) + + main_layout.addLayout(self.stats_layout) + # Search bar and button for horizontal layout search_layout = QHBoxLayout() search_layout.setSpacing(15) @@ -202,6 +220,14 @@ def start_search(self): self.worker.progress_signal.connect(self.update_progress) self.worker.start() + # We reset the statistics cards and counters at the start of a new search. + self.count_total = 0 + self.count_found = 0 + self.count_not_found = 0 + self.card_total.setText("🔍 Scanned\n0") + self.card_found.setText("✅ Found\n0") + self.card_not_found.setText("❌ Not Found\n0") + def add_result_to_table(self, site, status, url, color_code): status_text = "✅ Found" if status == "Found" else "❌ Not Found" @@ -249,6 +275,17 @@ def add_result_to_table(self, site, status, url, color_code): self.result_table.scrollToBottom() + # We update the statistics cards based on the new result. + self.count_total += 1 + if status == "Found": + self.count_found += 1 + else: + self.count_not_found += 1 + + self.card_total.setText(f"🔍 Scanned\n{self.count_total}") + self.card_found.setText(f"✅ Found\n{self.count_found}") + self.card_not_found.setText(f"❌ Not Found\n{self.count_not_found}") + def search_finished(self): self.search_button.setEnabled(True) self.search_button.setText("Search") From e394daa3f53ed4fb7bfdde19ca1d6af0aaf9a788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 18:14:18 +0300 Subject: [PATCH 12/14] fix: Make badge container transparent and hide underlying text Resolved a UI glitch where the QWidget container's default background and the underlying QTableWidgetItem text were the viibly protruding behind the custom rounded QLabel badges. --- sherlock_project/gui_app.py | 67 ++++++++++++++++++++++++++++++------- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 06abfc217d..0aaa55be09 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -122,13 +122,22 @@ def apply_styles(self): font-family: 'Segoe UI', Arial, sans-serif; } #infoLabel { - color: #4338CA; + background-color: #EFF6FF; + color: #1D4ED8; + border: 1px solid #BFDBFE; + border-radius: 6px; + padding: 10px; font-size: 13px; - background-color: #E0E7FF; - padding: 12px; + margin-bottom: 10px; + } + QLabel#statCard { + background-color: #FFFFFF; + border: 1px solid #E2E8F0; border-radius: 8px; - border: 1px solid #C7D2FE; - margin-bottom: 5px; + padding: 15px; + font-size: 14px; + font-weight: bold; + color: #334155; } QLineEdit { padding: 10px; @@ -248,7 +257,6 @@ def add_result_to_table(self, site, status, url, color_code): break else: # Rule 3: Newly entered "Not Found" entries should skip over existing "Found" rows - if current_status == "✅ Found": continue # Rule 4: "Not Found" sites should be sorted alphabetically (A-Z) within their own section @@ -260,19 +268,54 @@ def add_result_to_table(self, site, status, url, color_code): # 1. We create cell objects. site_item = QTableWidgetItem(site) - status_item = QTableWidgetItem(status_text) + # We are creating a hidden text item that will remain in the background to avoid disrupting the sorting algorithm. + status_hidden_item = QTableWidgetItem(status_text) + status_hidden_item.setForeground(QColor(0, 0, 0, 0)) url_item = QTableWidgetItem(url) # 2. We apply color coding to the text based on the status (green for "Found", red for "Not Found"). - site_item.setForeground(QColor(color_code)) - status_item.setForeground(QColor(color_code)) - url_item.setForeground(QColor(color_code)) + #site_item.setForeground(QColor(color_code)) + #url_item.setForeground(QColor(color_code)) + + + # We are creating QLabel for the new badge design. + badge_label = QLabel(status_text) + badge_label.setAlignment(Qt.AlignCenter) + + if status == "Found": + # Light pastel green background, bold dark green text. + badge_label.setStyleSheet(""" + background-color: #D1FAE5; + color: #065F46; + border-radius: 10px; + padding: 4px 10px; + font-weight: bold; + """) + else: + # Light pastel red background, bold dark red text. + badge_label.setStyleSheet(""" + background-color: #FEE2E2; + color: #991B1B; + border-radius: 10px; + padding: 4px 10px; + font-weight: bold; + """) + + # We are preparing a carrier QWidget so that the badge fits perfectly inside the cell and doesn't look unsightly. + badge_container = QWidget() + badge_container.setStyleSheet("background-color: transparent;") + badge_layout = QHBoxLayout(badge_container) + badge_layout.setContentsMargins(10, 2, 10, 2) + badge_layout.addWidget(badge_label) + badge_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) # 3. We insert the items into the table at the determined row index. self.result_table.setItem(insert_row, 0, site_item) - self.result_table.setItem(insert_row, 1, status_item) + self.result_table.setItem(insert_row, 1, status_hidden_item) + # We place our visual badge on the corresponding cell. + self.result_table.setCellWidget(insert_row, 1, badge_container) self.result_table.setItem(insert_row, 2, url_item) - + self.result_table.scrollToBottom() # We update the statistics cards based on the new result. From 06e71dd520c95bc95f92c01de4c5f7df779ba0ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Tue, 2 Jun 2026 23:49:05 +0300 Subject: [PATCH 13/14] style: increase title size and refine info box --- sherlock_project/gui_app.py | 81 ++++++++++++++++++++++++++++--------- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index 0aaa55be09..c7676852e8 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -24,7 +24,7 @@ def __init__(self): self.count_total = 0 self.count_found = 0 self.count_not_found = 0 - + def initUI(self): # Main window settings @@ -55,17 +55,17 @@ def initUI(self): # Statistics Cards Layout self.stats_layout = QHBoxLayout() - + self.card_total = QLabel("🔍 Scanned\n0") self.card_found = QLabel("✅ Found\n0") self.card_not_found = QLabel("❌ Not Found\n0") - + # We are applying a consistent style to all three cards and adding them to the horizontal layout. for card in [self.card_total, self.card_found, self.card_not_found]: card.setAlignment(Qt.AlignCenter) card.setObjectName("statCard") self.stats_layout.addWidget(card) - + main_layout.addLayout(self.stats_layout) # Search bar and button for horizontal layout @@ -74,7 +74,7 @@ def initUI(self): self.username_input = QLineEdit() self.username_input.setPlaceholderText("Enter the username to search (e.g., elif123)") - + self.search_button = QPushButton("Search") self.search_button.setCursor(Qt.PointingHandCursor) # We are attaching the function that will run when the button is clicked (Signal-Slot logic). @@ -88,7 +88,28 @@ def initUI(self): self.result_table = QTableWidget() self.result_table.setColumnCount(3) self.result_table.setHorizontalHeaderLabels(["Platform", "Status", "URL"]) - + + # We are creating a visually appealing empty state label that will be shown when there are no results to display. + # This provides a better user experience and guides the user on what to do next. + self.empty_state_label = QLabel( + "🔎\n\nReady to investigate.\nEnter a target username above to start the OSINT scan." + ) + self.empty_state_label.setAlignment(Qt.AlignCenter) + self.empty_state_label.setStyleSheet(""" + color: #94A3B8; /* Açık gri profesyonel metin */ + font-size: 16px; + font-weight: bold; + background-color: #FFFFFF; + border: 2px dashed #E2E8F0; /* Kesik çizgili şık çerçeve */ + border-radius: 8px; + padding: 80px; /* Kutuyu genişletmek için iç boşluk */ + """) + main_layout.addWidget(self.empty_state_label) + + # Initially, the results table is hidden and only the empty state label is visible. + # Once results start coming in, we will hide the empty state and show the table. + self.result_table.setVisible(False) + # Resize table columns proportionally to the window self.result_table.setEditTriggers(QAbstractItemView.NoEditTriggers) self.result_table.setSelectionBehavior(QAbstractItemView.SelectRows) @@ -100,7 +121,7 @@ def initUI(self): header.setSectionResizeMode(0, QHeaderView.ResizeToContents) header.setSectionResizeMode(1, QHeaderView.ResizeToContents) header.setSectionResizeMode(2, QHeaderView.Stretch) - + main_layout.addWidget(self.result_table) # Progress Bar Settings @@ -121,19 +142,27 @@ def apply_styles(self): color: #1F2937; font-family: 'Segoe UI', Arial, sans-serif; } + QLabel#TitleLabel { + font-size: 26px; /* Büyütüldü */ + font-weight: bold; + color: #1E293B; + margin-bottom: 5px; + } #infoLabel { background-color: #EFF6FF; color: #1D4ED8; border: 1px solid #BFDBFE; border-radius: 6px; - padding: 10px; - font-size: 13px; + padding: 6px 15px; /* Daraltıldı */ + font-size: 12px; /* Küçültüldü */ margin-bottom: 10px; } QLabel#statCard { background-color: #FFFFFF; border: 1px solid #E2E8F0; border-radius: 8px; + border: 1px solid #C7D2FE; + margin-bottom: 5px; padding: 15px; font-size: 14px; font-weight: bold; @@ -215,6 +244,11 @@ def apply_styles(self): def start_search(self): username = self.username_input.text().strip() + # When the search starts, we hide the empty state label and show the results table. + # This allows us to display incoming results in real-time as they are found. + self.empty_state_label.setVisible(False) + self.result_table.setVisible(True) + if not username: return @@ -239,13 +273,13 @@ def start_search(self): def add_result_to_table(self, site, status, url, color_code): status_text = "✅ Found" if status == "Found" else "❌ Not Found" - + insert_row = self.result_table.rowCount() - + for i in range(self.result_table.rowCount()): current_site = self.result_table.item(i, 0).text() current_status = self.result_table.item(i, 1).text() - + if status_text == "✅ Found": # Rule 1: The newly entered "Found" data should be placed above the first "Not Found" row in the table. if current_status == "❌ Not Found": @@ -257,23 +291,28 @@ def add_result_to_table(self, site, status, url, color_code): break else: # Rule 3: Newly entered "Not Found" entries should skip over existing "Found" rows + if current_status == "✅ Found": continue # Rule 4: "Not Found" sites should be sorted alphabetically (A-Z) within their own section if current_site.lower() > site.lower(): insert_row = i break - + self.result_table.insertRow(insert_row) # 1. We create cell objects. site_item = QTableWidgetItem(site) + status_item = QTableWidgetItem(status_text) # We are creating a hidden text item that will remain in the background to avoid disrupting the sorting algorithm. status_hidden_item = QTableWidgetItem(status_text) status_hidden_item.setForeground(QColor(0, 0, 0, 0)) url_item = QTableWidgetItem(url) - + # 2. We apply color coding to the text based on the status (green for "Found", red for "Not Found"). + site_item.setForeground(QColor(color_code)) + status_item.setForeground(QColor(color_code)) + url_item.setForeground(QColor(color_code)) #site_item.setForeground(QColor(color_code)) #url_item.setForeground(QColor(color_code)) @@ -308,13 +347,15 @@ def add_result_to_table(self, site, status, url, color_code): badge_layout.setContentsMargins(10, 2, 10, 2) badge_layout.addWidget(badge_label) badge_layout.setAlignment(Qt.AlignLeft | Qt.AlignVCenter) - + # 3. We insert the items into the table at the determined row index. self.result_table.setItem(insert_row, 0, site_item) + self.result_table.setItem(insert_row, 1, status_item) self.result_table.setItem(insert_row, 1, status_hidden_item) # We place our visual badge on the corresponding cell. self.result_table.setCellWidget(insert_row, 1, badge_container) self.result_table.setItem(insert_row, 2, url_item) + self.result_table.scrollToBottom() @@ -324,7 +365,7 @@ def add_result_to_table(self, site, status, url, color_code): self.count_found += 1 else: self.count_not_found += 1 - + self.card_total.setText(f"🔍 Scanned\n{self.count_total}") self.card_found.setText(f"✅ Found\n{self.count_found}") self.card_not_found.setText(f"❌ Not Found\n{self.count_not_found}") @@ -334,7 +375,7 @@ def search_finished(self): self.search_button.setText("Search") self.progress_bar.setValue(self.progress_bar.maximum()) print("[*] Search completed.") - + def update_progress(self, current, total): self.progress_bar.setMaximum(total) self.progress_bar.setValue(current) @@ -351,7 +392,7 @@ def __init__(self, username): def run(self): gui_notifier = QueryNotifyGUI(self.result_signal) - + try: with open("sherlock_project/resources/data.json", "r", encoding="utf-8") as f: site_data = json.load(f) @@ -360,14 +401,14 @@ def run(self): except Exception as e: self.finished_signal.emit() return - + total_sites = len(site_data) # The Callback function that Sherlock.py will call at the end of each site visit. def progress_callback(): self.checked_count += 1 self.progress_signal.emit(self.checked_count, total_sites) - + # We are sending the callback as a parameter. sherlock(self.username, site_data, gui_notifier, timeout=60, progress_callback=progress_callback) self.finished_signal.emit() From 884d1c7ea49b0301b1e9935237f66ddafe3a1038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Elif=20Hasg=C3=BCl?= Date: Wed, 3 Jun 2026 00:21:49 +0300 Subject: [PATCH 14/14] feat: implement alphabetical site filtering UI and get_sites_for_ui method --- sherlock_project/gui_app.py | 98 +++++++++++++++++++++++++++++-------- sherlock_project/sites.py | 50 +++++++++++++++++++ 2 files changed, 127 insertions(+), 21 deletions(-) diff --git a/sherlock_project/gui_app.py b/sherlock_project/gui_app.py index c7676852e8..e78ce7d038 100644 --- a/sherlock_project/gui_app.py +++ b/sherlock_project/gui_app.py @@ -8,9 +8,10 @@ from sherlock_project.sherlock import sherlock from sherlock_project.sites import SitesInformation -from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, - QHBoxLayout, QLineEdit, QPushButton, QTableWidget, - QTableWidgetItem, QLabel, QHeaderView, QAbstractItemView, QProgressBar) +from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, + QLineEdit, QPushButton, QTableWidget, QTableWidgetItem, QLabel, + QHeaderView, QAbstractItemView, QProgressBar, QTextEdit, QListWidget, + QListWidgetItem, QDialog, QScrollArea, QCheckBox, QDialogButtonBox) from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt, QThread, pyqtSignal @@ -80,8 +81,15 @@ def initUI(self): # We are attaching the function that will run when the button is clicked (Signal-Slot logic). self.search_button.clicked.connect(self.start_search) + # We are creating a new button for the site filter and connecting it to the corresponding function that will open the filter dialog. + self.filter_button = QPushButton("⚙️ Filter Sites") + self.filter_button.setCursor(Qt.PointingHandCursor) + self.filter_button.clicked.connect(self.open_site_filter) + self.filter_button.setStyleSheet("background-color: #64748B; color: white;") + search_layout.addWidget(self.username_input) search_layout.addWidget(self.search_button) + search_layout.addWidget(self.filter_button) main_layout.addLayout(search_layout) # Bottom section: Results table @@ -96,13 +104,13 @@ def initUI(self): ) self.empty_state_label.setAlignment(Qt.AlignCenter) self.empty_state_label.setStyleSheet(""" - color: #94A3B8; /* Açık gri profesyonel metin */ + color: #94A3B8; font-size: 16px; font-weight: bold; background-color: #FFFFFF; - border: 2px dashed #E2E8F0; /* Kesik çizgili şık çerçeve */ + border: 2px dashed #E2E8F0; border-radius: 8px; - padding: 80px; /* Kutuyu genişletmek için iç boşluk */ + padding: 80px; """) main_layout.addWidget(self.empty_state_label) @@ -143,7 +151,7 @@ def apply_styles(self): font-family: 'Segoe UI', Arial, sans-serif; } QLabel#TitleLabel { - font-size: 26px; /* Büyütüldü */ + font-size: 26px; font-weight: bold; color: #1E293B; margin-bottom: 5px; @@ -153,8 +161,8 @@ def apply_styles(self): color: #1D4ED8; border: 1px solid #BFDBFE; border-radius: 6px; - padding: 6px 15px; /* Daraltıldı */ - font-size: 12px; /* Küçültüldü */ + padding: 6px 15px; + font-size: 12px; margin-bottom: 10px; } QLabel#statCard { @@ -239,6 +247,50 @@ def apply_styles(self): background: none; } """) + + def open_site_filter(self): + # We are loading the site information only when the filter dialog is opened for the first time. + # This way, we avoid unnecessary loading during the initial startup of the application and only load the data when it's actually needed. + if not hasattr(self, 'sites_info'): + self.sites_info = SitesInformation(data_file_path="sherlock_project/resources/data.json") + self.all_sites = self.sites_info.get_sites_for_ui() + self.selected_sites = [site['name'] for site in self.all_sites] + + dialog = QDialog(self) + dialog.setWindowTitle("Select Target Platforms") + dialog.resize(400, 500) + dialog.setStyleSheet(self.styleSheet()) + + layout = QVBoxLayout(dialog) + + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll_content = QWidget() + scroll_layout = QVBoxLayout(scroll_content) + + self.checkboxes = {} + for site in self.all_sites: + # We are adding a clear NSFW tag next to the site name for platforms that are marked as NSFW in the data. + # This allows users to easily identify and exclude adult content platforms from their search if they choose to do so. + nsfw_tag = " 🔞 (NSFW)" if site['is_nsfw'] else "" + cb = QCheckBox(f"{site['name']}{nsfw_tag}") + + if site['name'] in self.selected_sites: + cb.setChecked(True) + + self.checkboxes[site['name']] = cb + scroll_layout.addWidget(cb) + + scroll.setWidget(scroll_content) + layout.addWidget(scroll) + + buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + buttons.accepted.connect(dialog.accept) + buttons.rejected.connect(dialog.reject) + layout.addWidget(buttons) + + if dialog.exec_() == QDialog.Accepted: + self.selected_sites = [name for name, cb in self.checkboxes.items() if cb.isChecked()] @@ -256,7 +308,7 @@ def start_search(self): self.search_button.setEnabled(False) self.search_button.setText("Searching...") - self.worker = SherlockWorker(username) + self.worker = SherlockWorker(username, getattr(self, 'selected_sites', None)) self.worker.result_signal.connect(self.add_result_to_table) self.worker.finished_signal.connect(self.search_finished) self.progress_bar.show() @@ -291,7 +343,6 @@ def add_result_to_table(self, site, status, url, color_code): break else: # Rule 3: Newly entered "Not Found" entries should skip over existing "Found" rows - if current_status == "✅ Found": continue # Rule 4: "Not Found" sites should be sorted alphabetically (A-Z) within their own section @@ -313,9 +364,6 @@ def add_result_to_table(self, site, status, url, color_code): site_item.setForeground(QColor(color_code)) status_item.setForeground(QColor(color_code)) url_item.setForeground(QColor(color_code)) - #site_item.setForeground(QColor(color_code)) - #url_item.setForeground(QColor(color_code)) - # We are creating QLabel for the new badge design. badge_label = QLabel(status_text) @@ -385,23 +433,31 @@ class SherlockWorker(QThread): finished_signal = pyqtSignal() progress_signal = pyqtSignal(int, int) - def __init__(self, username): + def __init__(self, username, selected_sites=None): super().__init__() self.username = username + self.selected_sites = selected_sites if selected_sites is not None else [] self.checked_count = 0 def run(self): gui_notifier = QueryNotifyGUI(self.result_signal) - + try: - with open("sherlock_project/resources/data.json", "r", encoding="utf-8") as f: - site_data = json.load(f) - if "$schema" in site_data: - del site_data["$schema"] + # We are loading the site information from the JSON file and applying the user's site filter preferences before starting the search. + sites_info = SitesInformation(data_file_path="sherlock_project/resources/data.json") + + if self.selected_sites: + sites_info.filter_sites_by_names(self.selected_sites) + + site_data = {} + for site in sites_info: + site_data[site.name] = site.information + except Exception as e: + print("Error loading sites:", e) self.finished_signal.emit() return - + total_sites = len(site_data) # The Callback function that Sherlock.py will call at the end of each site visit. diff --git a/sherlock_project/sites.py b/sherlock_project/sites.py index c42554bacc..1740a66581 100644 --- a/sherlock_project/sites.py +++ b/sherlock_project/sites.py @@ -258,3 +258,53 @@ def __len__(self): Length of sites object. """ return len(self.sites) + + def get_sites_for_ui(self): + """Formats and returns the site list for the user interface (GUI). + Return Value: + An alphabetically sorted list of site dictionaries prepared for display on the user interface. + """ + ui_sites = [] + # Sort sites alphabetically by name (case-insensitive) and format them for the UI + for site_name in sorted(self.sites.keys(), key=str.lower): + site_obj = self.sites[site_name] + ui_sites.append({ + "name": site_obj.name, + "url_main": site_obj.url_home, + "is_nsfw": site_obj.is_nsfw + }) + return ui_sites + + def filter_sites_by_names(self, selected_names: list): + """Sherlock can scan only the sites selected by the user. + Keyword Arguments: + selected_names -- A list containing the names of the sites selected by the user from the interface. + """ + # if the user did not select any sites, then we should not filter the list at all. + # This allows the user to easily reset the filter by deselecting all sites. + if not selected_names: + return + + filtered_sites = {} + # To prevent errors due to case sensitivity, we convert all names to lowercase. + selected_lower = [name.lower() for name in selected_names] + + for site_name, site_obj in self.sites.items(): + if site_name.lower() in selected_lower: + filtered_sites[site_name] = site_obj + + # replacing the current massive list of 400+ sites with only the sites chosen by the user. + self.sites = filtered_sites + if not selected_names: + return + + filtered_sites = {} + # To prevent errors due to case sensitivity, we convert all names to lowercase. + selected_lower = [name.lower() for name in selected_names] + + for site_name, site_obj in self.sites.items(): + if site_name.lower() in selected_lower: + filtered_sites[site_name] = site_obj + + # replacing the current massive list of 400+ sites with only the sites chosen by the user. + self.sites = filtered_sites