37 Commits

Author SHA1 Message Date
Martin Wagner
02dec1f5f8 preparations for 1.8.0 2022-09-11 11:50:37 +02:00
Martin Wagner
4cc8c63ef9 always clear window title on startup 2022-09-03 11:30:27 +02:00
Martin Wagner
806b599db2 don't enable back-to-current-album on reconnect 2022-09-03 11:18:16 +02:00
Martin Wagner
279107d2a6 fixed get_current_album_path 2022-09-03 11:15:34 +02:00
Martin Wagner
a99ce64c66 fixed back_to_current_album 2022-09-03 10:38:24 +02:00
Martin Wagner
f24f58e930 always select current album in AlbumList when possible 2022-09-03 10:12:41 +02:00
Martin Wagner
750a311327 set selection mode to BROWSE in AlbumList 2022-09-02 23:59:27 +02:00
Martin Wagner
23f9ffa76c avoid too many "disconnected" signals 2022-08-27 14:11:41 +02:00
Martin Wagner
6ed7a9c46d fixed typo 2022-08-27 14:05:58 +02:00
Martin Wagner
98d1a140bf separated selecting and scrolling in SelectionList 2022-08-27 14:05:10 +02:00
Martin Wagner
76c27816a8 improved auto scrolling in browser 2022-08-27 13:53:28 +02:00
Martin Wagner
b1994fc4bc fixed double selection in SelectionList 2022-08-27 13:50:11 +02:00
Martin Wagner
84216f569f disabled double-click play in ArtistList 2022-08-27 13:42:35 +02:00
Martin Wagner
a7a209d577 reworked SelectionList 2022-08-27 13:39:13 +02:00
Martin Wagner
953d5ec8d3 use try, except, else 2022-08-08 18:25:23 +02:00
Martin Wagner
9ed5e67f5e fixed some scrolling issues 2022-07-18 19:29:46 +02:00
Martin Wagner
fdeaad4b9f improved back_to_current_song_button to cover less information 2022-07-16 15:16:25 +02:00
Martin Wagner
f6124ae45e Merge pull request #53 from MasFlam/master
Add Polish translation
2022-06-20 19:51:53 +02:00
MasFlam
76b7b3b45b Add Polish translation 2022-06-20 19:24:44 +02:00
Martin Wagner
7e8b169705 reworked Duration 2022-05-28 14:16:56 +02:00
Martin Wagner
3e2f8c51d2 use GLib.DateTime to parse time 2022-05-27 22:52:02 +02:00
Martin Wagner
6acef07ccc unified time strings 2022-05-27 22:13:04 +02:00
Martin Wagner
666fe08208 fixed cover aspect ratio to 1 2022-05-26 15:58:50 +02:00
Martin Wagner
7f2cccdbab fixed fallback cover placing 2022-05-14 15:22:19 +02:00
Martin Wagner
2369aca946 fixed blurry fallback covers 2022-05-14 14:46:46 +02:00
Martin Wagner
22f452c4ef fixed normal player restore 2022-04-23 14:01:23 +02:00
Martin Wagner
d28b9901cb load svg at sufficient size 2022-04-23 13:25:43 +02:00
Martin Wagner
142ff75774 prevent silent crashes 2022-04-23 13:20:10 +02:00
Martin Wagner
9869bd8753 fixed popover placing in AlbumList 2022-04-23 13:06:17 +02:00
Martin Wagner
6f188128ae use get_cursor where possible 2022-04-23 13:03:44 +02:00
Martin Wagner
f240215b1f reworked playlist highlighting 2022-04-23 12:36:51 +02:00
Martin Wagner
df3f996f71 fixed default mini player size 2022-04-21 17:24:06 +02:00
Martin Wagner
67c914002d auto resize main cover 2022-04-21 17:14:43 +02:00
Martin Wagner
6198a821db fixed ctrl-c behavior 2022-04-16 22:02:18 +02:00
Martin Wagner
d07ca0697d set sensitivity of back_button 2022-04-16 21:21:50 +02:00
Martin Wagner
32893f7062 fixed MPRIS Previous method according to the specs
Using conditional previous in the MPRIS interface introduced some bugs.
It was not possible to go to the previous track via plasma's MPRIS client.
2022-04-16 20:47:59 +02:00
Martin Wagner
aacf27ccb0 added workaround for Gtk bug 2022-04-09 11:35:30 +02:00
10 changed files with 823 additions and 212 deletions

View File

@@ -2,7 +2,7 @@ README for mpdevil
==================
mpdevil is a simple music browser for the Music Player Daemon (MPD) which is focused on playing local music without the need of managing playlists. Instead of maintaining a client side database of your music library, mpdevil loads all tags and covers on demand. So you'll never see any outdated information in the browser. mpdevil strongly relies on tags.
![ScreenShot](screenshots/mainwindow_1.7.0.png)
![ScreenShot](screenshots/mainwindow_1.8.0.png)
Features
--------

View File

@@ -3,13 +3,14 @@
<object class="GtkAboutDialog" id="about_dialog">
<property name="modal">True</property>
<property name="program_name">mpdevil</property>
<property name="version">1.7.0</property>
<property name="version">1.8.0</property>
<property name="comments" translatable="yes">A simple music browser for MPD</property>
<property name="authors">Martin Wagner</property>
<property name="translator_credits">Martin de Reuver
Georgi Kamenov
Martin Wagner
Oğuz Ersen</property>
Oğuz Ersen
Łukasz Drukała</property>
<property name="website">https://github.com/SoongNoonien/mpdevil</property>
<property name="copyright">Copyright © 2020-2022 Martin Wagner</property>
<property name="license_type">gpl-3-0</property>

View File

@@ -19,12 +19,12 @@
</ul>
</description>
<releases>
<release version="1.7.0" date="2022-04-07"/>
<release version="1.8.0" date="2022-09-11"/>
</releases>
<launchable type="desktop-id">org.mpdevil.mpdevil.desktop</launchable>
<screenshots>
<screenshot type="default">
<image type="source" width="1121" height="790">https://raw.githubusercontent.com/SoongNoonien/mpdevil/v1.7.0/screenshots/mainwindow_1.7.0.png</image>
<image type="source" width="1121" height="790">https://raw.githubusercontent.com/SoongNoonien/mpdevil/v1.8.0/screenshots/mainwindow_1.8.0.png</image>
</screenshot>
</screenshots>
<url type="homepage">https://github.com/SoongNoonien/mpdevil</url>

View File

@@ -16,6 +16,14 @@
<default>691</default>
<summary>Default height of window</summary>
</key>
<key type="i" name="mini-player-width">
<default>400</default>
<summary>Default width of mini player</summary>
</key>
<key type="i" name="mini-player-height">
<default>447</default>
<summary>Default height of mini player</summary>
</key>
<key type="i" name="paned0">
<default>350</default>
<summary>Default position of cover/playlist separator</summary>
@@ -44,10 +52,6 @@
<default>160</default>
<summary>Size of covers in album view</summary>
</key>
<key type="i" name="track-cover">
<default>350</default>
<summary>Size of main cover</summary>
</key>
<key type="i" name="icon-size">
<default>24</default>
<summary>Size of icons in main control bar</summary>

View File

@@ -1,4 +1,4 @@
project('mpdevil', version: '1.7.0')
project('mpdevil', version: '1.8.0')
i18n = import('i18n')
gnome = import('gnome')

View File

@@ -1 +1 @@
de nl bg tr
de nl bg tr pl

563
po/pl.po Normal file
View File

