import os import sys from platform import system from PyQt6.QtCore import QTimer, pyqtSlot from PyQt6.QtGui import QAction, QIcon from PyQt6.QtWidgets import (QApplication, QCheckBox, QDialog, QFileDialog, QFormLayout, QLabel, QLineEdit, QMenu, QMessageBox, QPushButton, QSystemTrayIcon, QTextEdit, QVBoxLayout, QWidget) from .obs_client import close_obs, is_obs_running, open_obs from .playback import Player, get_latest_recording from .recorder import Recorder from .util import get_recordings_dir, open_file class TitleDescriptionDialog(QDialog): def __init__(self, parent=None): super().__init__(parent) self.setWindowTitle("Recording Details") layout = QVBoxLayout(self) self.form_layout = QFormLayout() self.title_label = QLabel("Title:") self.title_input = QLineEdit(self) self.form_layout.addRow(self.title_label, self.title_input) self.description_label = QLabel("Description:") self.description_input = QTextEdit(self) self.form_layout.addRow(self.description_label, self.description_input) layout.addLayout(self.form_layout) self.submit_button = QPushButton("Save", self) self.submit_button.clicked.connect(self.accept) layout.addWidget(self.submit_button) def get_values(self): return self.title_input.text(), self.description_input.toPlainText() class MainInterface(QWidget): def __init__(self, app: QApplication): super().__init__() self.tray = QSystemTrayIcon(QIcon(resource_path("assets/duck.png"))) self.tray.show() self.app = app self.init_tray() self.init_window() if not is_obs_running(): self.obs_process = open_obs() def init_window(self): self.setWindowTitle("DuckTrack") layout = QVBoxLayout(self) self.toggle_record_button = QPushButton("Start Recording", self) self.toggle_record_button.clicked.connect(self.toggle_record) layout.addWidget(self.toggle_record_button) self.toggle_pause_button = QPushButton("Pause Recording", self) self.toggle_pause_button.clicked.connect(self.toggle_pause) self.toggle_pause_button.setEnabled(False) layout.addWidget(self.toggle_pause_button) self.show_recordings_button = QPushButton("Show Recordings", self) self.show_recordings_button.clicked.connect(lambda: open_file(get_recordings_dir())) layout.addWidget(self.show_recordings_button) self.play_latest_button = QPushButton("Play Latest Recording", self) self.play_latest_button.clicked.connect(self.play_latest_recording) layout.addWidget(self.play_latest_button) self.play_custom_button = QPushButton("Play Custom Recording", self) self.play_custom_button.clicked.connect(self.play_custom_recording) layout.addWidget(self.play_custom_button) self.replay_recording_button = QPushButton("Replay Recording", self) self.replay_recording_button.clicked.connect(self.replay_recording) self.replay_recording_button.setEnabled(False) layout.addWidget(self.replay_recording_button) self.quit_button = QPushButton("Quit", self) self.quit_button.clicked.connect(self.quit) layout.addWidget(self.quit_button) self.natural_scrolling_checkbox = QCheckBox("Natural Scrolling", self, checked=system() == "Darwin") layout.addWidget(self.natural_scrolling_checkbox) self.natural_scrolling_checkbox.stateChanged.connect(self.toggle_natural_scrolling) self.setLayout(layout) def init_tray(self): self.menu = QMenu() self.tray.setContextMenu(self.menu) self.toggle_record_action = QAction("Start Recording") self.toggle_record_action.triggered.connect(self.toggle_record) self.menu.addAction(self.toggle_record_action) self.toggle_pause_action = QAction("Pause Recording") self.toggle_pause_action.triggered.connect(self.toggle_pause) self.toggle_pause_action.setVisible(False) self.menu.addAction(self.toggle_pause_action) self.show_recordings_action = QAction("Show Recordings") self.show_recordings_action.triggered.connect(lambda: open_file(get_recordings_dir())) self.menu.addAction(self.show_recordings_action) self.play_latest_action = QAction("Play Latest Recording") self.play_latest_action.triggered.connect(self.play_latest_recording) self.menu.addAction(self.play_latest_action) self.play_custom_action = QAction("Play Custom Recording") self.play_custom_action.triggered.connect(self.play_custom_recording) self.menu.addAction(self.play_custom_action) self.replay_recording_action = QAction("Replay Recording") self.replay_recording_action.triggered.connect(self.replay_recording) self.menu.addAction(self.replay_recording_action) self.replay_recording_action.setVisible(False) self.quit_action = QAction("Quit") self.quit_action.triggered.connect(self.quit) self.menu.addAction(self.quit_action) self.menu.addSeparator() self.natural_scrolling_option = QAction("Natural Scrolling", checkable=True, checked=system() == "Darwin") self.natural_scrolling_option.triggered.connect(self.toggle_natural_scrolling) self.menu.addAction(self.natural_scrolling_option) @pyqtSlot() def replay_recording(self): player = Player() if hasattr(self, "last_played_recording_path"): player.play(self.last_played_recording_path) else: self.display_error_message("No recording has been played yet!") @pyqtSlot() def play_latest_recording(self): player = Player() recording_path = get_latest_recording() self.last_played_recording_path = recording_path self.replay_recording_action.setVisible(True) self.replay_recording_button.setEnabled(True) player.play(recording_path) @pyqtSlot() def play_custom_recording(self): player = Player() directory = QFileDialog.getExistingDirectory(None, "Select Recording", get_recordings_dir()) if directory: self.last_played_recording_path = directory self.replay_recording_button.setEnabled(True) self.replay_recording_action.setVisible(True) player.play(directory) @pyqtSlot() def quit(self): if hasattr(self, "recorder_thread"): self.toggle_record() if hasattr(self, "obs_process"): close_obs(self.obs_process) self.app.quit() def closeEvent(self, event): self.quit() @pyqtSlot() def toggle_natural_scrolling(self): sender = self.sender() if sender == self.natural_scrolling_checkbox: state = self.natural_scrolling_checkbox.isChecked() self.natural_scrolling_option.setChecked(state) else: state = self.natural_scrolling_option.isChecked() self.natural_scrolling_checkbox.setChecked(state) @pyqtSlot() def toggle_pause(self): if self.recorder_thread._is_paused: self.recorder_thread.resume_recording() self.toggle_pause_action.setText("Pause Recording") self.toggle_pause_button.setText("Pause Recording") else: self.recorder_thread.pause_recording() self.toggle_pause_action.setText("Resume Recording") self.toggle_pause_button.setText("Resume Recording") @pyqtSlot() def toggle_record(self): if not hasattr(self, "recorder_thread"): self.recorder_thread = Recorder(natural_scrolling=self.natural_scrolling_checkbox.isChecked()) self.recorder_thread.recording_stopped.connect(self.on_recording_stopped) self.recorder_thread.start() self.update_menu(True) else: self.recorder_thread.stop_recording() self.recorder_thread.terminate() recording_dir = self.recorder_thread.recording_path del self.recorder_thread dialog = TitleDescriptionDialog() QTimer.singleShot(0, dialog.raise_) result = dialog.exec() if result == QDialog.DialogCode.Accepted: title, description = dialog.get_values() if title: renamed_dir = os.path.join(os.path.dirname(recording_dir), title) os.rename(recording_dir, renamed_dir) with open(os.path.join(renamed_dir, 'README.md'), 'w') as f: f.write(description) self.on_recording_stopped() @pyqtSlot() def on_recording_stopped(self): self.update_menu(False) def update_menu(self, is_recording: bool): self.toggle_record_button.setText("Stop Recording" if is_recording else "Start Recording") self.toggle_record_action.setText("Stop Recording" if is_recording else "Start Recording") self.toggle_pause_button.setEnabled(is_recording) self.toggle_pause_action.setVisible(is_recording) def display_error_message(self, message): QMessageBox.critical(None, "Error", message) def resource_path(relative_path: str) -> str: if hasattr(sys, '_MEIPASS'): base_path = getattr(sys, "_MEIPASS") else: base_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..') return os.path.join(base_path, relative_path)