Compare commits

...

10 Commits

Author SHA1 Message Date
Martin Wagner f859a08a5f fixed maximize 2022-11-23 18:49:34 +01:00
Martin Wagner da14ac2ef6 don't unset cursor in SelectionList
When no cursor is set in SelectionList and the keyboard focus enters the widget a "item-reselected" is triggered.
2022-11-23 18:34:53 +01:00
Martin Wagner 1231e07f52 removed unneeded argument of MainCover 2022-11-23 18:23:53 +01:00
Martin Wagner 11e45a5d5d use browse mode in SongsList 2022-11-23 18:21:26 +01:00
Martin Wagner 3e4ca4a116 reworked browsing 2022-11-23 18:18:56 +01:00
Martin Wagner fab8604fd3 replaced AlbumPopover with AlbumView 2022-11-23 17:42:42 +01:00
Martin Wagner 0e556a39f1
Merge pull request #63 from grosmanal/french-translator-credits
add french translator credits
2022-11-20 11:13:28 +01:00
Emmanuel Averty cf79da1f88 add french translator credits 2022-11-20 10:24:24 +01:00
Martin Wagner 9683c62274
Merge pull request #62 from grosmanal/translation-french
add french translations
2022-11-19 23:12:46 +01:00
Emmanuel Averty 9a7f1e431e add french translations 2022-11-19 15:48:32 +01:00
7 changed files with 635 additions and 152 deletions

View File

@ -7,6 +7,7 @@ Translators:
Georgi Kamenov (Bulgarian)
Oğuz Ersen (Turkish)
Łukasz Drukała (Polish)
Emmanuel Averty (French)
Gentoo ebuild:
Martin Wagner <martin.wagner.dev@gmail.com>

View File