@@ -0,0 +1,563 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the mpdevil package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
msgid ""
msgstr ""
"Project-Id-Version: mpdevil\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-04-07 12:12+0200\n"
"PO-Revision-Date: 2022-06-20 19:15+0200\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: pl\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && "
"(n%100<12 || n%100>14) ? 1 : 2);\n"
"X-Generator: Poedit 3.1\n"
#: src/mpdevil.py:504
#, python-brace-format
msgid "{days} day"
msgid_plural "{days} days"
msgstr[0] "{days} dzień"
msgstr[1] "{days} dni"
msgstr[2] "{days} dni"
#: src/mpdevil.py:541
#, python-brace-format
msgid "{channels} channel"
msgid_plural "{channels} channels"
msgstr[0] "{channels} kanał"
msgstr[1] "{channels} kanały"
msgstr[2] "{channels} kanałów"
#: src/mpdevil.py:1012
msgid "(restart required)"
msgstr "(wymagane ponowne uruchomienie)"
#: src/mpdevil.py:1058
msgid "Use Client-side decoration"
msgstr "Używaj dekoracji po stronie klienta"
#: src/mpdevil.py:1059
msgid "Show stop button"
msgstr "Pokaż przycisk „stop”"
#: src/mpdevil.py:1060
msgid "Show audio format"
msgstr "Pokaż format audio"
#: src/mpdevil.py:1061
msgid "Show lyrics button"
msgstr "Pokaż przycisk tekstu utworu"
#: src/mpdevil.py:1062
msgid "Place playlist at the side"
msgstr "Umieść playlistę z boku"
#: src/mpdevil.py:1068
msgid "Main cover size"
msgstr "Rozmiar głównej okładki"
#: src/mpdevil.py:1069
msgid "Album view cover size"
msgstr "Rozmiar okładek w przeglądarce"
#: src/mpdevil.py:1070
msgid "Action bar icon size"
msgstr "Rozmiar ikon na pasku akcji"
#: src/mpdevil.py:1080
msgid "Support “MPRIS”"
msgstr "Wspieraj „MPRIS”"
#: src/mpdevil.py:1081
msgid "Sort albums by year"
msgstr "Sortuj albumy według roku"
#: src/mpdevil.py:1082
msgid "Send notification on title change"
msgstr "Wysyłaj powiadomienie przy zmianie utworu"
#: src/mpdevil.py:1083
msgid "Play selected albums and titles immediately"
msgstr "Odtwarzaj wybrane albumy i utwory od razu"
#: src/mpdevil.py:1084
msgid "Rewind via previous button"
msgstr "Przewijaj w tył za pomocą przycisku „poprzedni”"
#: src/mpdevil.py:1085
msgid "Stop playback on quit"
msgstr "Zatrzymaj odtwarzanie przy wyjściu"
#: src/mpdevil.py:1112
msgid "Choose directory"
msgstr "Wybierz katalog"
#: src/mpdevil.py:1125
msgid "Connect via Unix domain socket"
msgstr "Połącz się poprzez Unix domain socket"
#: src/mpdevil.py:1144
msgid ""
"The first image in the same directory as the song file matching this regex "
"will be displayed. %AlbumArtist% and %Album% will be replaced by the "
"corresponding tags of the song."
msgstr ""
"Pierwszy obrazek w tym samym katalogu co plik utworu, który pasuje do tego "
"wyrażenia regularnego, zostanie wyświetlony. %AlbumArtist% oraz %Album% "
"zostaną zastąpione odpowiednimi tagami utworu."
#: src/mpdevil.py:1149
msgid "Socket:"
msgstr "Socket:"
#: src/mpdevil.py:1151
msgid "Host:"
msgstr "Host:"
#: src/mpdevil.py:1153
msgid "Password:"
msgstr "Hasło:"
#: src/mpdevil.py:1154
msgid "Music lib:"
msgstr "Biblioteka muzyczna:"
#: src/mpdevil.py:1156
msgid "Cover regex:"
msgstr "Wyrażenie regularne okładki:"
#: src/mpdevil.py:1180 src/mpdevil.py:3560
msgid "Profile 1"
msgstr "Profil 1"
#: src/mpdevil.py:1181 src/mpdevil.py:3560
msgid "Profile 2"
msgstr "Profil 2"
#: src/mpdevil.py:1182 src/mpdevil.py:3560
msgid "Profile 3"
msgstr "Profil 3"
#. connect button
#: src/mpdevil.py:1186 src/mpdevil.py:3448
msgid "Connect"
msgstr "Połącz się"
#: src/mpdevil.py:1209 src/mpdevil.py:1211 src/mpdevil.py:3449
#: src/mpdevil.py:3552
msgid "Preferences"
msgstr "Preferencje"
#: src/mpdevil.py:1224 src/mpdevil.py:1234
msgid "View"
msgstr "Widok"
#: src/mpdevil.py:1225 src/mpdevil.py:1235
msgid "Behavior"
msgstr "Zachowanie"
#: src/mpdevil.py:1226 src/mpdevil.py:1236
msgid "Profiles"
msgstr "Profile"
#: src/mpdevil.py:1253
msgid "Stats"
msgstr "Statystyki"
#: src/mpdevil.py:1262
msgid "<b>Protocol:</b>"
msgstr "<b>Protokół:</b>"
#: src/mpdevil.py:1263
msgid "<b>Uptime:</b>"
msgstr "<b>Czas działania:</b>"
#: src/mpdevil.py:1264
msgid "<b>Playtime:</b>"
msgstr "<b>Czas odtwarzania:</b>"
#: src/mpdevil.py:1265
msgid "<b>Artists:</b>"
msgstr "<b>Artyści:</b>"
#: src/mpdevil.py:1266
msgid "<b>Albums:</b>"
msgstr "<b>Albumy:</b>"
#: src/mpdevil.py:1267
msgid "<b>Songs:</b>"
msgstr "<b>Utwory:</b>"
#: src/mpdevil.py:1268
msgid "<b>Total Playtime:</b>"
msgstr "<b>Całkowity czas odtwarzania:</b>"
#: src/mpdevil.py:1269
msgid "<b>Database Update:</b>"
msgstr "<b>Aktualizacja bazy danych:</b>"
#: src/mpdevil.py:1348
msgid "Add to playlist"
msgstr "Dodaj do playlisty"
#: src/mpdevil.py:1351
msgid "Show in file manager"
msgstr "Pokaż w menedżerze plików"
#: src/mpdevil.py:1355 src/mpdevil.py:1590 src/mpdevil.py:2304
msgid "Append"
msgstr "Dodaj"
#: src/mpdevil.py:1356 src/mpdevil.py:1591 src/mpdevil.py:2305
msgid "Play"
msgstr "Odtwórz"
#: src/mpdevil.py:1357 src/mpdevil.py:1592 src/mpdevil.py:2306
msgid "Enqueue"
msgstr "Dodaj do kolejki"
#: src/mpdevil.py:1374
msgid "MPD-Tag"
msgstr "Tag MPD"
#: src/mpdevil.py:1377
msgid "Value"
msgstr "Wartość"
#: src/mpdevil.py:1448 src/mpdevil.py:2510
msgid "No"
msgstr "Nie"
#. the order of weight_set and weight seems to be important here
#: src/mpdevil.py:1449 src/mpdevil.py:2512
msgid "Title"
msgstr "Tytuł"
#: src/mpdevil.py:1450 src/mpdevil.py:2513
msgid "Length"
msgstr "Długość"
#: src/mpdevil.py:1463
msgid "Add all titles to playlist"
msgstr "Dodaj wszystkie utwory do playlisty"
#: src/mpdevil.py:1464
msgid "Directly play all titles"
msgstr "Bezpośrednio odtwórz wszystkie utwory"
#: src/mpdevil.py:1465
msgid ""
"Append all titles after the currently playing track and clear the playlist "
"from all other songs"
msgstr ""
"Dodaj wszystkie utwory po aktualnie odtwarzanym i usuń wszystkie inne z "
"playlisty"
#: src/mpdevil.py:1559 src/mpdevil.py:2394 src/mpdevil.py:2671
#, python-brace-format
msgid "{number} song ({duration})"
msgid_plural "{number} songs ({duration})"
msgstr[0] "{number} utwór ({duration})"
msgstr[1] "{number} utwory ({duration})"
msgstr[2] "{number} utworów ({duration})"
#: src/mpdevil.py:1658
#, python-brace-format
msgid "{hits} hit"
msgid_plural "{hits} hits"
msgstr[0] "{hits} trafienie"
msgstr[1] "{hits} trafienia"
msgstr[2] "{hits} trafień"
#: src/mpdevil.py:1743
msgid "all tags"
msgstr "wszystkie tagi"
#: src/mpdevil.py:1875
msgid "all genres"
msgstr "wszystkie gatunki"
#: src/mpdevil.py:1901
msgid "all artists"
msgstr "wszyscy artyści"
#: src/mpdevil.py:2314
msgid "Save"
msgstr "Zapisz"
#: src/mpdevil.py:2318
msgid "Delete"
msgstr "Usuń"
#: src/mpdevil.py:2439 data/ShortcutsWindow.ui:240
msgid "Clear playlist"
msgstr "Wyczyść playlistę"
#: src/mpdevil.py:2701
msgid "Scroll to current song"
msgstr "Przewiń do aktualnego utworu"
#: src/mpdevil.py:2713
msgid "Playlists"
msgstr "Playlisty"
#: src/mpdevil.py:2814
msgid "searching…"
msgstr "wyszukiwanie…"
#: src/mpdevil.py:2819
msgid "connection error"
msgstr "błąd połączenia"
#: src/mpdevil.py:2821
msgid "lyrics not found"
msgstr "nie znaleziono tekstu utworu"
#: src/mpdevil.py:2924
msgid "Lyrics"
msgstr "Tekst utworu"
#: src/mpdevil.py:3015 src/mpdevil.py:3016
#, python-brace-format
msgid "{number} song"
msgid_plural "{number} songs"
msgstr[0] "{number} utwór"
msgstr[1] "{number} utwory"
msgstr[2] "{number} utworów"
#: src/mpdevil.py:3201
msgid "Repeat mode"
msgstr "Tryb powtarzania"
#: src/mpdevil.py:3202
msgid "Random mode"
msgstr "Tryb losowy"
#: src/mpdevil.py:3203
msgid "Single mode"
msgstr "Tryb pojedynczy"
#: src/mpdevil.py:3204
msgid "Consume mode"
msgstr "Tryb wyczerpywania"
#: src/mpdevil.py:3418
msgid "Updating Database…"
msgstr "Aktualizowanie bazy danych…"
#: src/mpdevil.py:3470
#, python-brace-format
msgid "Connection to “{socket}” failed"
msgstr "Nie udało połączyć się do „{socket}”"
#: src/mpdevil.py:3472
#, python-brace-format
msgid "Connection to “{host}:{port}” failed"
msgstr "Nie udało połączyć się do „{host}:{port}”"
#: src/mpdevil.py:3537
msgid "Search"
msgstr "Szukaj"
#: src/mpdevil.py:3540 data/ShortcutsWindow.ui:99
msgid "Back to current album"
msgstr "Wróć do aktualnego albumu"
#: src/mpdevil.py:3553
msgid "Keyboard Shortcuts"
msgstr "Skróty klawiszowe"
#: src/mpdevil.py:3554
msgid "Help"
msgstr "Pomoc"
#: src/mpdevil.py:3555
msgid "About mpdevil"
msgstr "O mpdevil"
#: src/mpdevil.py:3557
msgid "Update Database"
msgstr "Aktualizuj bazę danych"
#: src/mpdevil.py:3558
msgid "Server Stats"
msgstr "Statystyki serwera"
#: src/mpdevil.py:3565
msgid "Mini Player"
msgstr "Mini odtwarzacz"
#: src/mpdevil.py:3566
msgid "Genre Filter"
msgstr "Filtr gatunku"
#: src/mpdevil.py:3576
msgid "Menu"
msgstr "Menu"
#: src/mpdevil.py:3629 src/mpdevil.py:3631
msgid "connecting…"
msgstr "łączenie…"
#: src/mpdevil.py:3790
msgid "Debug mode"
msgstr "Tryb debugowania"
#: data/org.mpdevil.mpdevil.desktop.in:3
msgid "mpdevil"
msgstr "mpdevil"
#: data/org.mpdevil.mpdevil.desktop.in:4
msgid "MPD Client"
msgstr "Klient MPD"
#: data/org.mpdevil.mpdevil.desktop.in:5 data/AboutDialog.ui:7
msgid "A simple music browser for MPD"
msgstr "Prosta przeglądarka muzyki do MPD"
#: data/ShortcutsWindow.ui:12
msgid "General"
msgstr "Ogólne"
#: data/ShortcutsWindow.ui:16
msgid "Open online help"
msgstr "Otwórz pomoc online"
#: data/ShortcutsWindow.ui:23
msgid "Open shortcuts window"
msgstr "Otwórz okno skrótów klawiszowych"
#: data/ShortcutsWindow.ui:30
msgid "Open menu"
msgstr "Otwórz menu"
#: data/ShortcutsWindow.ui:37
msgid "Update database"
msgstr "Aktualizuj bazę danych"
#: data/ShortcutsWindow.ui:44
msgid "Quit"
msgstr "Wyjdź"
#: data/ShortcutsWindow.ui:53
msgid "Window"
msgstr "Okno"
#: data/ShortcutsWindow.ui:57
msgid "Cycle through profiles"
msgstr "Przełącz między profilami"
#: data/ShortcutsWindow.ui:64
msgid "Cycle through profiles in reversed order"
msgstr "Przełącz między profilami w odwrotnej kolejności"
#: data/ShortcutsWindow.ui:71
msgid "Toggle mini player"
msgstr "Włącz/wyłącz mini odtwarzacz"
#: data/ShortcutsWindow.ui:78
msgid "Toggle genre filter"
msgstr "Włącz/wyłącz filtr gatunku"
#: data/ShortcutsWindow.ui:85
msgid "Toggle lyrics"
msgstr "Włącz/wyłącz tekst utworu"
#: data/ShortcutsWindow.ui:92
msgid "Toggle search"
msgstr "Włącz/wyłącz wyszukiwanie"
#: data/ShortcutsWindow.ui:108
msgid "Playback"
msgstr "Odtwarzanie"
#: data/ShortcutsWindow.ui:112
msgid "Play/Pause"
msgstr "Odtwórz/Pauza"
#: data/ShortcutsWindow.ui:119
msgid "Stop"
msgstr "Stop"
#: data/ShortcutsWindow.ui:126
msgid "Stop after current title"
msgstr "Stop po tym utworze"
#: data/ShortcutsWindow.ui:133
msgid "Next title"
msgstr "Następny utwór"
#: data/ShortcutsWindow.ui:140
msgid "Previous title"
msgstr "Poprzedni utwór"
#: data/ShortcutsWindow.ui:147
msgid "Seek forward"
msgstr "Przewiń do przodu"
#: data/ShortcutsWindow.ui:154
msgid "Seek backward"
msgstr "Przewiń do tyłu"
#: data/ShortcutsWindow.ui:161
msgid "Toggle repeat mode"
msgstr "Włącz/wyłącz tryb powtarzania"
#: data/ShortcutsWindow.ui:168
msgid "Toggle random mode"
msgstr "Włącz/wyłącz tryb losowy"
#: data/ShortcutsWindow.ui:175
msgid "Toggle single mode"
msgstr "Włącz/wyłącz tryb pojedynczy"
#: data/ShortcutsWindow.ui:182
msgid "Toggle consume mode"
msgstr "Włącz/wyłącz tryb wyczerpywania"
#: data/ShortcutsWindow.ui:191
msgid "Search, Album Dialog, Album List and Artist List"
msgstr "Wyszukiwanie, Okno albumu, Lista albumów i Lista artystów"
#: data/ShortcutsWindow.ui:195
msgid "Enqueue selected item"
msgstr "Dodaj wybraną pozycję do kolejki"
#: data/ShortcutsWindow.ui:202
msgid "Append selected item"
msgstr "Dodaj wybraną pozycję"
#: data/ShortcutsWindow.ui:203 data/ShortcutsWindow.ui:233
msgid "Middle-click"
msgstr "Kółko myszy"
#: data/ShortcutsWindow.ui:210
msgid "Play selected item immediately"
msgstr "Odtwórz wybraną pozycję natychmiast"
#: data/ShortcutsWindow.ui:211
msgid "Double-click"
msgstr "Podwójne kliknięcie"
#: data/ShortcutsWindow.ui:218 data/ShortcutsWindow.ui:247
msgid "Show additional information"
msgstr "Wyświetl dodatkowe informacje"
#: data/ShortcutsWindow.ui:219 data/ShortcutsWindow.ui:248
msgid "Right-click"
msgstr "Prawy przycisk myszy"
#: data/ShortcutsWindow.ui:228
msgid "Playlist"
msgstr "Playlista"
#: data/ShortcutsWindow.ui:232
msgid "Remove selected song"
msgstr "Usuń wybrany utwór"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 KiB

