feat: hide library toolbar buttons option, Ctrl+O hotkey, iTunes-style equalizer animation on playing track
This commit is contained in:
parent
ec797b8074
commit
d153d69bfd
@ -142,6 +142,8 @@ class ConfigLoader:
|
||||
self.config.set('Advanced', 'clean_temp', 'true')
|
||||
if not self.config.has_option('Advanced', 'show_download'):
|
||||
self.config.set('Advanced', 'show_download', 'false')
|
||||
if not self.config.has_option('Advanced', 'hide_library_buttons'):
|
||||
self.config.set('Advanced', 'hide_library_buttons', 'false')
|
||||
|
||||
# Hotkeys section
|
||||
if not self.config.has_section('Hotkeys'):
|
||||
@ -165,6 +167,7 @@ class ConfigLoader:
|
||||
'ipod_refresh': 'Ctrl+R',
|
||||
'download': 'Ctrl+Enter',
|
||||
'delete_selected': 'Delete',
|
||||
'library_add_files': 'Ctrl+O',
|
||||
}
|
||||
for key, value in hotkey_defaults.items():
|
||||
if not self.config.has_option('Hotkeys', key):
|
||||
|
||||
@ -95,6 +95,7 @@ DEFAULTS = [
|
||||
("download", "Ctrl+Enter", "Start Download", "global"),
|
||||
("delete_selected", "Delete", "Delete Selected", "global"),
|
||||
("edit_metadata", "F2", "Edit Metadata", "global"),
|
||||
("library_add_files", "Ctrl+O", "Library: Add Files", "global"),
|
||||
]
|
||||
|
||||
|
||||
|
||||
202
src/main.py
202
src/main.py
@ -15,10 +15,11 @@ from PyQt6.QtWidgets import (
|
||||
QPushButton, QLabel, QLineEdit, QProgressBar, QFileDialog,
|
||||
QComboBox, QCheckBox, QTabWidget, QListWidget, QListWidgetItem,
|
||||
QMessageBox, QSpinBox, QGroupBox, QHeaderView,
|
||||
QTableWidget, QTableWidgetItem, QSlider, QMenu, QDialog
|
||||
QTableWidget, QTableWidgetItem, QSlider, QMenu, QDialog, QStyle,
|
||||
QStyleOptionSlider
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl
|
||||
from PyQt6.QtGui import QPixmap
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal, QUrl, QTimer
|
||||
from PyQt6.QtGui import QPixmap, QColor
|
||||
from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
|
||||
|
||||
from youtube_downloader import YouTubeDownloader, TrackInfo
|
||||
@ -38,6 +39,23 @@ logger = logging.getLogger(__name__)
|
||||
AUDIO_EXTS = {'.m4a', '.mp3', '.aac'}
|
||||
SOURCE_EXTS = {'.flac', '.wav', '.ogg', '.opus', '.wma', '.ape', '.aiff', '.alac'}
|
||||
|
||||
EQ_FRAMES = [
|
||||
"\u2581\u2583\u2585",
|
||||
"\u2582\u2585\u2587",
|
||||
"\u2583\u2587\u2586",
|
||||
"\u2585\u2586\u2584",
|
||||
"\u2586\u2584\u2583",
|
||||
"\u2587\u2583\u2582",
|
||||
"\u2586\u2582\u2581",
|
||||
"\u2585\u2581\u2583",
|
||||
"\u2583\u2582\u2585",
|
||||
"\u2581\u2583\u2587",
|
||||
"\u2582\u2585\u2586",
|
||||
"\u2583\u2587\u2584",
|
||||
"\u2585\u2586\u2583",
|
||||
"\u2586\u2584\u2581",
|
||||
]
|
||||
|
||||
|
||||
class WorkerThread(QThread):
|
||||
"""Worker thread for background tasks"""
|
||||
@ -268,6 +286,31 @@ class WorkerThread(QThread):
|
||||
self.wait()
|
||||
|
||||
|
||||
class JumpSlider(QSlider):
|
||||
def _jump_to_pos(self, x):
|
||||
opt = QStyleOptionSlider()
|
||||
self.initStyleOption(opt)
|
||||
gr = self.style().subControlRect(
|
||||
QStyle.ComplexControl.CC_Slider, opt,
|
||||
QStyle.SubControl.SC_SliderGroove, self
|
||||
)
|
||||
val = QStyle.sliderValueFromPosition(
|
||||
self.minimum(), self.maximum(),
|
||||
int(x) - gr.x(), gr.width()
|
||||
)
|
||||
self.setValue(val)
|
||||
|
||||
def mousePressEvent(self, ev):
|
||||
if ev.button() == Qt.MouseButton.LeftButton:
|
||||
self._jump_to_pos(ev.position().x())
|
||||
super().mousePressEvent(ev)
|
||||
|
||||
def mouseMoveEvent(self, ev):
|
||||
if ev.buttons() & Qt.MouseButton.LeftButton:
|
||||
self._jump_to_pos(ev.position().x())
|
||||
super().mouseMoveEvent(ev)
|
||||
|
||||
|
||||
class MainWindow(QMainWindow):
|
||||
"""Main application window"""
|
||||
|
||||
@ -285,6 +328,8 @@ class MainWindow(QMainWindow):
|
||||
self.ipod_devices: List[Dict] = []
|
||||
self.current_mount_point: Optional[str] = None
|
||||
self.show_download_tab = False
|
||||
self.hide_library_buttons = False
|
||||
self._library_toolbar_buttons: list = []
|
||||
self.ipod_tab_index = 2
|
||||
|
||||
self.config_loader = ConfigLoader()
|
||||
@ -293,6 +338,9 @@ class MainWindow(QMainWindow):
|
||||
self.player: Optional[QMediaPlayer] = None
|
||||
self.audio_output: Optional[QAudioOutput] = None
|
||||
self.current_playback_index: int = -1
|
||||
self._eq_timer: Optional[QTimer] = None
|
||||
self._eq_frame: int = 0
|
||||
self._eq_running: bool = False
|
||||
self._is_playing = False
|
||||
self._saved_volume: int = 80
|
||||
self._saved_track_path: str = ""
|
||||
@ -351,6 +399,10 @@ class MainWindow(QMainWindow):
|
||||
self.show_download_tab = show_download
|
||||
self.tab_widget.setTabVisible(self.download_tab_index, show_download)
|
||||
|
||||
hide_library_buttons = config.get_boolean("Advanced", "hide_library_buttons", fallback=False)
|
||||
self.hide_library_buttons_check.setChecked(hide_library_buttons)
|
||||
self._on_hide_library_buttons_toggled(hide_library_buttons)
|
||||
|
||||
self._saved_volume = config.get_int("Playback", "last_volume", fallback=80)
|
||||
self._saved_track_path = config.get("Playback", "last_track_path", fallback="")
|
||||
self._saved_position_ms = config.get_int("Playback", "last_position_ms", fallback=0)
|
||||
@ -368,6 +420,7 @@ class MainWindow(QMainWindow):
|
||||
config.set("Advanced", "clean_temp", str(self.clean_temp_check.isChecked()).lower())
|
||||
config.set("Device", "auto_detect", str(self.auto_detect_check.isChecked()).lower())
|
||||
config.set("Advanced", "show_download", str(self.show_download_check.isChecked()).lower())
|
||||
config.set("Advanced", "hide_library_buttons", str(self.hide_library_buttons_check.isChecked()).lower())
|
||||
|
||||
config.set("Playback", "last_volume", str(self.volume_slider.value()))
|
||||
if self.current_playback_index >= 0 and self.player.source().isValid():
|
||||
@ -409,6 +462,7 @@ class MainWindow(QMainWindow):
|
||||
"download": self._on_download_clicked,
|
||||
"delete_selected": self._delete_selected_current,
|
||||
"edit_metadata": self._on_edit_metadata,
|
||||
"library_add_files": self._on_library_add_files,
|
||||
})
|
||||
self._refresh_shortcuts_table()
|
||||
|
||||
@ -522,12 +576,13 @@ class MainWindow(QMainWindow):
|
||||
self.time_label_start.setFixedWidth(40)
|
||||
self.time_label_start.setAlignment(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter)
|
||||
|
||||
self.position_slider = QSlider(Qt.Orientation.Horizontal)
|
||||
self.position_slider = JumpSlider(Qt.Orientation.Horizontal)
|
||||
self.position_slider.setMinimum(0)
|
||||
self.position_slider.setMaximum(1000)
|
||||
self.position_slider.setValue(0)
|
||||
self.position_slider.sliderPressed.connect(self._on_slider_pressed)
|
||||
self.position_slider.sliderReleased.connect(self._on_slider_released)
|
||||
self.position_slider.sliderMoved.connect(self._on_slider_pressed)
|
||||
self.position_slider.sliderReleased.connect(self._on_slider_pressed)
|
||||
|
||||
self.time_label_end = QLabel("0:00")
|
||||
self.time_label_end.setFixedWidth(40)
|
||||
@ -633,6 +688,15 @@ class MainWindow(QMainWindow):
|
||||
refresh_btn.clicked.connect(self._on_refresh_library_clicked)
|
||||
toolbar.addWidget(refresh_btn)
|
||||
|
||||
self._library_toolbar_buttons = [
|
||||
self.library_add_btn,
|
||||
self.library_convert_btn,
|
||||
self.library_transfer_btn,
|
||||
self.library_remove_btn,
|
||||
self.library_edit_btn,
|
||||
refresh_btn,
|
||||
]
|
||||
|
||||
toolbar.addStretch()
|
||||
|
||||
self.library_search_input = QLineEdit()
|
||||
@ -786,6 +850,11 @@ class MainWindow(QMainWindow):
|
||||
self.show_download_check.toggled.connect(self._on_show_download_toggled)
|
||||
advanced_layout.addWidget(self.show_download_check)
|
||||
|
||||
self.hide_library_buttons_check = QCheckBox("Hide Library Toolbar Buttons")
|
||||
self.hide_library_buttons_check.setChecked(False)
|
||||
self.hide_library_buttons_check.toggled.connect(self._on_hide_library_buttons_toggled)
|
||||
advanced_layout.addWidget(self.hide_library_buttons_check)
|
||||
|
||||
layout.addWidget(advanced_group)
|
||||
|
||||
shortcuts_group = QGroupBox("Keyboard Shortcuts")
|
||||
@ -824,6 +893,11 @@ class MainWindow(QMainWindow):
|
||||
self.show_download_tab = checked
|
||||
self.tab_widget.setTabVisible(self.download_tab_index, checked)
|
||||
|
||||
def _on_hide_library_buttons_toggled(self, checked: bool):
|
||||
self.hide_library_buttons = checked
|
||||
for btn in self._library_toolbar_buttons:
|
||||
btn.setVisible(not checked)
|
||||
|
||||
def _setup_player(self):
|
||||
"""Initialize QMediaPlayer and QAudioOutput for playback."""
|
||||
self.player = QMediaPlayer()
|
||||
@ -835,6 +909,10 @@ class MainWindow(QMainWindow):
|
||||
self.player.durationChanged.connect(self._on_media_duration_changed)
|
||||
self.player.mediaStatusChanged.connect(self._on_media_status_changed)
|
||||
|
||||
self._eq_timer = QTimer()
|
||||
self._eq_timer.setInterval(80)
|
||||
self._eq_timer.timeout.connect(self._tick_eq_animation)
|
||||
|
||||
def _play_track_from_index(self, index: int, start_position_ms: int = 0, auto_play: bool = True):
|
||||
"""Start playing a track from the library table by row index."""
|
||||
if index < 0 or index >= self.library_table.rowCount():
|
||||
@ -846,6 +924,9 @@ class MainWindow(QMainWindow):
|
||||
if not track_data or not os.path.exists(track_data.get("path", "")):
|
||||
return
|
||||
|
||||
old_index = self.current_playback_index
|
||||
self._clear_playing_row(old_index)
|
||||
|
||||
self.current_playback_index = index
|
||||
self.player.setSource(QUrl.fromLocalFile(track_data["path"]))
|
||||
if auto_play:
|
||||
@ -881,6 +962,10 @@ class MainWindow(QMainWindow):
|
||||
self.library_table.clearSelection()
|
||||
self.library_table.selectRow(index)
|
||||
|
||||
self._set_playing_row_bg(index)
|
||||
if auto_play:
|
||||
self._start_eq_animation()
|
||||
|
||||
if start_position_ms > 0:
|
||||
self._pending_seek_ms = start_position_ms
|
||||
else:
|
||||
@ -893,6 +978,7 @@ class MainWindow(QMainWindow):
|
||||
self.player.pause()
|
||||
self._is_playing = False
|
||||
self.play_btn.setText("\u25B6")
|
||||
self._stop_eq_animation()
|
||||
else:
|
||||
if self.player.playbackState() == QMediaPlayer.PlaybackState.StoppedState:
|
||||
# Nothing playing yet — play first selected row or row 0
|
||||
@ -903,6 +989,7 @@ class MainWindow(QMainWindow):
|
||||
self.player.play()
|
||||
self._is_playing = True
|
||||
self.play_btn.setText("\u23F8")
|
||||
self._start_eq_animation()
|
||||
|
||||
def _on_prev_clicked(self):
|
||||
if self.player is None:
|
||||
@ -978,8 +1065,10 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def _on_media_status_changed(self, status):
|
||||
if status == QMediaPlayer.MediaStatus.EndOfMedia:
|
||||
self._stop_eq_animation()
|
||||
self._on_next_clicked()
|
||||
elif status == QMediaPlayer.MediaStatus.NoMedia:
|
||||
self._stop_eq_animation()
|
||||
self.play_btn.setText("\u25B6")
|
||||
self._is_playing = False
|
||||
elif status == QMediaPlayer.MediaStatus.LoadedMedia:
|
||||
@ -987,6 +1076,75 @@ class MainWindow(QMainWindow):
|
||||
self.player.setPosition(self._pending_seek_ms)
|
||||
self._pending_seek_ms = 0
|
||||
|
||||
def _start_eq_animation(self):
|
||||
if not self._eq_timer or self._eq_running:
|
||||
return
|
||||
self._eq_running = True
|
||||
self._eq_frame = 0
|
||||
self._tick_eq_animation()
|
||||
self._eq_timer.start()
|
||||
|
||||
def _stop_eq_animation(self):
|
||||
if not self._eq_timer:
|
||||
return
|
||||
self._eq_timer.stop()
|
||||
self._eq_running = False
|
||||
idx = self.current_playback_index
|
||||
if idx >= 0 and idx < self.library_table.rowCount():
|
||||
item = self.library_table.item(idx, 0)
|
||||
if item:
|
||||
is_ready, data = item.data(Qt.ItemDataRole.UserRole)
|
||||
track_num = data.get("track_num", 0) if data else 0
|
||||
item.setText(str(track_num) if (is_ready and track_num) else "")
|
||||
item.setData(Qt.ItemDataRole.ForegroundRole, None)
|
||||
|
||||
def _tick_eq_animation(self):
|
||||
idx = self.current_playback_index
|
||||
if idx < 0 or idx >= self.library_table.rowCount():
|
||||
return
|
||||
item = self.library_table.item(idx, 0)
|
||||
if item is None:
|
||||
return
|
||||
glyph = EQ_FRAMES[self._eq_frame % len(EQ_FRAMES)]
|
||||
self._eq_frame += 1
|
||||
item.setText(glyph)
|
||||
accent = self.palette().color(self.palette().ColorRole.Highlight)
|
||||
item.setForeground(accent)
|
||||
|
||||
def _set_playing_row_bg(self, row: int):
|
||||
if row < 0 or row >= self.library_table.rowCount():
|
||||
return
|
||||
bg = QColor(self.palette().color(self.palette().ColorRole.Highlight))
|
||||
bg.setAlpha(50)
|
||||
for col in range(self.library_table.columnCount()):
|
||||
item = self.library_table.item(row, col)
|
||||
if item:
|
||||
item.setBackground(bg)
|
||||
|
||||
def _clear_playing_row_bg(self, row: int):
|
||||
if row < 0 or row >= self.library_table.rowCount():
|
||||
return
|
||||
for col in range(self.library_table.columnCount()):
|
||||
item = self.library_table.item(row, col)
|
||||
if item:
|
||||
item.setData(Qt.ItemDataRole.BackgroundRole, None)
|
||||
|
||||
def _clear_playing_row(self, row: int):
|
||||
if row < 0 or row >= self.library_table.rowCount():
|
||||
return
|
||||
for col in range(self.library_table.columnCount()):
|
||||
item = self.library_table.item(row, col)
|
||||
if item:
|
||||
item.setData(Qt.ItemDataRole.BackgroundRole, None)
|
||||
self._eq_timer.stop() if self._eq_timer else None
|
||||
self._eq_running = False
|
||||
item0 = self.library_table.item(row, 0)
|
||||
if item0:
|
||||
is_ready, data = item0.data(Qt.ItemDataRole.UserRole)
|
||||
track_num = data.get("track_num", 0) if data else 0
|
||||
item0.setText(str(track_num) if (is_ready and track_num) else "")
|
||||
item0.setData(Qt.ItemDataRole.ForegroundRole, None)
|
||||
|
||||
def _on_table_double_clicked(self, row: int):
|
||||
self._play_track_from_index(row)
|
||||
|
||||
@ -1240,6 +1398,40 @@ class MainWindow(QMainWindow):
|
||||
status = f"{len(all_tracks)} track(s) \u2014 {', '.join(parts)}" if parts else "No tracks in library"
|
||||
self.library_status.setText(status)
|
||||
|
||||
self._restore_playing_row()
|
||||
|
||||
def _restore_playing_row(self):
|
||||
idx = self.current_playback_index
|
||||
if idx < 0:
|
||||
return
|
||||
track_path = ""
|
||||
if self.player and self.player.source().isValid():
|
||||
track_path = self.player.source().toLocalFile()
|
||||
if not track_path and idx < self.library_table.rowCount():
|
||||
item = self.library_table.item(idx, 0)
|
||||
if item:
|
||||
_, data = item.data(Qt.ItemDataRole.UserRole)
|
||||
if data:
|
||||
track_path = data.get("path", "")
|
||||
|
||||
if not track_path:
|
||||
self.current_playback_index = -1
|
||||
return
|
||||
|
||||
for row in range(self.library_table.rowCount()):
|
||||
item = self.library_table.item(row, 0)
|
||||
if item is None:
|
||||
continue
|
||||
_, data = item.data(Qt.ItemDataRole.UserRole)
|
||||
if data and data.get("path") == track_path:
|
||||
self.current_playback_index = row
|
||||
self._set_playing_row_bg(row)
|
||||
if self._is_playing:
|
||||
self._start_eq_animation()
|
||||
return
|
||||
|
||||
self.current_playback_index = -1
|
||||
|
||||
def _get_selected_ready_tracks(self) -> List[Tuple[TrackInfo, str]]:
|
||||
selected_rows = set()
|
||||
for item in self.library_table.selectedItems():
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user