@ -73,4 +73,4 @@ sudo update-desktop-database
Translation
-----------
This program is currently available in English, German, Dutch, Bulgarian, Turkish and Polish. If you speak one of these or even another language, you can easily translate it by using [poedit](https://poedit.net). Just import `po/mpdevil.pot` from this repo into `poedit`. To test your translation, copy the new `.po` file into the `po` directory of your cloned mpdevil repo and proceed as described in the [Building](#building) section. To get your translation merged, just send me an e-mail or create a pull request.
This program is currently available in English, German, Dutch, Bulgarian, Turkish, Polish and French. If you speak one of these or even another language, you can easily translate it by using [poedit](https://poedit.net). Just import `po/mpdevil.pot` from this repo into `poedit`. To test your translation, copy the new `.po` file into the `po` directory of your cloned mpdevil repo and proceed as described in the [Building](#building) section. To get your translation merged, just send me an e-mail or create a pull request.

View File

@ -10,7 +10,8 @@
Martin de Reuver
Georgi Kamenov
Oğuz Ersen
Łukasz Drukała</property>
Łukasz Drukała
Emmanuel Averty</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

@ -110,10 +110,6 @@
<default>false</default>
<summary>Stop playback on quit</summary>
</key>
<key type="b" name="force-mode">
<default>false</default>
<summary>Play selected albums directly</summary>
</key>
<key type="b" name="mpris">
<default>true</default>
<summary>Provide MPRIS</summary>

View File

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

482
po/fr.po Normal file
View File

@ -0,0 +1,482 @@
# 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-11-06 11:32+0100\n"
"PO-Revision-Date: 2022-11-19 15:38+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: fr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"X-Generator: Poedit 3.1.1\n"
#: src/mpdevil.py:502
#, python-brace-format
msgid "{days} day"
msgid_plural "{days} days"
msgstr[0] "{days} jour"
msgstr[1] "{days} jours"
#: src/mpdevil.py:539
#, python-brace-format
msgid "{channels} channel"
msgid_plural "{channels} channels"
msgstr[0] "{channels} canal"
msgstr[1] "{channels} canaux"
#: src/mpdevil.py:1002
msgid "(restart required)"
msgstr "(redémarrage nécessaire)"
#: src/mpdevil.py:1048
msgid "Use Client-side decoration"
msgstr "Utiliser les décorations côté client"
#: src/mpdevil.py:1049
msgid "Show stop button"
msgstr "Afficher le bouton stop"
#: src/mpdevil.py:1050
msgid "Show audio format"
msgstr "Afficher le format audio"
#: src/mpdevil.py:1051
msgid "Show lyrics button"
msgstr "Afficher le bouton paroles"
#: src/mpdevil.py:1052
msgid "Place playlist at the side"
msgstr "Placer la liste de lecture sur le côté"
#: src/mpdevil.py:1058
msgid "Album view cover size"
msgstr "Taille de la vue couverture d'album"
#: src/mpdevil.py:1059
msgid "Action bar icon size"
msgstr "Taille de la barre des icônes d'action"
#: src/mpdevil.py:1069
msgid "Support “MPRIS”"
msgstr "Support du \"MPRIS\""
#: src/mpdevil.py:1070
msgid "Sort albums by year"
msgstr "Trier les albums par année"
#: src/mpdevil.py:1071
msgid "Send notification on title change"
msgstr "Notifier lors du changement de titre"
#: src/mpdevil.py:1072
msgid "Play selected albums and titles immediately"
msgstr "Jouer l'album sélectionné et les titres immédiatement"
#: src/mpdevil.py:1073
msgid "Rewind via previous button"
msgstr "Retour au début via bouton précédent"
#: src/mpdevil.py:1074
msgid "Stop playback on quit"
msgstr "Arrêt de la lecture en quittant"
#: src/mpdevil.py:1101
msgid "Choose directory"
msgstr "Choisir le répertoire"
#. labels and entries
#: src/mpdevil.py:1116
msgid "Connect via Unix domain socket"
msgstr "Connexion via socket Unix"
#: src/mpdevil.py:1135
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 ""
"La première image dans le même répertoire que le fichier de la chanson "
"correspondant à l'expression régulière sera affiché. %AlbumArtist% et "
"%Album% seront replacés par les étiquettes correspondantes de la chanson."
#: src/mpdevil.py:1140
msgid "Socket:"
msgstr "Socket :"
#: src/mpdevil.py:1142
msgid "Host:"
msgstr "Hôte :"
#: src/mpdevil.py:1144
msgid "Password:"
msgstr "Mot de passe :"
#: src/mpdevil.py:1145
msgid "Music lib:"
msgstr "Bibliothèque musicale :"
#: src/mpdevil.py:1147
msgid "Cover regex:"
msgstr "Regex des couvertures :"
#. connect button
#: src/mpdevil.py:1150 src/mpdevil.py:3139
msgid "Connect"
msgstr "Se connecter"
#: src/mpdevil.py:1173 src/mpdevil.py:1175 src/mpdevil.py:3140
#: src/mpdevil.py:3236
msgid "Preferences"
msgstr "Préférences"
#: src/mpdevil.py:1187 src/mpdevil.py:1197
msgid "View"
msgstr "Vue"
#: src/mpdevil.py:1188 src/mpdevil.py:1198
msgid "Behavior"
msgstr "Comportement"
#: src/mpdevil.py:1189 src/mpdevil.py:1199
msgid "Connection"
msgstr "Connexion"
#: src/mpdevil.py:1216
msgid "Stats"
msgstr "Statistiques"
#: src/mpdevil.py:1225
msgid "<b>Protocol:</b>"
msgstr "<b>Protocole :</b>"
#: src/mpdevil.py:1226
msgid "<b>Uptime:</b>"
msgstr "<b>Durée dactivité :</b>"
#: src/mpdevil.py:1227
msgid "<b>Playtime:</b>"
msgstr "<b>Durée de lecture :</b>"
#: src/mpdevil.py:1228
msgid "<b>Artists:</b>"
msgstr "<b>Artistes :</b>"
#: src/mpdevil.py:1229
msgid "<b>Albums:</b>"
msgstr "<b>Albums :</b>"
#: src/mpdevil.py:1230
msgid "<b>Songs:</b>"
msgstr "<b>Chansons :</b>"
#: src/mpdevil.py:1231
msgid "<b>Total Playtime:</b>"
msgstr "<b>Temps total de lecture :</b>"
#: src/mpdevil.py:1232
msgid "<b>Database Update:</b>"
msgstr "<b>Mise à jour de la base :</b>"
#: src/mpdevil.py:1292
msgid "Show in file manager"
msgstr "Afficher dans le gestionnaire de fichiers"
#: src/mpdevil.py:1296 data/ShortcutsWindow.ui:208
msgid "Append"
msgstr "Ajouter"
#: src/mpdevil.py:1296 src/mpdevil.py:2690 src/mpdevil.py:2723
msgid "Play"
msgstr "Jouer"
#: src/mpdevil.py:1312
msgid "MPD-Tag"
msgstr "Étiquette MPD"
#: src/mpdevil.py:1315
msgid "Value"
msgstr "Valeur"
#: src/mpdevil.py:1385 src/mpdevil.py:2212
msgid "No"
msgstr "Non"
#: src/mpdevil.py:1386 src/mpdevil.py:2213
msgid "Title"
msgstr "Titre"
#: src/mpdevil.py:1387 src/mpdevil.py:2214
msgid "Length"
msgstr "Longueur"
#: src/mpdevil.py:1400
msgid "Add all titles to playlist"
msgstr "Ajouter tous les titres à la liste de lecture"
#: src/mpdevil.py:1401
msgid "Directly play all titles"
msgstr "Jouer directement tous les titres"
#: src/mpdevil.py:1494
#, python-brace-format
msgid "{number} song ({duration})"
msgid_plural "{number} songs ({duration})"
msgstr[0] "{number} chanson ({duration})"
msgstr[1] "{number} chansons ({duration})"
#: src/mpdevil.py:1557
#, python-brace-format
msgid "{hits} hit"
msgid_plural "{hits} hits"
msgstr[0] "{hits} hit"
msgstr[1] "{hits} hits"
#: src/mpdevil.py:1642
msgid "all tags"
msgstr "toutes les étiquettes"
#: src/mpdevil.py:1774
msgid "all genres"
msgstr "tous les genres"
#: src/mpdevil.py:1797
msgid "all artists"
msgstr "tous les artistes"
#: src/mpdevil.py:2384
msgid "Scroll to current song"
msgstr "Défiler jusqu'à la chanson courante"
#: src/mpdevil.py:2502
msgid "searching…"
msgstr "recherche…"
#: src/mpdevil.py:2507
msgid "connection error"
msgstr "erreur de connexion"
#: src/mpdevil.py:2509
msgid "lyrics not found"
msgstr "paroles introuvables"
#: src/mpdevil.py:2634
msgid "Lyrics"
msgstr "Paroles"
#: src/mpdevil.py:2692 data/ShortcutsWindow.ui:105
msgid "Stop"
msgstr "Stop"
#: src/mpdevil.py:2696 data/ShortcutsWindow.ui:126
msgid "Previous title"
msgstr "Titre précédent"
#: src/mpdevil.py:2699 data/ShortcutsWindow.ui:119
msgid "Next title"
msgstr "Titre suivant"
#: src/mpdevil.py:2720
msgid "Pause"
msgstr "Pause"
#: src/mpdevil.py:2892
msgid "Repeat mode"
msgstr "Mode répétition"
#: src/mpdevil.py:2893
msgid "Random mode"
msgstr "Mode aléatoire"
#: src/mpdevil.py:2894
msgid "Single mode"
msgstr "Mode chanson unique"
#: src/mpdevil.py:2895
msgid "Consume mode"
msgstr "Mode consommer"
#: src/mpdevil.py:3109
msgid "Updating Database…"
msgstr "Mise à jour de la base…"
#: src/mpdevil.py:3157
#, python-brace-format
msgid "Connection to “{socket}” failed"
msgstr "La connexion à “{socket}” a échoué"
#: src/mpdevil.py:3159
#, python-brace-format
msgid "Connection to “{host}:{port}” failed"
msgstr "La connexion à “{host}:{port}” a échoué"
#: src/mpdevil.py:3220
msgid "Search"
msgstr "Rechercher"
#: src/mpdevil.py:3223 data/ShortcutsWindow.ui:85
msgid "Back to current album"
msgstr "Retour à l'album courant"
#: src/mpdevil.py:3237
msgid "Keyboard Shortcuts"
msgstr "Raccourcis clavier"
#: src/mpdevil.py:3238
msgid "Help"
msgstr "Aide"
#: src/mpdevil.py:3239
msgid "About mpdevil"
msgstr "À propos de mpdevil"
#: src/mpdevil.py:3241
msgid "Update Database"
msgstr "Mettre à jour la base"
#: src/mpdevil.py:3242
msgid "Server Stats"
msgstr "Statistiques serveur"
#: src/mpdevil.py:3244
msgid "Mini Player"
msgstr "Mini lecteur"
#: src/mpdevil.py:3245
msgid "Genre Filter"
msgstr "Filtre genre"
#: src/mpdevil.py:3254
msgid "Menu"
msgstr "Menu"
#: src/mpdevil.py:3438 src/mpdevil.py:3440
msgid "connecting…"
msgstr "connexion…"
#: src/mpdevil.py:3478
msgid "Debug mode"
msgstr "Mode debug"
#: data/org.mpdevil.mpdevil.desktop.in:3
msgid "mpdevil"
msgstr "mpdevil"
#: data/org.mpdevil.mpdevil.desktop.in:4
msgid "MPD Client"
msgstr "Client MPD"
#: data/org.mpdevil.mpdevil.desktop.in:5 data/AboutDialog.ui:7
msgid "A simple music browser for MPD"
msgstr "Un simple navigateur de musique pour MPD"
#: data/ShortcutsWindow.ui:12
msgid "General"
msgstr "Général"
#: data/ShortcutsWindow.ui:16
msgid "Open online help"
msgstr "Ouvrir l'aide en ligne"
#: data/ShortcutsWindow.ui:23
msgid "Open shortcuts window"
msgstr "Ouvrir la fenêtre des raccourcis"
#: data/ShortcutsWindow.ui:30
msgid "Open menu"
msgstr "Ouvrir le menu"
#: data/ShortcutsWindow.ui:37
msgid "Update database"
msgstr "Mettre à jour la base"
#: data/ShortcutsWindow.ui:44
msgid "Quit"
msgstr "Quitter"
#: data/ShortcutsWindow.ui:53
msgid "Window"
msgstr "Fenêtre"
#: data/ShortcutsWindow.ui:57
msgid "Toggle mini player"
msgstr "(Dés-)activer mini lecteur"
#: data/ShortcutsWindow.ui:64
msgid "Toggle genre filter"
msgstr "(Dés-)activer le filtre de genre"
#: data/ShortcutsWindow.ui:71
msgid "Toggle lyrics"
msgstr "(Dés-)activer les paroles"
#: data/ShortcutsWindow.ui:78
msgid "Toggle search"
msgstr "(Dés-)activer la recherche"
#: data/ShortcutsWindow.ui:94
msgid "Playback"
msgstr "Playback"
#: data/ShortcutsWindow.ui:98
msgid "Play/Pause"
msgstr "Jouer/Pause"
#: data/ShortcutsWindow.ui:112
msgid "Stop after current title"
msgstr "Arrêter après le titre courant"
#: data/ShortcutsWindow.ui:133
msgid "Seek forward"
msgstr "Atteindre vers l'avant"
#: data/ShortcutsWindow.ui:140
msgid "Seek backward"
msgstr "Atteindre vers l'arrière"
#: data/ShortcutsWindow.ui:147
msgid "Toggle repeat mode"
msgstr "(Dés-)activer le mode répétition"
#: data/ShortcutsWindow.ui:154
msgid "Toggle random mode"
msgstr "(Dés-)activer le mode aléatoire"
#: data/ShortcutsWindow.ui:161
msgid "Toggle single mode"
msgstr "(Dés-)activer le mode titre unique"
#: data/ShortcutsWindow.ui:168
msgid "Toggle consume mode"
msgstr "(Dés-)activer le mode consommer"
#: data/ShortcutsWindow.ui:177
msgid "Playlist"
msgstr "Liste de lecture"
#: data/ShortcutsWindow.ui:181
msgid "Remove song"
msgstr "Supprimer la chanson"
#: data/ShortcutsWindow.ui:188
msgid "Clear playlist"
msgstr "Effacer la liste de lecture"
#: data/ShortcutsWindow.ui:195 data/ShortcutsWindow.ui:222
msgid "Show information"
msgstr "Afficher les informations"
#: data/ShortcutsWindow.ui:204
msgid "Search, Album Dialog and Album List"
msgstr "Recherche, Boîte de dialogue album et Liste dalbum"
#: data/ShortcutsWindow.ui:215
msgid "Play immediately"
msgstr "Jouer immédiatement"

View File

@ -763,12 +763,7 @@ class Client(MPDClient):
except:
return False
def _to_playlist(self, append, mode="default"): # modes: default, play, append, enqueue
if mode == "default":
if self._settings.get_boolean("force-mode"):
mode="play"
else:
mode="enqueue"
def _to_playlist(self, append, mode): # modes: play, append, enqueue
if mode == "append":
append()
elif mode == "play":
@ -793,13 +788,13 @@ class Client(MPDClient):
self.move(0, duplicates[1]["pos"])
self.delete(int(duplicates[1]["pos"])-1)
def files_to_playlist(self, files, mode="default"):
def files_to_playlist(self, files, mode):
def append():
for f in files:
self.add(f)
self._to_playlist(append, mode)
def filter_to_playlist(self, tag_filter, mode="default"):
def filter_to_playlist(self, tag_filter, mode):
def append():
if tag_filter:
self.findadd(*tag_filter)
@ -807,7 +802,7 @@ class Client(MPDClient):
self.searchadd("any", "")
self._to_playlist(append, mode)
def album_to_playlist(self, albumartist, album, date, mode="default"):
def album_to_playlist(self, albumartist, album, date, mode):
self.filter_to_playlist(("albumartist", albumartist, "album", album, "date", date), mode)
def comp_list(self, *args): # simulates listing behavior of python-mpd2 1.0
@ -1069,7 +1064,6 @@ class BehaviorSettings(SettingsList):
(_("Support “MPRIS”"), "mpris", True),
(_("Sort albums by year"), "sort-albums-by-year", False),
(_("Send notification on title change"), "send-notify", False),
(_("Play selected albums and titles immediately"), "force-mode", False),
(_("Rewind via previous button"), "rewind-mode", False),
(_("Stop playback on quit"), "stop-on-quit", False),
)
@ -1365,18 +1359,17 @@ class SongPopover(Gtk.Popover):
self.popdown()
class SongsList(TreeView):
__gsignals__={"button-clicked": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, client, width=-1):
super().__init__(enable_search=False, activate_on_single_click=True, headers_visible=False)
def __init__(self, client):
super().__init__(activate_on_single_click=True, headers_visible=False, enable_search=False, search_column=4)
self._client=client
# store
# (track, title, duration, file)
self._store=Gtk.ListStore(str, str, str, str)
# (track, title, duration, file, search string)
self._store=Gtk.ListStore(str, str, str, str, str)
self.set_model(self._store)
# columns
renderer_text=Gtk.CellRendererText(width_chars=width, ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
renderer_text=Gtk.CellRendererText(ellipsize=Pango.EllipsizeMode.END, ellipsize_set=True)
attrs=Pango.AttrList()
attrs.insert(Pango.AttrFontFeatures.new("tnum 1"))
renderer_text_ralign_tnum=Gtk.CellRendererText(xalign=1, attributes=attrs, ypad=6)
@ -1394,6 +1387,7 @@ class SongsList(TreeView):
# selection
self._selection=self.get_selection()
self._selection.set_mode(Gtk.SelectionMode.BROWSE)
# buttons
self.buttons=Gtk.ButtonBox(layout_style=Gtk.ButtonBoxStyle.EXPAND)
@ -1417,11 +1411,11 @@ class SongsList(TreeView):
self._song_popover.popdown()
self._store.clear()
def append(self, track, title, duration, file):
self._store.insert_with_valuesv(-1, range(4), [track, title, duration, file])
def append(self, track, title, duration, file, search_string=""):
self._store.insert_with_valuesv(-1, range(5), [track, title, duration, file, search_string])
def _on_row_activated(self, widget, path, view_column):
self._client.files_to_playlist([self._store[path][3]])
self._client.files_to_playlist([self._store[path][3]], "play")
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:
@ -1437,7 +1431,6 @@ class SongsList(TreeView):
def _on_button_clicked(self, widget, mode):
self._client.files_to_playlist((row[3] for row in self._store), mode)
self.emit("button-clicked")
def show_info(self):
if (path:=self.get_cursor()[0]) is not None:
@ -1447,71 +1440,6 @@ class SongsList(TreeView):
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):
super().__init__(position=Gtk.PositionType.BOTTOM)
self._client=client
self._settings=settings
self._rect=Gdk.Rectangle()
# songs list
# sizing needed for correct popover height
self._songs_list=SongsList(self._client, width=60)
# scroll
self._scroll=Gtk.ScrolledWindow(child=self._songs_list, propagate_natural_height=True)
self._scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
# label
self._label=Gtk.Label()
# connect
self._songs_list.connect("button-clicked", lambda *args: self.popdown())
# packing
vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=6, spacing=6)
hbox=Gtk.Box(spacing=6)
hbox.pack_end(self._songs_list.buttons, False, False, 0)
hbox.pack_start(self._label, False, False, 6)
frame=Gtk.Frame(child=self._scroll)
vbox.pack_start(frame, True, True, 0)
vbox.pack_end(hbox, False, False, 0)
self.add(vbox)
vbox.show_all()
def open(self, albumartist, album, date, widget, x, y):
self._rect.x=x
self._rect.y=y
self.set_pointing_to(self._rect)
self.set_relative_to(widget)
window=self.get_toplevel()
self._scroll.set_max_content_height(window.get_size()[1]//2)
self._songs_list.clear()
tag_filter=("albumartist", albumartist, "album", album, "date", date)
count=self._client.count(*tag_filter)
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)
self._client.restrict_tagtypes("track", "title", "artist")
songs=self._client.find(*tag_filter)
self._client.tagtypes("all")
for song in songs:
# only show artists =/= albumartist
try:
song["artist"].remove(albumartist)
except ValueError:
pass
artist=str(song['artist'])
if artist == albumartist or not artist:
title_artist=f"<b>{GLib.markup_escape_text(song['title'][0])}</b>"
else:
title_artist=f"<b>{GLib.markup_escape_text(song['title'][0])}</b> • {GLib.markup_escape_text(artist)}"
self._songs_list.append(song["track"][0], title_artist, str(song["duration"]), song["file"])
self._songs_list.scroll_to_cell(Gtk.TreePath(0), None, False) # clear old scroll position
self.popup()
self._songs_list.columns_autosize()
##########
# search #
##########
@ -1677,7 +1605,9 @@ class SearchWindow(Gtk.Box):
###########
class SelectionList(TreeView):
__gsignals__={"item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()), "clear": (GObject.SignalFlags.RUN_FIRST, None, ())}
__gsignals__={"item-selected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"item-reselected": (GObject.SignalFlags.RUN_FIRST, None, ()),
"clear": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, select_all_string):
super().__init__(search_column=0, headers_visible=False, fixed_height_mode=True)
self.select_all_string=select_all_string
@ -1737,7 +1667,7 @@ class SelectionList(TreeView):
return len(self._store)-1
def select_path(self, path):
self._selection.select_path(path)
self.set_cursor(path)
def select(self, item):
row_num=len(self._store)
@ -1760,12 +1690,13 @@ class SelectionList(TreeView):
return self.get_item_at_path(self.get_path_selected())
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_selection_changed(self, *args):
if (treeiter:=self._selection.get_selected()[1]) is not None:
if (path:=self._store.get_path(treeiter)) != self._selected_path:
if (path:=self._store.get_path(treeiter)) == self._selected_path:
self.emit("item-reselected")
else:
self._selected_path=path
self.emit("item-selected")
@ -1997,6 +1928,7 @@ class AlbumLoadingThread(threading.Thread):
idle_add(callback)
class AlbumList(Gtk.IconView):
__gsignals__={"album-selected": (GObject.SignalFlags.RUN_FIRST, None, (str,str,str,))}
def __init__(self, client, settings, artist_list):
super().__init__(item_width=0,pixbuf_column=0,markup_column=1,activate_on_single_click=True,selection_mode=Gtk.SelectionMode.BROWSE)
self._settings=settings
@ -2012,15 +1944,11 @@ class AlbumList(Gtk.IconView):
self.progress_bar=Gtk.ProgressBar(no_show_all=True, valign=Gtk.Align.END, vexpand=False)
self.progress_bar.get_style_context().add_class("osd")
# popover
self._album_popover=AlbumPopover(self._client, self._settings)
# cover thread
self._cover_thread=AlbumLoadingThread(self._client, self._settings, self.progress_bar, self, self._store, None, None)
# connect
self.connect("item-activated", self._on_item_activated)
self.connect("button-press-event", self._on_button_press_event)
self._client.emitter.connect("disconnected", self._on_disconnected)
self._client.emitter.connect("connected", self._on_connected)
self._settings.connect("changed::sort-albums-by-year", self._sort_settings)
@ -2036,7 +1964,6 @@ class AlbumList(Gtk.IconView):
def _clear(self, *args):
def callback():
self._album_popover.popdown()
self._workaround_clear()
if self._cover_thread.is_alive():
self._cover_thread.set_callback(callback)
@ -2087,28 +2014,13 @@ class AlbumList(Gtk.IconView):
else:
callback()
def _path_to_playlist(self, path, mode="default"):
def _path_to_playlist(self, path, mode):
tags=self._store[path][3:6]
self._client.album_to_playlist(*tags, mode)
def _on_button_press_event(self, widget, event):
path=widget.get_path_at_pos(int(event.x), int(event.y))
if event.button == 1 and event.type == Gdk.EventType._2BUTTON_PRESS:
if path is not None:
self._path_to_playlist(path, "play")
elif event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
if path is not None:
self._path_to_playlist(path, "append")
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
v=self.get_vadjustment().get_value()
h=self.get_hadjustment().get_value()
if path is not None:
tags=self._store[path][3:6]
# when using "button-press-event" in iconview popovers only show up in combination with idle_add (bug in GTK?)
idle_add(self._album_popover.open, *tags, widget, event.x-h, event.y-v)
def _on_item_activated(self, widget, path):
self._path_to_playlist(path)
tags=self._store[path][3:6]
self.emit("album-selected", *tags)
def _on_disconnected(self, *args):
self.set_sensitive(False)
@ -2116,23 +2028,97 @@ class AlbumList(Gtk.IconView):
def _on_connected(self, *args):
self.set_sensitive(True)
def show_info(self):
if (path:=self.get_cursor()[1]) is not None:
cell=self.get_cell_rect(path, None)[1]
rect=self.get_allocation()
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):
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():
self._refresh()
class AlbumView(Gtk.Box):
__gsignals__={"close": (GObject.SignalFlags.RUN_FIRST, None, ())}
def __init__(self, client, settings):
super().__init__(orientation=Gtk.Orientation.VERTICAL)
self._client=client
self._settings=settings
# songs list
self.songs_list=SongsList(self._client)
self.songs_list.set_enable_search(True)
self.songs_list.buttons.set_halign(Gtk.Align.END)
scroll=Gtk.ScrolledWindow(child=self.songs_list)
scroll.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
# cover
self._cover=Gtk.Image()
size=self._settings.get_int("album-cover")*1.5
pixbuf=GdkPixbuf.Pixbuf.new_from_file_at_size(FALLBACK_COVER, size, size)
self._cover.set_from_pixbuf(pixbuf)
# labels
self._title=Gtk.Label(margin_start=12, margin_end=12, xalign=0)
self._title.set_line_wrap(True) # wrap=True is not working
self._duration=Gtk.Label(xalign=1, ellipsize=Pango.EllipsizeMode.END)
# close button
close_button=Gtk.Button(image=Gtk.Image.new_from_icon_name("go-previous-symbolic", Gtk.IconSize.BUTTON), halign=Gtk.Align.START)
close_button.set_focus_on_click(False)
# connect
self.connect("hide", lambda *args: print("test"))
close_button.connect("clicked", lambda *args: self.emit("close"))
# packing
hbox=Gtk.Box(spacing=12)
hbox.pack_end(self.songs_list.buttons, False, False, 0)
hbox.pack_end(self._duration, False, False, 0)
vbox=Gtk.Box(orientation=Gtk.Orientation.VERTICAL, border_width=6)
vbox.pack_start(close_button, False, False, 0)
vbox.set_center_widget(self._title)
vbox.pack_end(hbox, False, False, 0)
header=Gtk.Box()
header.pack_start(self._cover, False, False, 0)
header.pack_start(Gtk.Separator(), False, False, 0)
header.pack_start(vbox, True, True, 0)
self.pack_start(header, False, False, 0)
self.pack_start(Gtk.Separator(), False, False, 0)
self.pack_start(scroll, True, True, 0)
def display(self, albumartist, album, date):
if date:
self._title.set_markup(f"<b>{GLib.markup_escape_text(album)}</b> ({GLib.markup_escape_text(date)})\n"
f"{GLib.markup_escape_text(albumartist)}")
else:
self._title.set_markup(f"<b>{GLib.markup_escape_text(album)}</b>\n{GLib.markup_escape_text(albumartist)}")
self.songs_list.clear()
tag_filter=("albumartist", albumartist, "album", album, "date", date)
count=self._client.count(*tag_filter)
duration=str(Duration(count["playtime"]))
length=int(count["songs"])
text=ngettext("{number} song ({duration})", "{number} songs ({duration})", length).format(number=length, duration=duration)
self._duration.set_text(text)
self._client.restrict_tagtypes("track", "title", "artist")
songs=self._client.find(*tag_filter)
self._client.tagtypes("all")
for song in songs:
# only show artists =/= albumartist
try:
song["artist"].remove(albumartist)
except ValueError:
pass
artist=str(song['artist'])
if artist == albumartist or not artist:
title_artist=f"<b>{GLib.markup_escape_text(song['title'][0])}</b>"
else:
title_artist=f"<b>{GLib.markup_escape_text(song['title'][0])}</b> • {GLib.markup_escape_text(artist)}"
self.songs_list.append(song["track"][0], title_artist, str(song["duration"]), song["file"], song["title"][0])
self.songs_list.save_set_cursor(Gtk.TreePath(0))
self.songs_list.columns_autosize()
if (cover:=self._client.get_cover({"file": songs[0]["file"], "albumartist": albumartist, "album": album})) is None:
size=self._settings.get_int("album-cover")*1.5
pixbuf=GdkPixbuf.Pixbuf.new_from_file_at_size(FALLBACK_COVER, size, size)
self._cover.set_from_pixbuf(pixbuf)
else:
size=self._settings.get_int("album-cover")*1.5
self._cover.set_from_pixbuf(cover.get_pixbuf(size))
class Browser(Gtk.Paned):
def __init__(self, client, settings):
super().__init__()
@ -2146,6 +2132,16 @@ class Browser(Gtk.Paned):
genre_window=Gtk.ScrolledWindow(child=self._genre_list)
artist_window=Gtk.ScrolledWindow(child=self._artist_list)
album_window=Gtk.ScrolledWindow(child=self._album_list)
self._album_view=AlbumView(self._client, self._settings)
# album overlay
album_overlay=Gtk.Overlay(child=album_window)
album_overlay.add_overlay(self._album_list.progress_bar)
# album stack
self._album_stack=Gtk.Stack(transition_type=Gtk.StackTransitionType.SLIDE_LEFT_RIGHT, homogeneous=False)
self._album_stack.add_named(album_overlay, "album_list")
self._album_stack.add_named(self._album_view, "album_view")
# hide/show genre filter
self._genre_list.set_property("visible", True)
@ -2153,16 +2149,23 @@ class Browser(Gtk.Paned):
self._settings.bind("genre-filter", genre_window, "visible", Gio.SettingsBindFlags.GET)
self._settings.connect("changed::genre-filter", self._on_genre_filter_changed)
# connect
self._album_list.connect("album-selected", self._on_album_list_show_info)
self._album_view.connect("close", lambda *args: self._album_stack.set_visible_child_name("album_list"))
self._artist_list.connect("item-selected", lambda *args: self._album_stack.set_visible_child_name("album_list"))
self._artist_list.connect("item-reselected", lambda *args: self._album_stack.set_visible_child_name("album_list"))
self._client.emitter.connect("disconnected", lambda *args: self._album_stack.set_visible_child_name("album_list"))
self._settings.connect("changed::album-cover", lambda *args: self._album_stack.set_visible_child_name("album_list"))
# packing
album_overlay=Gtk.Overlay(child=album_window)
album_overlay.add_overlay(self._album_list.progress_bar)
self.paned1=Gtk.Paned()
self.paned1.pack1(artist_window, False, False)
self.paned1.pack2(album_overlay, True, False)
self.paned1.pack2(self._album_stack, True, False)
self.pack1(genre_window, False, False)
self.pack2(self.paned1, True, False)
def back_to_current_album(self):
self._album_stack.set_visible_child_name("album_list")
song=self._client.currentsong()
artist,genre=self._artist_list.get_artist_selected()
if genre is None or song["genre"][0] == genre:
@ -2180,6 +2183,11 @@ class Browser(Gtk.Paned):
if not settings.get_boolean(key):
self._genre_list.select_all()
def _on_album_list_show_info(self, widget, *tags):
self._album_view.display(*tags)
self._album_stack.set_visible_child_name("album_view")
GLib.idle_add(self._album_view.songs_list.grab_focus)
############
# playlist #
############
@ -2261,6 +2269,12 @@ class PlaylistView(TreeView):
except IndexError: # invalid path
pass
def _delete(self, path):
if path == self.get_property("selected-path"):
self._client.files_to_playlist([self._store[path][3]], "enqueue")
else:
self._store.remove(self._store.get_iter(path))
def _scroll_to_path(self, path):
self.save_scroll_to_cell(path, None, True, 0.25)
@ -2281,7 +2295,7 @@ class PlaylistView(TreeView):
if (path_re:=widget.get_path_at_pos(int(event.x), int(event.y))) is not None:
path=path_re[0]
if event.button == 2 and event.type == Gdk.EventType.BUTTON_PRESS:
self._store.remove(self._store.get_iter(path))
self._delete(path)
elif event.button == 3 and event.type == Gdk.EventType.BUTTON_PRESS:
point=self.convert_bin_window_to_widget_coords(event.x,event.y)
self._song_popover.open(self._store[path][3], widget, *point)
@ -2290,7 +2304,7 @@ class PlaylistView(TreeView):
if event.keyval == Gdk.keyval_from_name("Delete"):
if (path:=self.get_cursor()[0]) is not None:
try:
self._store.remove(self._store.get_iter(path))
self._delete(path)
except:
pass
@ -2533,15 +2547,10 @@ class CoverEventBox(Gtk.EventBox):
self._settings=settings
self._click_pos=()
self.set_events(Gdk.EventMask.POINTER_MOTION_MASK)
# album popover
self._album_popover=AlbumPopover(self._client, self._settings)
# connect
self.connect("button-press-event", self._on_button_press_event)
self.connect("button-release-event", self._on_button_release_event)
self.connect("motion-notify-event", self._on_motion_notify_event)
self._client.emitter.connect("disconnected", self._on_disconnected)
def _on_button_press_event(self, widget, event):
if event.button == 1 and event.type == Gdk.EventType.BUTTON_PRESS:
@ -2553,9 +2562,7 @@ class CoverEventBox(Gtk.EventBox):
if (song:=self._client.currentsong()):
tags=(song["albumartist"][0], song["album"][0], song["date"][0])
if event.button == 1:
self._client.album_to_playlist(*tags)
elif event.button == 3:
self._album_popover.open(*tags, widget, event.x, event.y)
self._client.album_to_playlist(*tags, "enqueue")
self._click_pos=()
def _on_motion_notify_event(self, widget, event):
@ -2569,14 +2576,10 @@ class CoverEventBox(Gtk.EventBox):
window.begin_move_drag(1, event.x_root, event.y_root, Gdk.CURRENT_TIME)
self._click_pos=()
def _on_disconnected(self, *args):
self._album_popover.popdown()
class MainCover(Gtk.DrawingArea):
def __init__(self, client, settings):
def __init__(self, client):
super().__init__()
self._client=client
self._settings=settings
self._fallback=True
# connect
@ -2625,7 +2628,7 @@ class CoverLyricsWindow(Gtk.Overlay):
self._settings=settings
# cover
main_cover=MainCover(self._client, self._settings)
main_cover=MainCover(self._client)
self._cover_event_box=CoverEventBox(self._client, self._settings)
self._cover_event_box.add(Gtk.AspectFrame(child=main_cover, shadow_type=Gtk.ShadowType.NONE))
@ -3267,7 +3270,6 @@ class MainWindow(Gtk.ApplicationWindow):
self._client.emitter.connect("connection_error", self._on_connection_error)
# auto save window state and size
self.connect("size-allocate", self._on_size_allocate)
self._settings.bind("maximize", self, "is-maximized", Gio.SettingsBindFlags.SET)
# packing
self._on_playlist_pos_changed() # set orientation
@ -3314,6 +3316,7 @@ class MainWindow(Gtk.ApplicationWindow):
Gtk.main_iteration_do(True)
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)
self._settings.bind("maximize", self, "is-maximized", Gio.SettingsBindFlags.SET) # same problem as one line above
self._client.start()
def _clear_title(self):