View File

@@ -28,10 +28,10 @@ import urllib.error
import threading
import functools
import itertools
import datetime
import collections
import os
import sys
import signal
import re
import locale
from gettext import gettext as _, ngettext, textdomain, bindtextdomain
@@ -56,7 +56,7 @@ FALLBACK_LIB=GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_MUSIC)
############################
def idle_add(*args, **kwargs):
GLib.idle_add(*args, priority=GLib.PRIORITY_DEFAULT , **kwargs)
GLib.idle_add(*args, priority=GLib.PRIORITY_DEFAULT, **kwargs)
def main_thread_function(func):
@functools.wraps(func)
@@ -329,16 +329,17 @@ class MPRISInterface: # TODO emit Seeked if needed
setter(value)
def GetAll(self, interface_name):
read_props={}
try:
props=self._prop_mapping[interface_name]
except KeyError: # interface has no properties
return {}
else:
read_props={}
for key, (getter, setter) in props.items():
if callable(getter):
getter=getter()
read_props[key]=getter
except KeyError: # interface has no properties
pass
return read_props
return read_props
def PropertiesChanged(self, interface_name, changed_properties, invalidated_properties):
self._bus.emit_signal(
@@ -362,7 +363,7 @@ class MPRISInterface: # TODO emit Seeked if needed
self._client.next()
def Previous(self):
self._client.conditional_previous()
self._client.previous()
def Pause(self):
self._client.pause(1)
@@ -481,42 +482,39 @@ class MPRISInterface: # TODO emit Seeked if needed
######################
class Duration():
def __init__(self, value=None):
if value is None:
def __init__(self, seconds=None):
if seconds is None:
self._fallback=True
self._value=0.0
self._seconds=0.0
else:
self._fallback=False
self._value=float(value)
self._seconds=float(seconds)
def __str__(self):
if self._fallback:
return ""
else:
if self._value < 0:
sign=""
value=-int(self._value)
seconds=int(self._seconds)
days,seconds=divmod(seconds, 86400) # 86400 seconds make a day
hours,seconds=divmod(seconds, 3600) # 3600 seconds make an hour
minutes,seconds=divmod(seconds, 60)
if days > 0:
days_string=ngettext("{days} day", "{days} days", days).format(days=days)
return f"{days_string}, {hours:02d}{minutes:02d}{seconds:02d}"
elif hours > 0:
return f"{hours}{minutes:02d}{seconds:02d}"
else:
sign=""
value=int(self._value)
delta=datetime.timedelta(seconds=value)
if delta.days > 0:
days=ngettext("{days} day", "{days} days", delta.days).format(days=delta.days)
time_string=f"{days}, {datetime.timedelta(seconds=delta.seconds)}"
else:
time_string=str(delta).lstrip("0").lstrip(":")
return sign+time_string.replace(":", "") # use 'ratio' as delimiter
return f"{minutes:02d}{seconds:02d}"
def __float__(self):
return self._value
return self._seconds
class LastModified():
def __init__(self, date):
self._date=date
def __str__(self):
time=datetime.datetime.strptime(self._date, "%Y-%m-%dT%H:%M:%SZ")
return time.strftime("%a %d %B %Y, %H%M UTC")
return GLib.DateTime.new_from_iso8601(self._date).to_local().format("%a %d %B %Y, %H%M")
def raw(self):
return self._date
@@ -599,23 +597,27 @@ class Song(collections.UserDict):
return f"{title}\n<small>{GLib.markup_escape_text(self.get_album_with_date())}</small>"
class BinaryCover(bytes):
def get_pixbuf(self, size):
def get_pixbuf(self, size=-1):
loader=GdkPixbuf.PixbufLoader()
try:
loader.write(self)
loader.close()
raw_pixbuf=loader.get_pixbuf()
ratio=raw_pixbuf.get_width()/raw_pixbuf.get_height()
if ratio > 1:
pixbuf=raw_pixbuf.scale_simple(size,size/ratio,GdkPixbuf.InterpType.BILINEAR)
else:
pixbuf=raw_pixbuf.scale_simple(size*ratio,size,GdkPixbuf.InterpType.BILINEAR)
except gi.repository.GLib.Error: # load fallback if cover can't be loaded
pixbuf=GdkPixbuf.Pixbuf.new_from_file_at_size(FALLBACK_COVER, size, size)
else:
loader.close()
if size == -1:
pixbuf=loader.get_pixbuf()
else:
raw_pixbuf=loader.get_pixbuf()
ratio=raw_pixbuf.get_width()/raw_pixbuf.get_height()
if ratio > 1:
pixbuf=raw_pixbuf.scale_simple(size,size/ratio,GdkPixbuf.InterpType.BILINEAR)
else:
pixbuf=raw_pixbuf.scale_simple(size*ratio,size,GdkPixbuf.InterpType.BILINEAR)
return pixbuf
class FileCover(str):
def get_pixbuf(self, size):
def get_pixbuf(self, size=-1):
try:
pixbuf=GdkPixbuf.Pixbuf.new_from_file_at_size(self, size, size)
except gi.repository.GLib.Error: # load fallback if cover can't be loaded
@@ -704,7 +706,6 @@ class Client(MPDClient):
return [Song(song) for song in super().listplaylistinfo(name)]
def start(self):
self.emitter.emit("disconnected") # bring player in defined state
profile=self._settings.get_active_profile()
if profile.get_boolean("socket-connection"):
socket=profile.get_string("socket")
@@ -718,6 +719,7 @@ class Client(MPDClient):
if profile.get_string("password"):
self.password(profile.get_string("password"))
except:
self.emitter.emit("disconnected")
self.emitter.emit("connection_error")
return False
# connect successful
@@ -733,6 +735,7 @@ class Client(MPDClient):
return True
else:
self.disconnect()
self.emitter.emit("disconnected")
self.emitter.emit("connection_error")
print("No read permission, check your mpd config.")
return False
@@ -1065,7 +1068,6 @@ class ViewSettings(SettingsList):
row=ToggleRow(label, settings, key, restart_required)
self.append(row)
int_data=(
(_("Main cover size"), (100, 1200, 10), "track-cover"),
(_("Album view cover size"), (50, 600, 10), "album-cover"),
(_("Action bar icon size"), (16, 64, 2), "icon-size"),
)
@@ -1210,7 +1212,6 @@ class SettingsDialog(Gtk.Dialog):
else:
super().__init__(title=_("Preferences"), transient_for=parent)
self.add_button(Gtk.STOCK_OK, Gtk.ResponseType.OK)
self.set_default_size(500, 400)
# widgets
view=ViewSettings(settings)
@@ -1272,7 +1273,7 @@ class ServerStats(Gtk.Dialog):
stats["protocol"]=str(client.mpd_version)
for key in ("uptime","playtime","db_playtime"):
stats[key]=str(Duration(stats[key]))
stats["db_update"]=str(datetime.datetime.fromtimestamp(int(stats["db_update"]))).replace(":", "")
stats["db_update"]=GLib.DateTime.new_from_unix_local(int(stats["db_update"])).format("%a %d %B %Y, %H%M")
for i, key in enumerate(("protocol","uptime","playtime","db_update","db_playtime","artists","albums","songs")):
grid.attach(Gtk.Label(label=display_str[key], use_markup=True, xalign=1), 0, i, 1, 1)
@@ -1300,6 +1301,17 @@ class TreeView(Gtk.TreeView):
rect.x,rect.y=self.convert_tree_to_widget_coords(rect.x,rect.y)
return (rect.x+rect.width//2, max(min(cell.y+cell.height//2, rect.y+rect.height), rect.y))
def save_set_cursor(self, *args, **kwargs):
# The standard set_cursor function should scroll normally, but it dosen't work as it should when the treeview is not completely
# initialized. This usually happens when the program is freshly started and the treeview isn't done with its internal tasks.
# See: https://lazka.github.io/pgi-docs/GLib-2.0/constants.html#GLib.PRIORITY_HIGH_IDLE
# Running set_cursor with a lower priority ensures that the treeview is done before it gets scrolled.
GLib.idle_add(self.set_cursor, *args, **kwargs)
def save_scroll_to_cell(self, *args, **kwargs):
# Similar problem as above.
GLib.idle_add(self.scroll_to_cell, *args, **kwargs)
class AutoSizedIcon(Gtk.Image):
def __init__(self, icon_name, settings_key, settings):
super().__init__(icon_name=icon_name)
@@ -1505,13 +1517,12 @@ class SongsList(TreeView):
self.emit("button-clicked")
def show_info(self):
if (treeiter:=self._selection.get_selected()[1]) is not None:
path=self._store.get_path(treeiter)
if (path:=self.get_cursor()[0]) is not None:
self._song_popover.open(self._store[path][3], self, *self.get_popover_point(path))
def add_to_playlist(self, mode):
if (treeiter:=self._selection.get_selected()[1]) is not None:
self._client.files_to_playlist([self._store.get_value(treeiter, 3)], mode)
if (path:=self.get_cursor()[0]) is not None:
self._client.files_to_playlist([self._store[path][3]], mode)
class AlbumPopover(Gtk.Popover):
def __init__(self, client, settings):
@@ -1554,7 +1565,7 @@ class AlbumPopover(Gtk.Popover):
self._songs_list.clear()
tag_filter=("albumartist", albumartist, "album", album, "date", date)
count=self._client.count(*tag_filter)
duration=str(Duration(float(count["playtime"])))
duration=str(Duration(count["playtime"]))
length=int(count["songs"])
text=ngettext("{number} song ({duration})", "{number} songs ({duration})", length).format(number=length, duration=duration)
self._label.set_text(text)
@@ -1780,35 +1791,36 @@ class SearchWindow(Gtk.Box):
class SelectionList(TreeView):
__gsignals__={"item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "clear": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, select_all_string):
super().__init__(activate_on_single_click=True, search_column=0, headers_visible=False, fixed_height_mode=True)
super().__init__(search_column=0, headers_visible=False, fixed_height_mode=True)
self.select_all_string=select_all_string
self._selected_path=None
# store
# item, weight, initial-letter, weight-initials
self._store=Gtk.ListStore(str, Pango.Weight, str, Pango.Weight)
self._store.append([self.select_all_string, Pango.Weight.NORMAL, "", Pango.Weight.NORMAL])
# item, initial-letter, weight-initials
self._store=Gtk.ListStore(str, str, Pango.Weight)
self._store.append([self.select_all_string, "", Pango.Weight.NORMAL])
self.set_model(self._store)
self._selection=self.get_selection()
self._selection.set_mode(Gtk.SelectionMode.BROWSE)
# columns
renderer_text_malign=Gtk.CellRendererText(xalign=0.5)
self._column_initial=Gtk.TreeViewColumn("", renderer_text_malign, text=2, weight=3)
self._column_initial=Gtk.TreeViewColumn("", renderer_text_malign, text=1, weight=2)
self._column_initial.set_property("sizing", Gtk.TreeViewColumnSizing.FIXED)
self._column_initial.set_property("min-width", 30)
self.append_column(self._column_initial)
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True, ypad=6)
self._column_item=Gtk.TreeViewColumn("", renderer_text, text=0, weight=1)
self._column_item=Gtk.TreeViewColumn("", renderer_text, text=0)
self._column_item.set_property("sizing", Gtk.TreeViewColumnSizing.FIXED)
self._column_item.set_property("expand", True)
self.append_column(self._column_item)
# connect
self.connect("row-activated", self._on_row_activated)
self._selection.connect("changed", self._on_selection_changed)
def clear(self):
self._store.clear()
self._store.append([self.select_all_string, Pango.Weight.NORMAL, "", Pango.Weight.NORMAL])
self._store.append([self.select_all_string, "", Pango.Weight.NORMAL])
self._selected_path=None
self.emit("clear")
@@ -1822,7 +1834,7 @@ class SelectionList(TreeView):
if item[0] is None:
char=item[1]
else:
self._store.insert_with_valuesv(-1, range(4), [item[0], Pango.Weight.NORMAL, char, Pango.Weight.BOLD])
self._store.insert_with_valuesv(-1, range(3), [item[0], char, Pango.Weight.BOLD])
char=""
def get_item_at_path(self, path):
@@ -1835,8 +1847,7 @@ class SelectionList(TreeView):
return len(self._store)-1
def select_path(self, path):
self.set_cursor(path, None, False)
self.row_activated(path, self._column_item)
self._selection.select_path(path)
def select(self, item):
row_num=len(self._store)
@@ -1847,8 +1858,7 @@ class SelectionList(TreeView):
break
def select_all(self):
self.set_cursor(Gtk.TreePath(0), None, False)
self.row_activated(Gtk.TreePath(0), self._column_item)
self.select_path(Gtk.TreePath(0))
def get_path_selected(self):
if self._selected_path is None:
@@ -1859,16 +1869,15 @@ class SelectionList(TreeView):
def get_item_selected(self):
return self.get_item_at_path(self.get_path_selected())
def highlight_selected(self):
self.set_cursor(self._selected_path, None, False)
def scroll_to_selected(self):
self.set_cursor(Gtk.TreePath(len(self._store)), None, False) # unset cursor
self.save_scroll_to_cell(self._selected_path, None, True, 0.25)
def _on_row_activated(self, widget, path, view_column):
if path != self._selected_path:
if self._selected_path is not None:
self._store[self._selected_path][1]=Pango.Weight.NORMAL
self._store[path][1]=Pango.Weight.BOLD
self._selected_path=path
self.emit("item-selected")
def _on_selection_changed(self, *args):
if (treeiter:=self._selection.get_selected()[1]) is not None:
if (path:=self._store.get_path(treeiter)) != self._selected_path:
self._selected_path=path
self.emit("item-selected")
class GenreList(SelectionList):
def __init__(self, client):
@@ -1880,9 +1889,6 @@ class GenreList(SelectionList):
self._client.emitter.connect_after("reconnected", self._on_reconnected)
self._client.emitter.connect("updated_db", self._refresh)
def deactivate(self):
self.select_all()
def _refresh(self, *args):
l=self._client.comp_list("genre")
self.set_items(list(zip(l,l)))
@@ -1938,14 +1944,13 @@ class ArtistList(SelectionList):
self.select_path(Gtk.TreePath(1))
else:
self.select_path(Gtk.TreePath(0))
self.scroll_to_selected()
def _on_button_press_event(self, widget, event):
if (path_re:=widget.get_path_at_pos(int(event.x), int(event.y))) is not None:
path=path_re[0]
artist,genre=self.get_artist_at_path(path)
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
self._client.artist_to_playlist(artist, genre, "play")
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
self._client.artist_to_playlist(artist, genre, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
self._artist_popover.open(artist, genre, self, event.x, event.y)
@@ -1959,15 +1964,12 @@ class ArtistList(SelectionList):
return self.get_artist_at_path(self.get_path_selected())
def add_to_playlist(self, mode):
selected_rows=self._selection.get_selected_rows()
if selected_rows is not None:
path=selected_rows[1][0]
if (path:=self.get_cursor()[0]) is not None:
artist,genre=self.get_artist_at_path(path)
self._client.artist_to_playlist(artist, genre, mode)
def show_info(self):
if (treeiter:=self._selection.get_selected()[1]) is not None:
path=self._store.get_path(treeiter)
if (path:=self.get_cursor()[0]) is not None:
artist,genre=self.get_artist_at_path(path)
self._artist_popover.open(artist, genre, self, *self.get_popover_point(path))
@@ -2053,6 +2055,13 @@ class AlbumLoadingThread(threading.Thread):
else:
main_thread_function(self._store.set_sort_column_id)(6, Gtk.SortType.ASCENDING)
idle_add(self._iconview.set_model, self._store)
# select album
path=main_thread_function(self._iconview.get_current_album_path)()
if path is None:
path=Gtk.TreePath(0)
idle_add(self._iconview.set_cursor, path, None, False)
idle_add(self._iconview.select_path, path)
idle_add(self._iconview.scroll_to_path, path, True, 0.25, 0)
# load covers
total=2*len(self._store)
@main_thread_function
@@ -2102,7 +2111,7 @@ class AlbumLoadingThread(threading.Thread):
class AlbumList(Gtk.IconView):
def __init__(self, client, settings, artist_list):
super().__init__(item_width=0, pixbuf_column=0, markup_column=1, activate_on_single_click=True)
super().__init__(item_width=0,pixbuf_column=0,markup_column=1,activate_on_single_click=True,selection_mode=Gtk.SelectionMode.BROWSE)
self._settings=settings
self._client=client
self._artist_list=artist_list
@@ -2148,19 +2157,24 @@ class AlbumList(Gtk.IconView):
else:
callback()
def scroll_to_current_album(self):
def callback():
song=self._client.currentsong()
album=song["album"][0]
self.unselect_all()
def get_current_album_path(self):
if (song:=self._client.currentsong()):
album=[song["albumartist"][0], song["album"][0], song["date"][0]]
row_num=len(self._store)
for i in range(0, row_num):
path=Gtk.TreePath(i)
if self._store[path][4] == album:
self.set_cursor(path, None, False)
self.select_path(path)
self.scroll_to_path(path, True, 0, 0)
break
if self._store[path][3:6] == album:
return path
return None
else:
return None
def scroll_to_current_album(self):
def callback():
if (path:=self.get_current_album_path()) is not None:
self.set_cursor(path, None, False)
self.select_path(path)
self.scroll_to_path(path, True, 0.25, 0)
if self._cover_thread.is_alive():
self._cover_thread.set_callback(callback)
else:
@@ -2216,20 +2230,17 @@ class AlbumList(Gtk.IconView):
self.set_sensitive(True)
def show_info(self):
paths=self.get_selected_items()
if len(paths) > 0:
path=paths[0]
if (path:=self.get_cursor()[1]) is not None:
cell=self.get_cell_rect(path, None)[1]
rect=self.get_allocation()
x=max(min(rect.x+cell.width//2, rect.x+rect.width), rect.x)
x=max(min(cell.x+cell.width//2, rect.x+rect.width), rect.x)
y=max(min(cell.y+cell.height//2, rect.y+rect.height), rect.y)
tags=self._store[path][3:6]
self._album_popover.open(*tags, self, x, y)
def add_to_playlist(self, mode):
paths=self.get_selected_items()
if len(paths) != 0:
self._path_to_playlist(paths[0], mode)
if (path:=self.get_cursor()[1]) is not None:
self._path_to_playlist(path, mode)
def _on_cover_size_changed(self, *args):
if self._client.connected():
@@ -2264,25 +2275,23 @@ class Browser(Gtk.Paned):
self.pack1(genre_window, False, False)
self.pack2(self.paned1, True, False)
def back_to_current_album(self, force=False):
if (song:=self._client.currentsong()):
artist,genre=self._artist_list.get_artist_selected()
# deactivate genre filter to show all artists (if needed)
if song["genre"][0] != genre or force:
self._genre_list.deactivate()
# select artist
if artist is None and not force: # all artists selected
self._artist_list.highlight_selected()
else: # one artist selected
def back_to_current_album(self):
song=self._client.currentsong()
artist,genre=self._artist_list.get_artist_selected()
if genre is None or song["genre"][0] == genre:
if artist is not None and song["albumartist"][0] != artist:
self._artist_list.select(song["albumartist"][0])
self._album_list.scroll_to_current_album()
self._artist_list.scroll_to_selected()
else:
self._album_list.scroll_to_current_album()
else:
self._genre_list.deactivate()
self._genre_list.select_all()
self._genre_list.scroll_to_selected()
def _on_genre_filter_changed(self, settings, key):
if self._client.connected():
if not settings.get_boolean(key):
self._genre_list.deactivate()
self._genre_list.select_all()
############
# playlist #
@@ -2483,21 +2492,24 @@ class PlaylistsPopover(Gtk.Popover):
self._playlists_view.columns_autosize()
class PlaylistView(TreeView):
selected_path=GObject.Property(type=Gtk.TreePath, default=None) # currently marked song (bold text)
selected_path=GObject.Property(type=Gtk.TreePath, default=None) # currently marked song
def __init__(self, client, settings):
super().__init__(activate_on_single_click=True, reorderable=True, search_column=5, headers_visible=False)
self._client=client
self._settings=settings
self._playlist_version=None
self._inserted_path=None # needed for drag and drop
# selection
self._selection=self.get_selection()
self._selection.set_select_function(self._select_function)
# label
self.label=Gtk.Label(xalign=0, ellipsize=Pango.EllipsizeMode.END)
# store
# (track, title, human duration, file, duration, search, weight, weight_set)
self._store=Gtk.ListStore(str, str, str, str, float, str, Pango.Weight, bool)
# (track, title, human duration, file, duration, search)
self._store=Gtk.ListStore(str, str, str, str, float, str)
self.set_model(self._store)
# columns
@@ -2507,10 +2519,9 @@ class PlaylistView(TreeView):
renderer_text_ralign_tnum=Gtk.CellRendererText(xalign=1, attributes=attrs)
renderer_text_centered_tnum=Gtk.CellRendererText(xalign=0.5, attributes=attrs)
columns=(
Gtk.TreeViewColumn(_("No"), renderer_text_centered_tnum, text=0, weight=6),
# the order of weight_set and weight seems to be important here
Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1, weight_set=7, weight=6),
Gtk.TreeViewColumn(_("Length"), renderer_text_ralign_tnum, text=2, weight=6)
Gtk.TreeViewColumn(_("No"), renderer_text_centered_tnum, text=0),
Gtk.TreeViewColumn(_("Title"), renderer_text, markup=1),
Gtk.TreeViewColumn(_("Length"), renderer_text_ralign_tnum, text=2)
)
for column in columns:
column.set_sizing(Gtk.TreeViewColumnSizing.AUTOSIZE)
@@ -2548,35 +2559,31 @@ class PlaylistView(TreeView):
def _select(self, path):
self._unselect()
try:
self._store[path][6]=Pango.Weight.BOLD
self._store[path][7]=True
self.set_property("selected-path", path)
self._selection.select_path(path)
except IndexError: # invalid path
pass
def _unselect(self):
if self.get_property("selected-path") is not None:
if (path:=self.get_property("selected-path")) is not None:
self.set_property("selected-path", None)
try:
self._store[self.get_property("selected-path")][6]=Pango.Weight.NORMAL
self._store[self.get_property("selected-path")][7]=False
self.set_property("selected-path", None)
self._selection.unselect_path(path)
except IndexError: # invalid path
self.set_property("selected-path", None)
pass
def scroll_to_selected_title(self):
if (treeiter:=self._selection.get_selected()[1]) is not None:
path=self._store.get_path(treeiter)
self.scroll_to_cell(path, None, True, 0.25)
self.save_scroll_to_cell(path, None, True, 0.25)
def _refresh_selection(self): # Gtk.TreePath(len(self._store) is used to generate an invalid TreePath (needed to unset cursor)
self.set_cursor(Gtk.TreePath(len(self._store)), None, False)
song=self._client.status().get("song")
if song is None:
self._selection.unselect_all()
self._unselect()
else:
path=Gtk.TreePath(int(song))
self._selection.select_path(path)
self._select(path)
def _set_playlist_info(self, text):
@@ -2593,9 +2600,9 @@ class PlaylistView(TreeView):
def _on_key_release_event(self, widget, event):
if event.keyval == Gdk.keyval_from_name("Delete"):
if (treeiter:=self._selection.get_selected()[1]) is not None:
if (path:=self.get_cursor()[0]) is not None:
try:
self._store.remove(treeiter)
self._store.remove(self._store.get_iter(path))
except:
pass
@@ -2642,23 +2649,21 @@ class PlaylistView(TreeView):
title=song.get_markup()
try:
treeiter=self._store.get_iter(song["pos"])
except ValueError:
self._store.insert_with_valuesv(-1, range(8), [
song["track"][0], title,
str(song["duration"]), song["file"],
float(song["duration"]), song["title"][0]
])
else:
self._store.set(treeiter,
0, song["track"][0],
1, title,
2, str(song["duration"]),
3, song["file"],
4, float(song["duration"]),
5, song["title"][0],
6, Pango.Weight.NORMAL,
7, False
5, song["title"][0]
)
except:
self._store.insert_with_valuesv(-1, range(8), [
song["track"][0], title,
str(song["duration"]), song["file"],
float(song["duration"]), song["title"][0],
Pango.Weight.NORMAL, False
])
self.thaw_child_notify()
for i in reversed(range(int(self._client.status()["playlistlength"]), len(self._store))):
treeiter=self._store.get_iter(i)
@@ -2689,22 +2694,22 @@ class PlaylistView(TreeView):
def _on_reconnected(self, *args):
self.set_sensitive(True)
def _select_function(self, selection, model, path, path_currently_selected):
return (path == self.get_property("selected-path")) == (not path_currently_selected)
def show_info(self):
if (treeiter:=self._selection.get_selected()[1]) is not None:
path=self._store.get_path(treeiter)
if (path:=self.get_cursor()[0]) is not None:
self._song_popover.open(self._store[path][3], self, *self.get_popover_point(path))
class PlaylistWindow(Gtk.Overlay):
def __init__(self, client, settings):
super().__init__()
self._back_to_current_song_button=Gtk.Button(
image=Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON), tooltip_text=_("Scroll to current song"),
can_focus=False
)
self._back_button_icon=Gtk.Image.new_from_icon_name("go-down-symbolic", Gtk.IconSize.BUTTON)
self._back_to_current_song_button=Gtk.Button(image=self._back_button_icon, tooltip_text=_("Scroll to current song"), can_focus=False)
self._back_to_current_song_button.get_style_context().add_class("osd")
self._back_button_revealer=Gtk.Revealer(
child=self._back_to_current_song_button, transition_duration=0,
margin_bottom=6, margin_start=6, halign=Gtk.Align.START, valign=Gtk.Align.END
margin_bottom=6, margin_top=6, halign=Gtk.Align.CENTER, valign=Gtk.Align.END
)
self._treeview=PlaylistView(client, settings)
scroll=Gtk.ScrolledWindow(child=self._treeview)
@@ -2728,14 +2733,23 @@ class PlaylistWindow(Gtk.Overlay):
if visible_range is None or self._treeview.get_property("selected-path") is None:
self._back_button_revealer.set_reveal_child(False)
else:
current_song_visible=(visible_range[0] <= self._treeview.get_property("selected-path") <= visible_range[1])
self._back_button_revealer.set_reveal_child(not(current_song_visible))
if visible_range[0] > self._treeview.get_property("selected-path"): # current song is above upper edge
self._back_button_icon.set_property("icon-name", "go-up-symbolic")
self._back_button_revealer.set_valign(Gtk.Align.START)
self._back_button_revealer.set_reveal_child(True)
elif self._treeview.get_property("selected-path") > visible_range[1]: # current song is below lower edge
self._back_button_icon.set_property("icon-name", "go-down-symbolic")
self._back_button_revealer.set_valign(Gtk.Align.END)
self._back_button_revealer.set_reveal_child(True)
else: # current song is visible
self._back_button_revealer.set_reveal_child(False)
def _on_back_to_current_song_button_clicked(self, *args):
self._treeview.set_cursor(Gtk.TreePath(len(self._treeview.get_model())), None, False) # unset cursor
if self._treeview.get_property("selected-path") is not None:
self._treeview.get_selection().select_path(self._treeview.get_property("selected-path"))
self._treeview.scroll_to_selected_title()
self._back_button_revealer.set_reveal_child(False) # workaround for Gtk bug in _on_show_hide_back_button
####################
# cover and lyrics #
@@ -2872,30 +2886,30 @@ class CoverEventBox(Gtk.EventBox):
def _on_disconnected(self, *args):
self._album_popover.popdown()
class MainCover(Gtk.Image):
class MainCover(Gtk.DrawingArea):
def __init__(self, client, settings):
super().__init__()
self._client=client
self._settings=settings
# set default size
size=self._settings.get_int("track-cover")
self.set_size_request(size, size)
self._fallback=True
# connect
self._client.emitter.connect("current_song", self._refresh)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("reconnected", self._on_reconnected)
self._settings.connect("changed::track-cover", self._on_settings_changed)
def _clear(self):
size=self._settings.get_int("track-cover")
self.set_from_pixbuf(GdkPixbuf.Pixbuf.new_from_file_at_size(FALLBACK_COVER, size, size))
self._fallback=True
self.queue_draw()
def _refresh(self, *args):
if self._client.current_cover is None:
self._clear()
else:
self.set_from_pixbuf(self._client.current_cover.get_pixbuf(self._settings.get_int("track-cover")))
self._pixbuf=self._client.current_cover.get_pixbuf()
self._surface=Gdk.cairo_surface_create_from_pixbuf(self._pixbuf, 0, None)
self._fallback=False
self.queue_draw()
def _on_disconnected(self, *args):
self.set_sensitive(False)
@@ -2904,10 +2918,19 @@ class MainCover(Gtk.Image):
def _on_reconnected(self, *args):
self.set_sensitive(True)
def _on_settings_changed(self, *args):
size=self._settings.get_int("track-cover")
self.set_size_request(size, size)
self._refresh()
def do_draw(self, context):
if self._fallback:
size=min(self.get_allocated_height(), self.get_allocated_width())
self._pixbuf=GdkPixbuf.Pixbuf.new_from_file_at_size(FALLBACK_COVER, size, size)
self._surface=Gdk.cairo_surface_create_from_pixbuf(self._pixbuf, 0, None)
scale_factor=1
else:
scale_factor=min(self.get_allocated_width()/self._pixbuf.get_width(), self.get_allocated_height()/self._pixbuf.get_height())
context.scale(scale_factor, scale_factor)
x=((self.get_allocated_width()/scale_factor)-self._pixbuf.get_width())/2
y=((self.get_allocated_height()/scale_factor)-self._pixbuf.get_height())/2
context.set_source_surface(self._surface, x, y)
context.paint()
class CoverLyricsWindow(Gtk.Overlay):
def __init__(self, client, settings):
@@ -2946,7 +2969,7 @@ class CoverLyricsWindow(Gtk.Overlay):
self._client.emitter.connect("reconnected", self._on_reconnected)
# packing
self.add(main_cover)
self.add(Gtk.AspectFrame(child=main_cover, shadow_type=Gtk.ShadowType.NONE))
self.add_overlay(self._stack)
self.add_overlay(self._lyrics_button_revealer)
@@ -3483,9 +3506,6 @@ class MainWindow(Gtk.ApplicationWindow):
def __init__(self, client, settings, **kwargs):
super().__init__(title=("mpdevil"), icon_name="org.mpdevil.mpdevil", **kwargs)
self.set_default_icon_name("org.mpdevil.mpdevil")
self.set_default_size(settings.get_int("width"), settings.get_int("height"))
if settings.get_boolean("maximize"):
self.maximize() # request maximize
self._client=client
self._settings=settings
self._use_csd=self._settings.get_boolean("use-csd")
@@ -3536,9 +3556,10 @@ class MainWindow(Gtk.ApplicationWindow):
self._search_button=Gtk.ToggleButton(
image=icon("system-search-symbolic"), tooltip_text=_("Search"), can_focus=False, no_show_all=True)
self._settings.bind("mini-player", self._search_button, "visible", Gio.SettingsBindFlags.INVERT_BOOLEAN|Gio.SettingsBindFlags.GET)
self._back_button=Gtk.Button(
image=icon("go-previous-symbolic"), tooltip_text=_("Back to current album"), can_focus=False, no_show_all=True)
self._settings.bind("mini-player", self._back_button, "visible", Gio.SettingsBindFlags.INVERT_BOOLEAN|Gio.SettingsBindFlags.GET)
back_button=Gtk.Button(
image=icon("go-previous-symbolic"), tooltip_text=_("Back to current album"),
action_name="win.back-to-current-album", can_focus=False, no_show_all=True)
self._settings.bind("mini-player", back_button, "visible", Gio.SettingsBindFlags.INVERT_BOOLEAN|Gio.SettingsBindFlags.GET)
# stack
self._stack=Gtk.Stack(transition_type=Gtk.StackTransitionType.CROSSFADE)
@@ -3579,8 +3600,6 @@ class MainWindow(Gtk.ApplicationWindow):
# connect
self._search_button.connect("toggled", self._on_search_button_toggled)
self._back_button.connect("clicked", self._on_back_button_clicked)
self._back_button.connect("button-press-event", self._on_back_button_press_event)
self._settings.connect_after("changed::mini-player", self._mini_player)
self._settings.connect_after("notify::cursor-watch", self._on_cursor_watch)
self._settings.connect("changed::playlist-right", self._on_playlist_pos_changed)
@@ -3601,11 +3620,11 @@ class MainWindow(Gtk.ApplicationWindow):
if self._use_csd:
self._header_bar=Gtk.HeaderBar(show_close_button=True)
self.set_titlebar(self._header_bar)
self._header_bar.pack_start(self._back_button)
self._header_bar.pack_start(back_button)
self._header_bar.pack_end(self._menu_button)
self._header_bar.pack_end(self._search_button)
else:
action_bar.pack_start(self._back_button)
action_bar.pack_start(back_button)
action_bar.pack_end(self._menu_button)
action_bar.pack_end(self._search_button)
action_bar.pack_start(playback_control)
@@ -3621,22 +3640,27 @@ class MainWindow(Gtk.ApplicationWindow):
overlay.add_overlay(update_notify)
overlay.add_overlay(connection_notify)
self.add(overlay)
# bring player in consistent state
self._client.emitter.emit("disconnected")
self._mini_player()
# indicate connection process in window title
if self._use_csd:
self._header_bar.set_subtitle(_("connecting…"))
else:
self.set_title("mpdevil "+_("connecting…"))
# set default window size
if self._settings.get_boolean("mini-player"):
self.set_default_size(settings.get_int("mini-player-width"), settings.get_int("mini-player-height"))
else:
self.set_default_size(settings.get_int("width"), settings.get_int("height"))
if settings.get_boolean("maximize"):
self.maximize() # request maximize
# show window
self.show_all()
while Gtk.events_pending(): # ensure window is visible
Gtk.main_iteration_do(True)
# restore paned settings when window is visible (fixes a bug when window is maximized)
self._settings.bind("paned0", self._paned0, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned1", self._browser.paned1, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned2", self._paned2, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned3", self._browser, "position", Gio.SettingsBindFlags.DEFAULT)
if not self._settings.get_boolean("mini-player"):
self._bind_paned_settings() # restore paned settings when window is visible (fixes a bug when window is maximized)
# start client
def callback(*args):
@@ -3644,20 +3668,42 @@ class MainWindow(Gtk.ApplicationWindow):
return False
idle_add(callback)
def _clear_title(self):
self.set_title("mpdevil")
if self._use_csd:
self._header_bar.set_subtitle("")
def _bind_paned_settings(self):
self._settings.bind("paned0", self._paned0, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned1", self._browser.paned1, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned2", self._paned2, "position", Gio.SettingsBindFlags.DEFAULT)
self._settings.bind("paned3", self._browser, "position", Gio.SettingsBindFlags.DEFAULT)
def _unbind_paned_settings(self):
self._settings.unbind(self._paned0, "position")
self._settings.unbind(self._browser.paned1, "position")
self._settings.unbind(self._paned2, "position")
self._settings.unbind(self._browser, "position")
def _mini_player(self, *args):
if self.is_maximized():
self.unmaximize()
if self._settings.get_boolean("mini-player"):
if self.is_maximized():
self.unmaximize()
self.resize(1,1)
self._unbind_paned_settings()
self.resize(self._settings.get_int("mini-player-width"), self._settings.get_int("mini-player-height"))
else:
self.resize(self._settings.get_int("width"), self._settings.get_int("height"))
self.show_all()
while Gtk.events_pending(): # ensure window is resized
Gtk.main_iteration_do(True)
self._bind_paned_settings()
self.show_all() # show hidden gui elements
def _on_toggle_lyrics(self, action, param):
self._cover_lyrics_window.lyrics_button.emit("clicked")
def _on_back_to_current_album(self, action, param):
self._back_button.emit("clicked")
self._search_button.set_active(False)
self._browser.back_to_current_album()
def _on_toggle_search(self, action, param):
self._search_button.emit("clicked")
@@ -3707,16 +3753,9 @@ class MainWindow(Gtk.ApplicationWindow):
else:
self._stack.set_visible_child_name("browser")
def _on_back_button_clicked(self, *args):
self._search_button.set_active(False)
self._browser.back_to_current_album()
def _on_back_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
self._browser.back_to_current_album(force=True)
def _on_song_changed(self, *args):
if (song:=self._client.currentsong()):
self.lookup_action("back-to-current-album").set_enabled(True)
album=song.get_album_with_date()
title="".join(filter(None, (song["title"][0], str(song["artist"]))))
if self._use_csd:
@@ -3737,32 +3776,32 @@ class MainWindow(Gtk.ApplicationWindow):
else:
self.get_application().withdraw_notification("title-change")
else:
self.set_title("mpdevil")
if self._use_csd:
self._header_bar.set_subtitle("")
self.lookup_action("back-to-current-album").set_enabled(False)
self._clear_title()
self.get_application().withdraw_notification("title-change")
def _on_reconnected(self, *args):
for action in ("stats","toggle-lyrics","back-to-current-album","toggle-search"):
self._clear_title()
for action in ("stats","toggle-lyrics","toggle-search"):
self.lookup_action(action).set_enabled(True)
self._search_button.set_sensitive(True)
self._back_button.set_sensitive(True)
def _on_disconnected(self, *args):
self.set_title("mpdevil")
if self._use_csd:
self._header_bar.set_subtitle("")
self._clear_title()
for action in ("stats","toggle-lyrics","back-to-current-album","toggle-search"):
self.lookup_action(action).set_enabled(False)
self._search_button.set_active(False)
self._search_button.set_sensitive(False)
self._back_button.set_sensitive(False)
def _on_size_allocate(self, widget, rect):
if not self.is_maximized() and not self._settings.get_boolean("mini-player"):
if not self.is_maximized():
if (size:=self.get_size()) != self._size: # prevent unneeded write operations
self._settings.set_int("width", size[0])
self._settings.set_int("height", size[1])
if self._settings.get_boolean("mini-player"):
self._settings.set_int("mini-player-width", size[0])
self._settings.set_int("mini-player-height", size[1])
else:
self._settings.set_int("width", size[0])
self._settings.set_int("height", size[1])
self._size=size
def _on_cursor_watch(self, obj, typestring):
@@ -3825,7 +3864,10 @@ class mpdevil(Gtk.Application):
Gtk.binding_entry_remove(Gtk.binding_set_find('GtkTreeView'), Gdk.keyval_from_name("space"), Gdk.ModifierType.MOD2_MASK)
def do_activate(self):
self._window.present()
try:
self._window.present()
except: # failed to show window so the user can't see anything
self.quit()
def do_shutdown(self):
Gtk.Application.do_shutdown(self)
@@ -3855,4 +3897,5 @@ class mpdevil(Gtk.Application):
if __name__ == "__main__":
app=mpdevil()
signal.signal(signal.SIGINT, signal.SIG_DFL) # allow using ctrl-c to terminate
app.run(sys.argv)