Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions UI/AppStartup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from PySide6.QtWidgets import QApplication, QMessageBox
from PySide6.QtCore import QThread, Signal, QObject
import os, time

from UI.SplashScreen import SplashScreen
from UI.MainWindow import MainWindow
from utils.Logger import logger
from utils.CheckInternet import Internet


# STARTUP WORKER THREAD
class StartupWorker(QThread):
status_updated = Signal(str, int) # message, progress %
finished = Signal(bool)
step_timing = {}

def __init__(self, parent=None):
super().__init__(parent)

def run(self):
self.step("Checking internet connection...", 10)
internet = Internet()
connected = internet.check_internet()

for _ in range(2):
if connected:
break
time.sleep(1)
connected = internet.check_internet()

if not connected:
self.finished.emit(False)
return

self.step("Initializing plugins...", 25)
time.sleep(0.5)

self.step("Loading UI modules...", 45)
time.sleep(0.5)

self.step("Preparing database engine...", 65)
time.sleep(0.5)

self.step("Optimizing startup cache...", 80)
time.sleep(0.5)

self.step("Finalizing system checks...", 95)
time.sleep(0.4)

self.step("Startup ready", 100)
self.finished.emit(True)

def step(self, msg, progress):
logger.info(msg)
self.status_updated.emit(msg, progress)
self.step_timing[msg] = time.time()


# APP STARTUP CONTROLLER
class AppStartup(QObject):
def __init__(self):
super().__init__()

base_dir = os.path.dirname(os.path.abspath(__file__))
self.base_dir = os.path.dirname(base_dir)
gif_path = os.path.join(self.base_dir, "assets", "splash", "loading.gif")

# parent=None to avoid QDialog parent type error
self.splash = SplashScreen(parent=None, gif_path=gif_path)
self.splash.set_title("StaTube - YouTube Data Analysis Tool")
self.splash.update_status("Booting system...")
self.splash.set_progress(5)

# Use animated show
self.splash.show_with_animation()

logger.info("Splash screen shown with fade-in animation.")

self.start_worker()

# START BACKGROUND TASK
def start_worker(self):
self.worker = StartupWorker()
self.worker.status_updated.connect(self.on_status_update)
self.worker.finished.connect(self.on_finished)
self.worker.start()

def on_status_update(self, message: str, progress: int):
self.splash.update_status(message)
self.splash.set_progress(progress)

# FINAL HANDOFF
def on_finished(self, connected: bool):
self.splash.fade_and_close()
if not connected:
msg = QMessageBox()
msg.setIcon(QMessageBox.Warning)
msg.setWindowTitle("Connection Issue")
msg.setText(
"No internet connection detected.\n\n"
"StaTube can continue in offline mode, but some features may not work."
)
continue_btn = msg.addButton("Continue Offline", QMessageBox.AcceptRole)
quit_btn = msg.addButton("Quit", QMessageBox.RejectRole)
msg.setDefaultButton(continue_btn)
msg.exec()

if msg.clickedButton() == quit_btn:
QApplication.instance().quit()
return

# NOW SAFE TO CREATE MAIN WINDOW
logger.info("Launching MainWindow after verified startup.")
try:
self.main_window = MainWindow()
self.main_window.finish_initialization()
self.main_window.showMaximized()
except Exception as e:
logger.exception("Fatal UI startup failure")
QMessageBox.critical(
None,
"Startup Failure",
"StaTube failed to initialize UI. Starting in Safe Mode."
)
logger.info("===== Startup Timing Report =====")
for step, t in self.worker.step_timing.items():
logger.info(f"{step}")
93 changes: 56 additions & 37 deletions UI/Homepage.py
Original file line number Diff line number Diff line change
Expand Up @@ -207,18 +207,27 @@ def on_item_selected(self, item: QListWidgetItem) -> None:

@QtCore.Slot()
def show_search_splash(self) -> None:
"""
Show splash screen for search operation.

Creates and displays a splash screen with an animated loading GIF
to provide visual feedback during channel search operations.
"""
cwd = os.getcwd()
gif_path = os.path.join(cwd, "assets", "gif", "loading.gif")

if self.splash:
self.splash.close()

self.splash = SplashScreen(parent=self.mainwindow, gif_path=gif_path)
self.splash.set_title("Searching Channels...")
self.splash.update_status("Fetching channel information...")
self.splash.show()
self.splash.set_progress(0)

# Runtime overlay + cancel support
self.splash.enable_runtime_mode(
parent_window=self.mainwindow,
cancel_callback=self.cancel_search
)

# THIS IS REQUIRED
self.splash.show_with_animation()
self.splash.raise_()
self.splash.activateWindow()

@QtCore.Slot(int, str)
def on_progress_update(self, progress: int, status: str) -> None:
Expand All @@ -240,15 +249,20 @@ def on_progress_update(self, progress: int, status: str) -> None:

@QtCore.Slot()
def close_splash(self) -> None:
"""
Close splash screen.

Closes and cleans up the splash screen instance if it exists.
"""
if self.splash:
self.splash.close()
self.splash.fade_and_close(400)
self.splash = None

def cancel_search(self):
logger.warning("User cancelled search operation.")

self.stop_event.set()

if self.search_thread_instance and self.search_thread_instance.is_alive():
self.search_thread_instance.join(timeout=0.5)

self.close_splash_signal.emit()

def update_results(self, channels: List[str]) -> None:
"""
Update dropdown list with search results.
Expand Down Expand Up @@ -382,27 +396,25 @@ def _run_search(self, query: str, final: bool) -> None:

# Check if thread should stop before starting work
if self.stop_event.is_set():
self.close_splash_signal.emit()
logger.debug("Search thread cancelled before execution")
return

try:
if final:
# Show splash screen for final search
self.show_splash_signal.emit()

if final:
# Define progress callback for the search
def progress_callback(progress: Any, status: Optional[str] = None) -> None:
"""
Callback function for reporting search progress.

Args:
progress: Progress value (int/float for percentage, str for status message)
status: Optional status message, defaults to None
"""
def progress_callback(progress: Any, status: Optional[str] = None):
if self.stop_event.is_set():
return

if isinstance(progress, (int, float)):
self.progress_update.emit(int(progress), status or "")
if self.splash:
self.splash.update_eta(int(progress))

elif isinstance(progress, str):
self.progress_update.emit(-1, progress) # -1 means don't update progress bar
self.progress_update.emit(-1, progress)


# Perform search with progress tracking
self.channels = self.search.search_channel(
Expand Down Expand Up @@ -444,26 +456,33 @@ def progress_callback(progress: Any, status: Optional[str] = None) -> None:
# Signal that search is complete
self.search_complete.emit()

if self.stop_event.is_set():
self.close_splash_signal.emit()
return

def search_channel(self) -> None:
"""
Handle search button click event.

Initiates a final comprehensive search for the query entered in the search bar.
Cancels any ongoing auto-search operations before starting the new search.
Clears previous incomplete results to ensure fresh data is retrieved.
"""
query = self.searchbar.text().strip()
if not query:
return

# --- Cancel any ongoing auto-search ---
# CANCEL any running search
if self.search_thread_instance and self.search_thread_instance.is_alive():
self.stop_event.set()
self.search_thread_instance.join(timeout=1.0) # Ensure it fully stops before continuing
self.search_thread_instance.join(timeout=0.5)

# Clear old incomplete results
self.channels = None
self.search.search_channel(name=None) # reset internal state if needed

# --- Now run the final search with fresh data ---
self.search_keyword(query=query, final=True)
self.stop_event.clear()

# SHOW SPLASH IMMEDIATELY (MAIN THREAD)
self.show_search_splash()

# START WORKER THREAD AFTER SPLASH IS VISIBLE
self.search_thread_instance = threading.Thread(
target=self._run_search,
daemon=True,
args=(query, True)
)
self.search_thread_instance.start()
Loading