From 27261ff871c95c15ca088f88d1fe0c8270f9365e Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Fri, 6 Dec 2024 21:55:04 +0000 Subject: [PATCH 1/6] Only highlight current/next track in correct playlist Fixes #259 --- app/playlistmodel.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 6719027..977e3c4 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -155,10 +155,18 @@ class PlaylistModel(QAbstractTableModel): if file_is_unreadable(rat.path): return QBrush(QColor(Config.COLOUR_UNREADABLE)) # Current track - if track_sequence.current and track_sequence.current.row_number == row: + if ( + track_sequence.current + and track_sequence.current.playlist_id == self.playlist_id + and track_sequence.current.row_number == row + ): return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) # Next track - if track_sequence.next and track_sequence.next.row_number == row: + if ( + track_sequence.next + and track_sequence.next.playlist_id == self.playlist_id + and track_sequence.next.row_number == row + ): return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) # Individual cell colouring From 2c19981cd82bd680d0a7335f4228395d05ad4efd Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sat, 7 Dec 2024 21:09:54 +0000 Subject: [PATCH 2/6] Add icons to playlist tabs Green on tab currently playing Yellow on next tab if different Fixes #245 --- app/config.py | 2 + app/musicmuster.py | 33 ++ app/ui/green-circle.png | Bin 0 -> 6744 bytes app/ui/icons.qrc | 2 + app/ui/icons_rc.py | 889 ++++++++++++++++++++++++++++++++++++++- app/ui/main_window_ui.py | 2 +- app/ui/yellow-circle.png | Bin 0 -> 6778 bytes 7 files changed, 914 insertions(+), 14 deletions(-) create mode 100644 app/ui/green-circle.png create mode 100644 app/ui/yellow-circle.png diff --git a/app/config.py b/app/config.py index fa26ea5..1d7a196 100644 --- a/app/config.py +++ b/app/config.py @@ -85,6 +85,8 @@ class Config(object): OBS_PORT = 4455 PLAY_NEXT_GUARD_MS = 10000 PLAY_SETTLE = 500000 + PLAYLIST_ICON_CURRENT = ":/icons/green-circle.png" + PLAYLIST_ICON_NEXT = ":/icons/yellow-circle.png" PREVIEW_ADVANCE_MS = 5000 PREVIEW_BACK_MS = 5000 PREVIEW_END_BUFFER_MS = 1000 diff --git a/app/musicmuster.py b/app/musicmuster.py index 055a9c2..e48dd8a 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -26,6 +26,7 @@ from PyQt6.QtCore import ( from PyQt6.QtGui import ( QCloseEvent, QColor, + QIcon, QKeySequence, QPalette, QShortcut, @@ -1814,6 +1815,38 @@ class Window(QMainWindow, Ui_MainWindow): else: self.hdrNextTrack.setText("") + self.update_playlist_icons() + + def update_playlist_icons(self) -> None: + """ + Set current / next playlist tab icons + """ + + # Do we need to set a 'next' icon? + set_next = True + if ( + track_sequence.current + and track_sequence.next + and track_sequence.current.playlist_id == track_sequence.next.playlist_id + ): + set_next = False + + for idx in range(self.tabBar.count()): + widget = self.tabPlaylist.widget(idx) + if ( + track_sequence.next + and set_next + and widget.playlist_id == track_sequence.next.playlist_id + ): + self.tabPlaylist.setTabIcon(idx, QIcon(Config.PLAYLIST_ICON_NEXT)) + elif ( + track_sequence.current + and widget.playlist_id == track_sequence.current.playlist_id + ): + self.tabPlaylist.setTabIcon(idx, QIcon(Config.PLAYLIST_ICON_CURRENT)) + else: + self.tabPlaylist.setTabIcon(idx, QIcon()) + class DownloadCSV(QDialog): def __init__(self, parent=None): diff --git a/app/ui/green-circle.png b/app/ui/green-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..388a876fa5885e7d65c0f4515480b395f0a569dc GIT binary patch literal 6744 zcmeHLdvH@_7XNPgO5Z83W|7j{R0YK(H;<-CY-magZ7nTbs1$@Py-98$woO73+7t^7 zP?#!jK?;hB!&XoflwB>#qv--Ipn@!mIw&^6iYTtM!Yq}>cF%Wj`al?W*jfK@hIBf) z`Of+Me&?L;obPM;bYVeubi~LAgiv&DPS#X}c<{(W{7`ti?*7M3csshVXohPlT}9d* zHnXLiAzjsWhGg6pGeYij6@{NiFQcOTH-;r7EncU-7@X5+{kZc==?-39NE5H@Sx=1$&R<5-#1!4=Iv!^=PjXrl=kt=PmU=UokNJIvt(u# z=4NL0B?M_~U9?`8b67v>=$vVdqTz9m)@w~W3o1J!+n!i1D&oI0VOPw94U75tBcB}> zpWX6~&(~%++j4YfaaeJNz>nrv+NZkK59w_f_QVml{fo6#H%3=oMX!{YpFbNHraZ4Y zbG&ZzM{m}@Qd}1|A#nxufZ~JA&;5B=P1~nBUd_RkosaZ7YuAiUvB^ksNZS_r+>t9s z&b;t$OZCg@zdg41v6_N3^WQ_BPJC~-+u!=$<-J}-viU(>_Nb&0D=Xgkt8Cnsk)8L? zFFNZw5x;lzuB}b#vacU5`*K4wwe{16hJ6K2VeTWPaYM4k?lc53pVgm!eS;w>zdJm! zciWYQV~y*|f>w>Xwo||C`>6H8Bd zpZoI1;_tj4j`{YTO(S%?g-uIVUfJ~O_e8^^Uhjsa7xs<|u|zEAbu6CLzA@;gZOIRh z?`gU;z97*Pwcxq;iI|urr$x-3pAPKbb+D=Sl(M^5B zOJx#DT2Ub#jBvUpRsu;NpgSU*MKGt*sf^RMz+q%2Rx(ys@?Z*+u^(?=;3#L)F&U*y zIa2|yPEeKIBAK0=U)YbpC75feu(K#A_AQ<+i+P|}x5S1mY&wGj0eC;}E#7^(v(Dg^ zpRdib85iL2a9h?R0CRjg4P%M1XWXtKq+z&&6nvQ#kBTr{qxELEXM(a2L& zDur66R-_C#O=lcV=tRsZqa+F?Yr)&1g=C^JAbIGJb{okhDCAf@C=txT?^Qj27%TDe9mSB|47Ej7sBX0n*8|C=`+9x{#F@*Im3 z^sCt=Zlb0!^SMvl$8rlhN+ih+g_bsQDLCm$hTCu8;!MVQv~?~6r$?Y&`_?UYXa$Nf zs2G!x7RxD(Osq5*&0a=AflG^M7Bm1c!TY@jr> zSS@2TFfww+$jEOE`hQMQCKnI%geuSzffUoE_*m@Q)HLaB+s-)*I!B>2+!~zEaNJA# z&-*~lpvb@FE6{Ggr36Xd6S=GU-YeI=a@|!0?#lUIcik)3T~*+&obPql|E*jR1K$`K zEBv8X1>Xm^yk)S$cgavgUUnAh#Xs8)?$`h;;r5)FPWS?P82=J&ww)$e40Gk?PYk;p z6&9B;a(>MFpMYCJZdOK-``o|2IKTS46C;-Y@$!tVVsFM@xbxBc=>$ay^LDlJ>QhQviJPY?{@db3Xpg5Mad97LMuvy9)zL}Qiwpp znhf}lP}HH8_+S!weaU=1@|@0&0{iy0La8XA$O|BGYGJZFCYes_eK6oU+-r<{-MYdiC*dIwiqEF&$FWz6|*S_2%G}R zyX3nMAv@tclkOSEW?1ies5%5833A^IvxnFT#3}hAaB4_YT3A8^;+##)M(9=<;l@ES z(Bs|^G^H2q2DaNVyU{*?vpTa0PKOQH2mf5$$EgDxHVt`e3uG_ad%~uFl>!xlG<@GC zdWVBD(gVZS>K5fCf9xA4%J7OegiVE21mlmIgUSp3*p3S{Rm!lrOKtHobxNblf(g_F+AO_Yz+yi0!y6H~tlxJ7%QsL)|p6V(_$C}zJ zXMN3;E}#|Qg}eb=`ee_(31n>)pIn>H`{ytHyg0vWdB~$&Ox&J4RA#!b0YNb%`*s%y zaxNs~J#Z)pb*;J4iyMhcVcmyMZF;6A40UX|W%cO(O($U0ezLnZ&>xMOCZ#V0dDmZS z<_hKE7f4pSqfy5N|5YwN@|nNnWtIe?UCy2=$N~x^IR0&;Y!BgiT+_QB^d!*3Fq-o@ z6O%<}WWTCV4<8A5aIb(g4nrS|!vMhHwFo6) zH5R}_b#Rvi2LK$HPr%gxal_Ss$Y8h{Kn*UUKD8uJ4n`C3MZ~HFBMj(Q0|2P;;TA(4 z0S2Qa3 + yellow-circle.png + green-circle.png star.png star_empty.png record-red-button.png diff --git a/app/ui/icons_rc.py b/app/ui/icons_rc.py index b3de8d0..d9681af 100644 --- a/app/ui/icons_rc.py +++ b/app/ui/icons_rc.py @@ -5154,6 +5154,430 @@ I\x92$I\x92$I\x92$I\x92$I\x92$I\ $I\x92$I\x92$I\x92$I\x92$I\x92\xa4\ U\xfd/\xdc\xe3\x0a%XB:O\x00\x00\x00\x00I\ END\xaeB`\x82\ +\x00\x00\x1aX\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x02\x00\x00\x00\x02\x00\x08\x06\x00\x00\x00\xf4x\xd4\xfa\ +\x00\x00\x00\xc5zTXtRaw prof\ +ile type exif\x00\x00x\ +\xdamP\xdb\x0d\x830\x0c\xfc\xf7\x14\x1d!~\x908\ +\xe3\x04H\xa5n\xd0\xf1\xeb`\xa7\x02\x84\xa5\x9c\xcd\x1d\ +:?\xa0\x7f?ox\x8d \x14\x90\xa5h\xae9'\ +\x0b\xa9R\xa9Y\xa1\xc9\xa3\x1d\x88I\x0e\x9cA\xc1^\ +x\xf8\x0bd\x14[f\x174{\xc6\xc9O\xa3\xc8\xd8\ +\xacZNF\xba\x85\xb0^\x85*\xd1^oF\xd1\x88\ +\xc7Dc\x84=\x8cj\x181\xb9\x80a\xd0|\xad\x94\ +\xab\x96\xf3\x0akO\xd7P\x7f0`\xeb\xbe\x1eF\xb7\ +\xfb\xb7\x14\xbb\xde\xbe\x18\xc9D\x9d\x8d6d\x16\x1f\x80\ +\xc7\x13\xe0f\x02\x19&.\xf6#r\xb1\x9a\xb8\x1e8\ +'\xb1\x83<\xddi\x06\xfc\x00<\xd0Y\xd6\xc84}\ +\xda\x00\x00\x01\x84iCCPICC pro\ +file\x00\x00x\x9c}\x91=H\xc3@\x1c\xc5\ +_S\xa5\x22\x15\x11;\x88:d\xa8Nv\xf1\x0b\xc7\ +Z\x85\x22T\x08\xb5B\xab\x0e&\x97~\x08M\x1a\x92\ +\x14\x17G\xc1\xb5\xe0\xe0\xc7b\xd5\xc1\xc5YW\x07W\ +A\x10\xfc\x00qvpRt\x91\x12\xff\x97\x14Z\xc4\ +xp\xdc\x8fw\xf7\x1ew\xef\x00\xa1^f\x9a\xd5\x11\ +\x074\xdd6\xd3\xc9\x84\x98\xcd\xad\x88\xa1W\x84\x11B\ +\x1f\x860%3\xcb\x98\x95\xa4\x14|\xc7\xd7=\x02|\ +\xbd\x8b\xf1,\xffs\x7f\x8e\x1e5o1 \x12\xc7\ +\x99a\xda\xc4\xeb\xc4\xd3\x9b\xb6\xc1y\x9f8\xc2J\xb2\ +J|NN\x1f\ +\x80\x0cu\x95\xba\x01\x0e\x0e\x81\xd1\x22e\xaf\xf9\xbc\xbb\ +\xab\xbd\xb7\x7f\xcf4\xfb\xfb\x01\xbfSr\xc5n=\xc5\ +\x13\x00\x00\x0e[iTXtXML:com\ +.adobe.xmp\x00\x00\x00\x00\x00<\ +?xpacket begin=\x22\ +\xef\xbb\xbf\x22 id=\x22W5M0MpC\ +ehiHzreSzNTczkc9\ +d\x22?>\x0a\x0a \x0a \x0a \x0a \ +\x0a <\ +rdf:li\x0a stE\ +vt:action=\x22saved\ +\x22\x0a stEvt:ch\ +anged=\x22/\x22\x0a \ +stEvt:instanceID\ +=\x22xmp.iid:0eb6ed\ +ca-2091-4bcf-976\ +3-62819213ae14\x22\x0a\ + stEvt:soft\ +wareAgent=\x22Gimp \ +2.10 (Linux)\x22\x0a \ + stEvt:when=\x22\ +2024-05-06T17:21\ +:44+01:00\x22/>\x0a \ + \x0a\ + \x0a \ + \x0a \x0a \x0a\ +\x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a\ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ +\x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a\ + \ + \ + \ + \ + \ + \ + \x0a \ + \ +\x0a\x99\xb0bn\x00\x00\x00\x06bKGD\ +\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09pH\ +Ys\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\x00\ +\x00\x00\x07tIME\x07\xe8\x0c\x07\x11\x1d\x1aq\x0e\ +\xba\xd6\x00\x00\x09\x1dIDATx\xda\xed\xdc\xdd\x8d\ +\xea\xca\x16\x85Q\xe8X\x9c \x91\x90\xa0s\x81\x07\x9e\ +L[\x08\xaaj\xd5\xef\x18\x09\x5ciK\xe7\xce\xcf\xcb\ +\xa6/\x17\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe0\xe0\xea\x9f\x00\ +\xc6\xb6\xdd\xb7G\x8b\xff\xdd\xfd\xb6\xfb\xff\x0f\x10\x00\xc0\ +L\xe3.\x12@\x00\x00\x86^\x18\x80\x00\x00\x0c\xbd0\ +\x00\x01\x00\x18|A\x00\x02\x00\x0c\xbe\xc1\x17\x04 \x00\ +\xc0\xe0#\x08@\x00\x80\xd1G\x0c\x80\x00\x00\xa3\x8f\x18\ +\x00\x01\x00F\x1f1\x00\x02\x00\x8c>b\x00\x04\x00\x18\ +~\x84\x00\x08\x000\xfa\x88\x01\x10\x00`\xf8\x11\x02 \ +\x00\xc0\xe8#\x06@\x00`\xf8A\x08\x80\x00\xc0\xf0\x83\ +\x10\x00\x01\x80\xe1\x07!\x80\x00\x00\xc3\x0fB\x00\x01\x00\ +\x86\x1f\x84\x00\x02\x00\x0c?\x08\x01\x04\x00\x18~\x10\x02\ +\x08\x000\xfe \x02\x10\x00`\xf8\x11\x02 \x00\xc0\xf0\ +#\x04@\x00\x80\xf1G\x04\x80\x00\x00\xc3\x8f\x10\x00\x01\ +\x80\xe1\x07!\x00\x02\x00\xe3\x0f\x22\x00\x04\x00\x86\x1f\x84\ +\x00\x08\x00\x8c?\x88\x00\x10\x00\x18\x7f\x10\x01 \x000\ +\xfc \x04@\x00`\xfcA\x04\x80\x00\xc0\xf0\x83\x10@\ +\x00\x80\xf1\x07\x11\x80\x00\x00\xe3\x0f\x22\x00\x01\x00\xc6\x1f\ +D\x00\x02\x00\x0c?\x08\x01\x04\x00\xc6\xdf\xf8\x83\x08@\ +\x00`\xfc\x01\x11\x80\x00\xc0\xf8\x03\x22\x00\x01\x80\xf1\x07\ +D\x00\x02\x00\xe3\x0f\x88\x00\x04\x00\x86\x1f\x10\x02\x08\x00\ +\x8c? \x02\x10\x00\x18\x7f@\x04 \x000\xfe\x80\x08\ +@\x00`\xfc\x01\x11\x80\x00\xc0\xf8\x83\x08\x10\x01\x08\x00\ +\x8c?\x88\x00\x10\x00\x18\x7f\x10\x01\x08\x000\xfe \x02\ +\x10\x00\x18\x7f@\x04 \x000\xfe\x80\x08@\x00`\xfc\ +\x01\x11\xc0\xd8\xfe\xfc\x13\x00\x80\x0b\x00\x9e\xfe\x01W\x00\ +\x04\x00\xc6\x1f\x10\x01\x08\x00\x8c? \x02\x10\x00\x18\x7f\ +@\x04 \x000\xfe\x80\x08@\x00`\xfc\x01\x11@\xaf\ +\xfc\x0c\x10\x00\x5c\x00\xf0\xf4\x0f\xb8\x02\xb8\x02\x08\x00\x8c\ +? \x02\x10\x00\x18\x7f@\x04 \x000\xfe\x80\x08`\ +P>\x02\x04\x00\x17\x00<\xfd\x03\xb8\x02\x08\x00\x8c?\ +\x80\x08\x98\x92W\x00\x00\xe0\x02\x80\xa7\x7f\x00W\x00\x01\ +\x80\xf1\x07\x10\x01\x02\x00\xe3\x0f \x02\xe6\xe0\x1b\x00\x00\ +p\x01\xc0\xd3?\x80+\x80\x00\xc0\xf8\x03\x88\x80)y\ +\x05\x00\x00.\x00x\xfa\x07p\x05p\x01\x00\x00\x5c\x00\ +\xf0\xf4\x0f\xe0\x0a \x000\xfe\x00\x22`P^\x01\x00\ +\x80\x0b\x00\x9e\xfe\x01\x5c\x01\x5c\x00\x00\x00\x17\x00<\xfd\ +\x03\xb8\x02\x08\x00\x8c?\x80\x08\x18\x94W\x00\x00\xe0\x02\ +\x80\xa7\x7f\x00W\x00\x17\x00\x00\xc0\x05\x00O\xff\x00\xae\ +\x00.\x00\x00\x80\x0b\x00\x9e\xfe\x01\x5c\x01\x5c\x00\x00\x00\ +\x17\x00<\xfd\x03\xae\x00\xb8\x00\x00\x00.\x00x\xfa\x07\ +\x5c\x01p\x01\x00\x00\x5c\x00<\xfd\x03\xb8\x02\xe0\x02\x00\ +\x00\xb8\x00x\xfa\x07p\x05\xc0\x05\x00\x00\x10\x00\x00\xc0\ +\x7fN1\x9dp\xfe\x07V\xe15\x80\x0b\x00\x00\xe0\x02\ +\xe0\xe9\x1f\xc0\x15\x00\x17\x00\x00@\x00\x00\x00\xe58\xc1\ +4\xe6\xfc\x0f\xac\xcak\x00\x17\x00\x00\xc0\x05\xc0\xd3?\ +\x80+\x00.\x00\x00\x80\x00\x00\x00\xf29\xbd4\xe2\xfc\ +\x0f\xf0\xe25\x80\x0b\x00\x00 \x00\x00\x80(\xce.\x0d\ +8\xff\x03\x1cy\x0d\xe0\x02\x00\x00\x08\x00\x00 \x82\x93\ +Ke\xce\xff\x00\xe7\xbc\x06p\x01\x00\x00\x04\x00\x00P\ +\x9asKE\xce\xff\x00\x9fy\x0d\xe0\x02\x00\x00\x08\x00\ +\x00@\x00\x00\x00Y\xbck\xa9\xc4\xfb\x7f\x80\xef\xf8\x0e\ +\xc0\x05\x00\x00\x10\x00\x00\x80\x00\x00\x00\x92y\xcfR\x81\ +\xf7\xff\x00\xbf\xf1\x1d\x80\x0b\x00\x00 \x00\x00\x00\x01\x00\ +\x00$\xf1\x8e%\x98\xf7\xff\x00i|\x07\xe0\x02\x00\x00\ +\x08\x00\x00@\x00\x00\x00\x02\x00\x00\x10\x00\x00\xc0\x09_\ +X\x06\xf2\x0b\x00\x80<~\x09\xe0\x02\x00\x00\x08\x00\x00\ +@\x00\x00\x00\x02\x00\x00\x10\x00\x00\xc0\x1b_W\x06\xf1\ +\x0b\x00\x802\xfc\x12\xc0\x05\x00\x00\x10\x00\x00\x80\x00\x00\ +\x00\x04\x00\x00 \x00\x00\x00\x01\x00\x00\x02\x00\x00\x10\x00\ +\x00\xc0\xec\xfcq\x85\x00\xfe\x08\x10@Y\xfe\x18\x90\x0b\ +\x00\x00 \x00\x00\x00\x01\x00\x00\x08\x00\x00@\x00\x00\x00\ +\x02\x00\x00\x04\x00\x00 \x00\x00\x00\x01\x00\x00\x08\x00\x00\ +@\x00\x00\x00\x02\x00\x00\x10\x00\x00\x80\x00\x00\x00\x04\x00\ +\x00 \x00\x00\x00\x01\x00\x00\x08\x00\x00@\x00\x00\x00\x02\ +\x00\x00\x10\x00\x00\x80\x00\x00\x00\x04\x00\x00\x08\x00\x00@\ +\x00\x00\x00\x02\x00\x00\x10\x00\x00\x80\x00\x00\x00\x04\x00\x00\ +0\x80\xab\x7f\x82\x18\xdb}{\xf8W\x00\xc8\xb7\xdfv\ +[\xe5\x02\x00\x00\x08\x00\x00@\x00\x00\x00\x02\x00\x00\x10\ +\x00\x00\x80\x00\x00\x00\x01\x00\x00\x08\x00\x00`j\xfe\xb8\ +B \x7f\x0c\x08 \x8f?\x02\xe4\x02\x00\x00\x08\x00\x00\ +@\x00\x00\x00\x02\x00\x00\x10\x00\x00\xc0\x1b_W\x06\xf3\ +K\x00\x804~\x01\xe0\x02\x00\x00\x08\x00\x00@\x00\x00\ +\x00\x02\x00\x00\x10\x00\x00\xc0\x09_XV\xe0\x97\x00\x00\ +\xbf\xf1\x0b\x00\x17\x00\x00@\x00\x00\x00\x02\x00\x00H\xe2\ +\x1dK%\xbe\x03\x00\xf8\x8e\xf7\xff.\x00\x00\x80\x00\x00\ +\x00\x04\x00\x00\x90\xcc{\x96\x8a|\x07\x00\xf0\x99\xf7\xff\ +.\x00\x00\x80\x00\x00\x00\x04\x00\x00\x90\xc5\xbb\x96\xca|\ +\x07\x00p\xce\xfb\x7f\x17\x00\x00@\x00\x00\x00\xa59\xb7\ +4\xe05\x00\xc0\x91\xf3\xbf\x0b\x00\x00 \x00\x00\x80\x08\ +N.\x8dx\x0d\x00\xf0\xe2\xfc\xef\x02\x00\x00\x08\x00\x00\ + \x8a\xb3KC^\x03\x00\xabs\xfew\x01\x00\x00\x04\ +\x00\x00\x10\xc9\xe9\xa51\xaf\x01\x80U9\xff\xbb\x00\x00\ +\x00.\x00\xae\x00\x00\x9e\xfeq\x01\x00\x00\x04\x00\x00\x90\ +\xcf\x09\xa6\x13^\x03\x00\xabp\xfew\x01\x00\x00\x5c\x00\ +p\x05\x00<\xfd\xe3\x02\x00\x00\x08\x00\x00\xa0\x1c\xa7\x98\ +\xcex\x0d\x00\xcc\xca\xf9\xdf\x05\x00\x00p\x01\xc0\x15\x00\ +\xf0\xf4\x8f\x0b\x00\x00\xe0\x02\xe0\x0a\x00\xe0\xe9\x1f\x17\x00\ +\x00\xc0\x05\xc0\x15\x00\xc0\xd3?.\x00\x00\x80\x0b\x80+\ +\x00\x80\xa7\x7f\x5c\x00\x00\x00\x17\x00W\x00\x00O\xff.\ +\x00\x00\x80\x0b\x00\xae\x00\x00\x9e\xfe]\x00\x00\x00\x17\x00\ +\x5c\x01\x00<\xfd\x0b\x00D\x00\x80\xf1\x1f\x8cW\x00\x00\ +\xe0\x02\x80+\x00\x80\xa7\x7f\x17\x00\x00\xc0\x05\x00W\x00\ +\x00O\xff\x02\x00\x11\x00\x18\x7f;2(\xaf\x00\x00\xc0\ +\x05\x00W\x00\x00O\xff.\x00\x00\x80\x0b\x00\xae\x00\x00\ +\x9e\xfe\x05\x00\x22\x000\xfe\x0c\xca+\x00\x00p\x01\xc0\ +\x15\x00\xc0\xd3\xbf\x00@\x04\x00\x18\x7f\x01\x80\x08\x00\x8c\ +?s\xf0\x0d\x00\x00\xb8\x00\xe0\x0a\x00\xe0\xe9_\x00 \ +\x02\x00\xe3\xcf\x94\xbc\x02\x00\xc0\xf8\x0b\x00\xfc\xc7\x0b\xc0\ +\x0a\x0c\xc4\x02\xbc\x0a\x00<@ \x00D\x00\x80\xf1G\ +\x00\x88\x00\x00\xe3\xbf\x22\xdf\x00\x00\x80\x0b\x00\xae\x00\x80\ +\xa7\x7f\x04\x00\x22\x000\xfe\x08\x00D\x00`\xfc\x11\x00\ +\x88\x00\xc0\xf8#\x00\x10\x01\x80\xf1G\x00 \x02\x00\xe3\ +O\xb7\xfc\x0c\x10\x00\x5c\x00p\x05\x00<\xfd#\x00\x10\ +\x01\x80\xf1G\x00 \x02\x00\xe3\x8f\x00@\x04\x00\xc6\x1f\ +\x01\x80\x08\x00\x8c?\x02\x00\x11\x00\x18\x7f\x04\x00\x22\x00\ +0\xfe\x08\x00D\x00`\xfc\x11\x00\x88\x00\xc0\xf8#\x00\ +\x10\x01\x80\xf1G\x00 \x02\x00\xe3\x8f\x00@\x08\x00\x86\ +\x1f\x01\x80\x08\x00\xe3\x0f\x02\x00\x11\x00\xc6\x1f\x04\x00\x22\ +\x00\x8c?\x08\x00D\x00\x18\x7f\x10\x00\x88\x000\xfe\x08\ +\x00\x10\x02`\xf8\x11\x00 \x02\xc0\xf8#\x00@\x04\x80\ +\xf1G\x00\x80\x08\x00\xc3\x8f\x00\x00!\x00\xc6\x1f\x01\x00\ +\x22\x00\x8c?\x02\x00\x84\x00\x18~\x04\x00\x88\x000\xfe\ +\x08\x00D\x00\x18\x7f\x10\x00\x08\x010\xfc \x00\x10\x01\ +`\xfcA\x00 \x04\xc0\xf0\x83\x00@\x08\x80\xe1\x07\x01\ +\x80\x08\x00\xe3\x0f\x02\x00!\x00\x86\x1f\x04\x00B\x00\x0c\ +?\x08\x00D\x00\x18\x7f\x04\x00\x08\x010\xfc\x08\x00\x10\ +\x02`\xf8\x11\x00 \x04\xc0\xf0#\x00@\x08`\xf8A\ +\x00\x80\x10\xc0\xf0\x83\x00\x00!\x80\xe1\x07\x01\x00B\x00\ +\xc3\x0f\x02\x00\xc4\x00F\x1f\x04\x00\x08\x01\x0c?\x08\x00\ +\x10\x03\x18}\x10\x00 \x040\xfc \x00@\x0c`\xf4\ +A\x00\x80\x18\xc0\xe8\x83\x00\x001`\xf4\x01\x01\x00\x82\ +\xc0\xe8\x03\x02\x00\xc4\x80\xc1\x07\x04\x00\x08\x02\x83\x0f\x02\ +\x00\x10\x04\x06\x1f\x04\x00 \x0c\x0c=\x08\x00@\x18\x18\ +z\x10\x00\x80H0\xee\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +L\xe6\x09l=\x1eh\xbb\x22\xdfY\x00\x00\x00\x00I\ +END\xaeB`\x82\ \x00\x00\x1aS\ \x89\ PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ @@ -6987,6 +7411,432 @@ PH\x00\x00@!\x01\x00\x00\x85\x04\x00\x00\x14\x12\x00\ \x00\x00PH\x00\x00@!\x01\x00\x00\x85\xfe\x17S\xd3\ \xbcN3\xeb\xa5U\x00\x00\x00\x00IEND\xaeB\ `\x82\ +\x00\x00\x1az\ +\x89\ +PNG\x0d\x0a\x1a\x0a\x00\x00\x00\x0dIHDR\x00\ +\x00\x02\x00\x00\x00\x02\x00\x08\x06\x00\x00\x00\xf4x\xd4\xfa\ +\x00\x00\x00\xc6zTXtRaw prof\ +ile type exif\x00\x00x\ +\xdamP[\x0e\xc30\x08\xfb\xe7\x14;B\x024\x8f\ +\xe3\xa4m&\xed\x06;\xfe\x9cB\xa6\xb6\xaa\xa5\x00\xb1\ +#\x03\xa1\xfe\xfd\xbc\xe95\xc0QI\x97\x5cRM)\ +\x00Z\xb5rCQ\x82\xa1\x1d1\x06=\xe2\x04;{\ +\xe1\xe9/0(A\x16\x13J\xb2\x1c'?\x8d<\xc7\ +\x86j9\x19\x95\xcd\x85\xf5*T\xf5\xf6\xe5f\xe4\x8d\ +dL4F\xd8\xdd\xa8\xba\x91\xb0\x09\xd1\x0d\x9a\xad\x15\ +R-\xf9\xbc\xc2\xda\xc3\x15\xc5\x0e\x8d\xb0u[/z\ +\xb7\xfb]3~o_@\x0as\x17\xd0\x88\x22j\x03\ +\xc88J\xd2 0b\x90\x8c\x87Q2j\xc1m\xf0\ +\xd9'\xc1\x87<\xfd\xd3\x04\xfd\x00;\xb0Y\xd0\xf2*\ +1\xb7\x00\x00\x01\x84iCCPICC pr\ +ofile\x00\x00x\x9c}\x91=H\xc3@\x1c\ +\xc5_S\xa5\x22\x15\x11;\x88:d\xa8Nv\xf1\x0b\ +\xc7Z\x85\x22T\x08\xb5B\xab\x0e&\x97~\x08M\x1a\ +\x92\x14\x17G\xc1\xb5\xe0\xe0\xc7b\xd5\xc1\xc5YW\x07\ +WA\x10\xfc\x00qvpRt\x91\x12\xff\x97\x14Z\ +\xc4xp\xdc\x8fw\xf7\x1ew\xef\x00\xa1^f\x9a\xd5\ +\x11\x074\xdd6\xd3\xc9\x84\x98\xcd\xad\x88\xa1W\x84\x11\ +B\x1f\x860%3\xcb\x98\x95\xa4\x14|\xc7\xd7=\x02\ +|\xbd\x8b\xf1,\xffs\x7f\x8e\x1e5o1 \x12\ +\xc7\x99a\xda\xc4\xeb\xc4\xd3\x9b\xb6\xc1y\x9f8\xc2J\ +\xb2J|NN\ +\x1f\x80\x0cu\x95\xba\x01\x0e\x0e\x81\xd1\x22e\xaf\xf9\xbc\ +\xbb\xab\xbd\xb7\x7f\xcf4\xfb\xfb\x01\xbfSr\xc5n=\ +\xc5\x13\x00\x00\x0e[iTXtXML:co\ +m.adobe.xmp\x00\x00\x00\x00\x00\ +\x0a\x0a \x0a <\ +rdf:Description \ +rdf:about=\x22\x22\x0a \ + xmlns:xmpMM=\x22ht\ +tp://ns.adobe.co\ +m/xap/1.0/mm/\x22\x0a \ + xmlns:stEvt=\x22\ +http://ns.adobe.\ +com/xap/1.0/sTyp\ +e/ResourceEvent#\ +\x22\x0a xmlns:dc=\x22\ +http://purl.org/\ +dc/elements/1.1/\ +\x22\x0a xmlns:GIMP\ +=\x22http://www.gim\ +p.org/xmp/\x22\x0a \ +xmlns:tiff=\x22http\ +://ns.adobe.com/\ +tiff/1.0/\x22\x0a x\ +mlns:xmp=\x22http:/\ +/ns.adobe.com/xa\ +p/1.0/\x22\x0a xmpMM\ +:DocumentID=\x22gim\ +p:docid:gimp:66a\ +dc484-6096-437b-\ +aa0d-3b0c0a65c53\ +d\x22\x0a xmpMM:Inst\ +anceID=\x22xmp.iid:\ +35e7befe-1312-42\ +39-acb8-27e7aad8\ +cbff\x22\x0a xmpMM:O\ +riginalDocumentI\ +D=\x22xmp.did:9f854\ +f62-b4a5-42e7-98\ +c2-a9cc8ee1f29f\x22\ +\x0a dc:Format=\x22i\ +mage/png\x22\x0a GIM\ +P:API=\x222.0\x22\x0a G\ +IMP:Platform=\x22Li\ +nux\x22\x0a GIMP:Tim\ +eStamp=\x2217335926\ +19389840\x22\x0a GIM\ +P:Version=\x222.10.\ +34\x22\x0a tiff:Orie\ +ntation=\x221\x22\x0a x\ +mp:CreatorTool=\x22\ +GIMP 2.10\x22\x0a xm\ +p:MetadataDate=\x22\ +2024:12:07T17:30\ +:17+00:00\x22\x0a xm\ +p:ModifyDate=\x2220\ +24:12:07T17:30:1\ +7+00:00\x22>\x0a \x0a \ + \x0a \ +\x0a \ + \ +\x0a \x0a\ + \x0a \x0a \x0a\x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ +\x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a\ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \ + \ + \ + \ + \ +\x0a \ + \ + \ + \ + \ + \ + \x0a \ + \ + \x0a\xc2\x0b\x10K\x00\x00\x00\x06bKG\ +D\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\x00\x00\x09p\ +HYs\x00\x00\x0e\xc3\x00\x00\x0e\xc3\x01\xc7o\xa8d\ +\x00\x00\x00\x07tIME\x07\xe8\x0c\x07\x11\x1e\x13#\ +\xffQ\xb1\x00\x00\x09>IDATx\xda\xed\xdc\xcb\ +\xb1\xe2@\x10EA`\x83Y\xd8\x8eY\xac\xc4\x86\x8d\ +P\x10@wW\x7f3Mx\x113\xf7\xa8\x04\x9cN\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\xc0\xce\xd9\x9f\x00\xc6\xb6\xdd\ +\xaf[\x93\xffb\x00\x04\x00\x18}\xc4\x00\x08\x000\ +\xfa\x88\x01\x10\x00`\xf4\x11\x03 \x00\xc0\xf0#\x04@\ +\x00\x80\xd1G\x0c\x80\x00\x00\xc3\x8f\x10\x00\x01\x00F\x1f\ +1\x00\x02\x00\xc3\x0fB\x00\x04\x00\x86\x1f\x84\x00\x08\x00\ +\x0c?\x08\x01\x04\x00\x18~\x10\x02\x08\x000\xfc \x04\ +\x10\x00`\xf8A\x08 \x00\xc0\xf0\x83\x10@\x00\x80\xf1\ +\x07\x11\x80\x00\x00\xc3\x8f\x10\x00\x01\x00\x86\x1f!\x00\x02\ +\x00\x8c?\x22\x00\x04\x00\x18~\x84\x00\x08\x00\x0c?\x08\ +\x01\x10\x00\x18\x7f\x10\x01 \x000\xfc \x04@\x00`\ +\xfcA\x04\x80\x00\xc0\xf8\x83\x08\x00\x01\x80\xe1\x07!\x00\ +\x02\x00\xe3\x0f\x22\x00\x04\x00\x86\x1f\x84\x00\x02\x00\x8c?\ +\x88\x00\x04\x00\x18\x7f\x10\x01\x08\x000\xfe \x02\x10\x00\ +`\xf8A\x08 \x000\xfe\xc6\x1fD\x00\x02\x00\xe3\x0f\ +\x88\x00\x04\x00\xc6\x1f\x10\x01\x08\x00\x8c? \x02\x10\x00\ +\x18\x7f@\x04 \x000\xfc\x80\x10@\x00`\xfc\x01\x11\ +\x80\x00\xc0\xf8\x03\x22\x00\x01\x80\xf1\x07D\x00\x02\x00\xe3\ +\x0f\x88\x00\x04\x00\xc6\x1fD\x80\x08@\x00`\xfcA\x04\ +\x80\x00\xc0\xf8\x83\x08@\x00\x80\xf1\x07\x11\x80\x00\xc0\xf8\ +\x03\x22\x00\x01\x80\xf1\x07D\x00\x02\x00\xe3\x0f\x88\x00\xc6\ +v\xf1'\x00\x00\x17\x00<\xfd\x03\xae\x00\x08\x00\x8c?\ + \x02\x10\x00\x18\x7f@\x04 \x000\xfe\x80\x08@\x00\ +`\xfc\x01\x11\x80\x00\xc0\xf8\x03\x22\x80^\xf9\x1a \x00\ +\xb8\x00\xe0\xe9\x1fp\x05p\x05\x10\x00\x18\x7f@\x04 \ +\x000\xfe\x80\x08@\x00`\xfc\x01\x11\xc0\xa0|\x08\x10\ +\x00\x5c\x00\xf0\xf4\x0f\xe0\x0a \x000\xfe\x00\x22`J\ +^\x01\x00\x80\x0b\x00\x9e\xfe\x01\x5c\x01\x04\x00\xc6\x1f@\ +\x04\x08\x00\x8c?\x80\x08\x98\x83\xcf\x00\x00\x80\x0b\x00\x9e\ +\xfe\x01\x5c\x01\x04\x00\xc6\x1f@\x04L\xc9+\x00\x00p\ +\x01\xc0\xd3?\x80+\x80\x0b\x00\x00\xe0\x02\x80\xa7\x7f\x00\ +W\x00\x01\x80\xf1\x07\x10\x01\x83\xf2\x0a\x00\x00\x5c\x00\xf0\ +\xf4\x0f\xe0\x0a\xe0\x02\x00\x00\xb8\x00\xe0\xe9\x1f\xc0\x15@\ +\x00`\xfc\x01D\xc0\xa0\xbc\x02\x00\x00\x17\x00<\xfd\x03\ +\xb8\x02\xb8\x00\x00\x00.\x00x\xfa\x07p\x05p\x01\x00\ +\x00\x5c\x00\xf0\xf4\x0f\xe0\x0a\xe0\x02\x00\x00\xb8\x00\xe0\xe9\ +\x1fp\x05\xc0\x05\x00\x00p\x01\xc0\xd3?\xe0\x0a\x80\x0b\ +\x00\x00\xe0\x02\xe0\xe9\x1f\xc0\x15\x00\x17\x00\x00\xc0\x05\xc0\ +\xd3?\x80+\x00.\x00\x00\x80\x00\x00\x00\x8e\x9cb:\ +\xe1\xfc\x0f,3<^\x03\xb8\x00\x00\x00.\x00\x9e\xfe\ +\x01\x5c\x01p\x01\x00\x00\x04\x00\x00P\x8c\x13Lc\xce\ +\xff\xc0\xb2\x03\xe45\x80\x0b\x00\x00\xe0\x02\xe0\xe9\x1f\xc0\ +\x15\x00\x17\x00\x00@\x00\x00\x00\xd9\x9c^\x1aq\xfe\x07\ +x\x0d\x91\xd7\x00.\x00\x00\x80\x00\x00\x00\x828\xbb4\ +\xe0\xfc\x0f\xf06F^\x03\xb8\x00\x00\x00\x02\x00\x00\x08\ +\xe0\xe4R\x99\xf3?\xc0\x87A\xf2\x1a\xc0\x05\x00\x00\x10\ +\x00\x00@a\xce-\x159\xff\x03|\x19%\xaf\x01\x5c\ +\x00\x00\x00\x01\x00\x00\x08\x00\x00 \x87w-\x95x\xff\ +\x0f\xf0\xe30\xf9\x1c\x80\x0b\x00\x00 \x00\x00\x00\x01\x00\ +\x00\xa4\xf2\x9e\xa5\x02\xef\xff\x01\xfe\x1c'\x9f\x03p\x01\ +\x00\x00\x04\x00\x00 \x00\x00\x80\x14\xde\xb1\x04\xf3\xfe\x1f\ + q\xa0|\x0e\xc0\x05\x00\x00\x10\x00\x00\x80\x00\x00\x00\ +\x04\x00\x00 \x00\x00\x80#\x9f\xb0\x0c\xe4\x1b\x00\x00\x99\ +#\xe5\x9b\x00.\x00\x00\x80\x00\x00\x00\x04\x00\x00 \x00\ +\x00\x00\x01\x00\x00\xec\xf9te\x10\xdf\x00\x00(4T\ +\xbe\x09\xe0\x02\x00\x00\x08\x00\x00@\x00\x00\x00\x02\x00\x00\ +\x10\x00\x00\x80\x00\x00\x00\x01\x00\x00\x08\x00\x00`r~\ +\x5c!\x80\x1f\x01\x02(\x08\x00\x10\x02\x18~\x10\x00 \x06\ +0\xfa \x00@\x0c`\xf4A\x00\x80\x180\xfa\x80\x00\ +\x00A`\xf4\x01\x01\x00b\xc0\xe0\x03\x02\x00\x04\x81\xc1\ +\x07\x01\x00\x08\x02\x83\x0f\x02\x00\x10\x06\x86\x1e\x04\x00 \ +\x0c\x0c=\x08\x00@$\x18w\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00&\xf3\x042\xbc\x1c\x88\x0c\xb2\xbfh\x00\x00\x00\ +\x00IEND\xaeB`\x82\ \x00\x03\x03W\ \xff\ \xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x00\x00\x01\x00\ @@ -21203,6 +22053,10 @@ qt_resource_name = b"\ \x02\xac\xf9g\ \x00s\ \x00t\x00a\x00r\x00_\x00e\x00m\x00p\x00t\x00y\x00.\x00p\x00n\x00g\ +\x00\x10\ +\x02\x1c\xb4\xa7\ +\x00g\ +\x00r\x00e\x00e\x00n\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00p\x00n\x00g\ \x00\x0a\ \x0bb\xb2#\ \x00h\ @@ -21215,6 +22069,11 @@ qt_resource_name = b"\ \x00\x06\xc7\xa5\ \x00f\ \x00a\x00d\x00e\ +\x00\x11\ +\x07 \x1a\x87\ +\x00y\ +\x00e\x00l\x00l\x00o\x00w\x00-\x00c\x00i\x00r\x00c\x00l\x00e\x00.\x00p\x00n\x00g\ +\ \x00\x08\ \x0bg\x90\x9e\ \x00s\ @@ -21241,11 +22100,11 @@ qt_resource_name = b"\ qt_resource_struct = b"\ \x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x00\x00\x00\x02\x00\x00\x00\x12\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x14\x00\x00\x00\x02\ \x00\x00\x00\x00\x00\x00\x00\x00\ -\x00\x00\x01*\x00\x00\x00\x00\x00\x01\x00\x01\xa2\x89\ +\x00\x00\x01P\x00\x00\x00\x00\x00\x01\x00\x01\xbc\xe5\ \x00\x00\x01xr\xe9}\xc0\ -\x00\x00\x01\xb2\x00\x00\x00\x00\x00\x01\x00\x05&\x0f\ +\x00\x00\x02\x00\x00\x00\x00\x00\x00\x01\x00\x05Z\xe9\ \x00\x00\x01x\x84r}X\ \x00\x00\x00\x10\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ \x00\x00\x01x\xbb\xe5wh\ @@ -21253,25 +22112,29 @@ qt_resource_struct = b"\ \x00\x00\x01xJ\xff\x88\xe0\ \x00\x00\x00\xb0\x00\x00\x00\x00\x00\x01\x00\x01\x0aa\ \x00\x00\x01x\x84r}X\ -\x00\x00\x01\x9a\x00\x00\x00\x00\x00\x01\x00\x05\x11D\ +\x00\x00\x01\xe8\x00\x00\x00\x00\x00\x01\x00\x05F\x1e\ \x00\x00\x01y/9=X\ +\x00\x00\x00\xfa\x00\x00\x00\x00\x00\x01\x00\x01@o\ +\x00\x00\x01\x93\xa2+J\x8e\ \x00\x00\x00\xd8\x00\x00\x00\x00\x00\x01\x00\x01\x12\x83\ -\x00\x00\x01\x8fTX\xa5\x95\ -\x00\x00\x01j\x00\x00\x00\x00\x00\x01\x00\x04\xf7\xc9\ -\x00\x00\x01\x8fN\xb6\x0a3\ +\x00\x00\x01\x90x\xb0R\x0a\ +\x00\x00\x01\xb8\x00\x00\x00\x00\x00\x01\x00\x05,\xa3\ +\x00\x00\x01\x90x\xb0R\x06\ +\x00\x00\x01^\x00\x00\x00\x00\x00\x01\x00\x01\xcc\xec\ +\x00\x00\x01\x93\xa2,\x19\x82\ \x00\x00\x00\x1e\x00\x00\x00\x00\x00\x01\x00\x00\x04\x98\ \x00\x00\x01xr\xe5\x9d\x90\ \x00\x00\x006\x00\x00\x00\x00\x00\x01\x00\x00d\xab\ \x00\x00\x01x\x84r}X\ \x00\x00\x00b\x00\x00\x00\x00\x00\x01\x00\x00\x80\x85\ -\x00\x00\x01\x8fN\xb2\xe2%\ -\x00\x00\x01\x14\x00\x00\x00\x00\x00\x01\x00\x01Z\xc6\ -\x00\x00\x01\x8fTd\x16^\ -\x00\x00\x01N\x00\x00\x00\x00\x00\x01\x00\x04\xb5\xeb\ +\x00\x00\x01\x90x\xb0R\x06\ +\x00\x00\x01:\x00\x00\x00\x00\x00\x01\x00\x01u\x22\ +\x00\x00\x01\x90x\xb0R\x06\ +\x00\x00\x01\x9c\x00\x00\x00\x00\x00\x01\x00\x04\xea\xc5\ \x00\x00\x01\x80A\xd4\xb2`\ -\x00\x00\x00\xfa\x00\x00\x00\x00\x00\x01\x00\x01@o\ +\x00\x00\x01 \x00\x00\x00\x00\x00\x01\x00\x01Z\xcb\ \x00\x00\x01\x8bRz\xee0\ -\x00\x00\x018\x00\x00\x00\x00\x00\x01\x00\x01\xb2\x90\ +\x00\x00\x01\x86\x00\x00\x00\x00\x00\x01\x00\x01\xe7j\ \x00\x00\x01x\xe8\xe8Z\xc8\ \x00\x00\x00\x98\x00\x00\x00\x00\x00\x01\x00\x00\xbf\x98\ \x00\x00\x01xr\xe6\xdd\xe0\ diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index e41eff2..b59a6f6 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -1,6 +1,6 @@ # Form implementation generated from reading ui file 'app/ui/main_window.ui' # -# Created by: PyQt6 UI code generator 6.7.0 +# Created by: PyQt6 UI code generator 6.7.1 # # WARNING: Any manual changes made to this file will be lost when pyuic6 is # run again. Do not edit this file unless you know what you are doing. diff --git a/app/ui/yellow-circle.png b/app/ui/yellow-circle.png new file mode 100644 index 0000000000000000000000000000000000000000..94a4f60267ab8ed47066f588549cb98d24adc8a0 GIT binary patch literal 6778 zcmeHLdr(tn7XNM%AR%t; zO`op#d34#&zkFxDxW+U^G2yFkcD%P?*MJj)UVJ;DRNQgk(3!&tM`PFQS};qpsOk38 z@+J0XbkVNF&uc}`a*wGdeL+%&^>wSJ%AP%FZ96^k;HvC9CwX_!*j+O|ZyzmfLWo#y z$<3D8Gmoyo3SJ6m+b1j_b}2~?Zrx0u zIr8I?lP|q&X#vC?eWXg%pRiEeZ{V%qA}(1g9}FOFmRc#YEQhe(U7XY zJwO=T`eXgE?d#`p>r$@o(A8YIbubw4G(AZhzMdRC(sw+uwIhkh+pl}&d)2wtwI@}? zvd{dR&bCkg!T-s~%X^=HNXuE&v~=~4&%btss2}V1Z%ln@PkMwUdIhI#$%J!%)Fr#X+>v`2>^6g$fT9rl!d zipFTSX3!?PfywY%9ng9RW#@Puv~eEeCJju5#ikWq-QO%CEhepKx>8T+9r?^$%fv-a zX4<0SGUK9oMzu+lGcG#Ys{sNl3 zCI%0&YoX0ed&M?a8m0(w6fiEM)8cSj>^2f}(gyniw^k$qJJ}td)uGq-(A!+$DnLCX zUfLm%W>6BVRniyXa*wY9l5jxxM7YXeP$i{|%f7&AWX4x9Hg{TI3X`!1?^xihWYaMj zB}^q_1y>iSO8ZF`7U@fR5V!;t7OR6rL9zRJx-I73V)cs+TiA5^1_JON-hSTQxwFpT zrPpf;?8XH+yrKfF2=vCO2c+q ztaJqf>)WSQXFGCQ>86D=;# zU&t#DasiYMNulXI7_Kii4}~L7OSLk zlQ@e~$W4@4Ny$vA{z3oGDZ(`M_Jlgz6X6uICHPqE-qdW#UE3aV=yQ%jX+mpoKErV@ z={fJiHG?Amp09Ab{hkse`9S2p>ieKv56X346}T_w2i^6cT=!Lh`*MEJUH`XoMfZMU zWNh${o(Db;9vUc^44)+<4U-ECP!RuYegE~1urk0gafS;%fF8!bM5}#=2^OQ=Mf&kk zmtvyghYv{$PTdV|$R8NU5uoz6 zK_8HaK#{^+u-A<yiNds2|_6V4#(vI zV5M+50Ah0Z1b~+aH~{V>c>und%g5jv0f4VX#rJ?$3RAIKjtHwQ=>`i>Qni2}S+EY3 zcjoeZ$bYkjj|kuSC=9p|hXGg(gKIGu0HD@I;#62Qs;fme@(z5>fUY?oPUdbh$A6*b zGxQt7ln>JpauEKLIlgp&pa94BdNrEPrY0b2+TlCEFn0jH17zdyC$L2*@NYc;OKcYV zIQtMn8OVDp3Zf+L#yBGU5n-J`bVvs>@+SZs`6F0$fFsytw6?*ZIo}yKMxH%~8)9Yi zz^pukO6rD8F&+>4_i!&Nf5W2#@acv*$@9CSyn{BJ>gHRf+OG(7#GJu+l^wf*Jo zoU1{iD`n&sZa6eB`G%g|&AZ+uBVyBu+j)HxoS zm@?`dv|ZnrA-=$|kz>-^^vIXf`e7FIU3bRh3eT8LH&c**-QkOOn)~m9XbTtJlubGt z+z;-dymhfxXO6zEM!vY39(UiNhFQsN$tYm0YY09aV&*xVmLv0!@038@jpl!Q<%{*F zkiR|cV&_$0!!5A>hH4;8cRBOk%&FiWu?PjFIha%y_ zXN9Y6Sbsyymd6%GfLtQF**efa_~G-9Q#p+s6er+=PI+fM(D`pR9;*n8K`NmDc&rW| zLd8zC;FN|0aBjXn>w`!>%)mYlB-PhYh)6iC*WqAVgrQ*mGZO%KaIpXYPGbzX5QhOR zLM&aEgQb(Id5BL?oqUkSVUQdg1^~_%BA)=Ou>b<)!A-;o147{C_*R0r;hsPO4%`!< z2DehTngFgb4oZcKC8Wkd7|^2z08rz>m4czD_{0+H6%A~ literal 0 HcmV?d00001 From 17ab9c1c65b07f2c3a8e610cecd73602517d6974 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Thu, 5 Dec 2024 17:42:46 +0000 Subject: [PATCH 3/6] Temp changes for profiling --- app/playlistmodel.py | 33 +++++++++++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 977e3c4..6272830 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -92,7 +92,7 @@ class PlaylistModel(QAbstractTableModel): # Ensure row numbers in playlist are contiguous PlaylistRows.fixup_rownumbers(session, playlist_id) # Populate self.playlist_rows - self.refresh_data(session) + self.load_data(session) self.update_track_times() def __repr__(self) -> str: @@ -811,8 +811,10 @@ class PlaylistModel(QAbstractTableModel): self.update_track_times() self.invalidate_rows(list(row_map.keys())) + @line_profiler.profile def move_rows_between_playlists( - self, from_rows: list[int], to_row_number: int, to_playlist_id: int + self, from_rows: list[int], to_row_number: int, to_playlist_id: int, + dummy_for_profiling=None ) -> None: """ Move the playlist rows given to to_row and below of to_playlist. @@ -1010,6 +1012,33 @@ class PlaylistModel(QAbstractTableModel): # Copy to self.playlist_rows self.playlist_rows = new_playlist_rows + # Same as refresh data, but only used when creating playslit. + # Distinguishes profile time between initial load and other + # refreshes. + def load_data(self, session: db.session, dummy_for_profiling=None) -> None: + """Populate self.playlist_rows with playlist data""" + + # We used to clear self.playlist_rows each time but that's + # expensive and slow on big playlists + + # Note where each playlist_id is + plid_to_row: dict[int, int] = {} + for oldrow in self.playlist_rows: + plrdata = self.playlist_rows[oldrow] + plid_to_row[plrdata.playlistrow_id] = plrdata.row_number + + # build a new playlist_rows + new_playlist_rows: dict[int, RowAndTrack] = {} + for p in PlaylistRows.get_playlist_rows(session, self.playlist_id): + if p.id not in plid_to_row: + new_playlist_rows[p.row_number] = RowAndTrack(p) + else: + new_playlist_rows[p.row_number] = self.playlist_rows[plid_to_row[p.id]] + new_playlist_rows[p.row_number].row_number = p.row_number + + # Copy to self.playlist_rows + self.playlist_rows = new_playlist_rows + def refresh_row(self, session, row_number): """Populate dict for one row from database""" From eaac2ef4cadcf83924bde62ba1d760fbb2708a72 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 8 Dec 2024 17:00:22 +0000 Subject: [PATCH 4/6] Handle moving next track between playlists Fixes #266 --- app/classes.py | 12 ++ app/models.py | 9 +- app/musicmuster.py | 10 ++ app/playlistmodel.py | 18 +-- app/ui/main_window_ui.py | 300 ++++++++++++++++++++++++++++++--------- 5 files changed, 268 insertions(+), 81 deletions(-) diff --git a/app/classes.py b/app/classes.py index 5399dea..c9750fc 100644 --- a/app/classes.py +++ b/app/classes.py @@ -12,6 +12,7 @@ from typing import Any, Optional, NamedTuple # Third party imports import numpy as np import pyqtgraph as pg # type: ignore +from sqlalchemy.orm.session import Session import vlc # type: ignore # PyQt imports @@ -761,6 +762,17 @@ class RowAndTrack: self.fade_graph.tick(self.time_playing()) + def update_playlist_and_row(self, session: Session) -> None: + """ + Update local playlist_id and row_number from playlistrow_id + """ + + plr = session.get(PlaylistRows, self.playlistrow_id) + if not plr: + raise ApplicationError(f"(Can't retrieve PlaylistRows entry, {self=}") + self.playlist_id = plr.playlist_id + self.row_number = plr.row_number + @dataclass class TrackFileData: diff --git a/app/models.py b/app/models.py index 38ffa88..7cf73fc 100644 --- a/app/models.py +++ b/app/models.py @@ -479,9 +479,7 @@ class PlaylistRows(dbtables.PlaylistRowsTable): """ plrs = session.scalars( - select(cls) - .where(cls.playlist_id == playlist_id) - .order_by(cls.row_number) + select(cls).where(cls.playlist_id == playlist_id).order_by(cls.row_number) ).all() return plrs @@ -566,7 +564,10 @@ class PlaylistRows(dbtables.PlaylistRowsTable): @staticmethod @line_profiler.profile def update_plr_row_numbers( - session: Session, playlist_id: int, sqla_map: List[dict[str, int]], dummy_for_profiling=None + session: Session, + playlist_id: int, + sqla_map: List[dict[str, int]], + dummy_for_profiling=None, ) -> None: """ Take a {plrid: row_number} dictionary and update the row numbers accordingly diff --git a/app/musicmuster.py b/app/musicmuster.py index e48dd8a..d72be6b 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -965,6 +965,16 @@ class Window(QMainWindow, Ui_MainWindow): row_numbers, to_row, to_playlist_id ) + # Reset track_sequences + with db.Session() as session: + for ts in [ + track_sequence.next, + track_sequence.current, + track_sequence.previous, + ]: + if ts: + ts.update_playlist_and_row(session) + def move_selected(self) -> None: """ Move selected rows to another playlist diff --git a/app/playlistmodel.py b/app/playlistmodel.py index 6272830..afdf28b 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -746,7 +746,9 @@ class PlaylistModel(QAbstractTableModel): self.invalidate_rows(row_numbers) @line_profiler.profile - def move_rows(self, from_rows: list[int], to_row_number: int, dummy_for_profiling=None) -> None: + def move_rows( + self, from_rows: list[int], to_row_number: int, dummy_for_profiling=None + ) -> None: """ Move the playlist rows given to to_row and below. """ @@ -813,8 +815,11 @@ class PlaylistModel(QAbstractTableModel): @line_profiler.profile def move_rows_between_playlists( - self, from_rows: list[int], to_row_number: int, to_playlist_id: int, - dummy_for_profiling=None + self, + from_rows: list[int], + to_row_number: int, + to_playlist_id: int, + dummy_for_profiling=None, ) -> None: """ Move the playlist rows given to to_row and below of to_playlist. @@ -834,7 +839,6 @@ class PlaylistModel(QAbstractTableModel): self.signals.begin_reset_model_signal.emit(to_playlist_id) with db.Session() as session: - for row_group in row_groups: # Make room in destination playlist max_destination_row_number = PlaylistRows.get_last_used_row( @@ -1098,10 +1102,8 @@ class PlaylistModel(QAbstractTableModel): track_sequence.current, track_sequence.previous, ]: - if ts and ts.playlist_id == self.playlist_id and ts.row_number: - playlist_row = session.get(PlaylistRows, ts.playlistrow_id) - if playlist_row and playlist_row.row_number != ts.row_number: - ts.row_number = playlist_row.row_number + if ts: + ts.update_playlist_and_row(session) self.update_track_times() diff --git a/app/ui/main_window_ui.py b/app/ui/main_window_ui.py index b59a6f6..7b3c1b9 100644 --- a/app/ui/main_window_ui.py +++ b/app/ui/main_window_ui.py @@ -15,7 +15,11 @@ class Ui_MainWindow(object): MainWindow.resize(1280, 857) MainWindow.setMinimumSize(QtCore.QSize(1280, 0)) icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/icons/musicmuster"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon.addPixmap( + QtGui.QPixmap(":/icons/musicmuster"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) MainWindow.setWindowIcon(icon) MainWindow.setStyleSheet("") self.centralwidget = QtWidgets.QWidget(parent=MainWindow) @@ -27,39 +31,62 @@ class Ui_MainWindow(object): self.verticalLayout_3 = QtWidgets.QVBoxLayout() self.verticalLayout_3.setObjectName("verticalLayout_3") self.previous_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.previous_track_2.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.previous_track_2.sizePolicy().hasHeightForWidth() + ) self.previous_track_2.setSizePolicy(sizePolicy) self.previous_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) font = QtGui.QFont() font.setFamily("Sans") font.setPointSize(20) self.previous_track_2.setFont(font) - self.previous_track_2.setStyleSheet("background-color: #f8d7da;\n" -"border: 1px solid rgb(85, 87, 83);") - self.previous_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.previous_track_2.setStyleSheet( + "background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.previous_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.previous_track_2.setObjectName("previous_track_2") self.verticalLayout_3.addWidget(self.previous_track_2) self.current_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.current_track_2.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.current_track_2.sizePolicy().hasHeightForWidth() + ) self.current_track_2.setSizePolicy(sizePolicy) self.current_track_2.setMaximumSize(QtCore.QSize(230, 16777215)) font = QtGui.QFont() font.setFamily("Sans") font.setPointSize(20) self.current_track_2.setFont(font) - self.current_track_2.setStyleSheet("background-color: #d4edda;\n" -"border: 1px solid rgb(85, 87, 83);") - self.current_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.current_track_2.setStyleSheet( + "background-color: #d4edda;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.current_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.current_track_2.setObjectName("current_track_2") self.verticalLayout_3.addWidget(self.current_track_2) self.next_track_2 = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.next_track_2.sizePolicy().hasHeightForWidth()) @@ -69,19 +96,29 @@ class Ui_MainWindow(object): font.setFamily("Sans") font.setPointSize(20) self.next_track_2.setFont(font) - self.next_track_2.setStyleSheet("background-color: #fff3cd;\n" -"border: 1px solid rgb(85, 87, 83);") - self.next_track_2.setAlignment(QtCore.Qt.AlignmentFlag.AlignRight|QtCore.Qt.AlignmentFlag.AlignTrailing|QtCore.Qt.AlignmentFlag.AlignVCenter) + self.next_track_2.setStyleSheet( + "background-color: #fff3cd;\n" "border: 1px solid rgb(85, 87, 83);" + ) + self.next_track_2.setAlignment( + QtCore.Qt.AlignmentFlag.AlignRight + | QtCore.Qt.AlignmentFlag.AlignTrailing + | QtCore.Qt.AlignmentFlag.AlignVCenter + ) self.next_track_2.setObjectName("next_track_2") self.verticalLayout_3.addWidget(self.next_track_2) self.horizontalLayout_3.addLayout(self.verticalLayout_3) self.verticalLayout = QtWidgets.QVBoxLayout() self.verticalLayout.setObjectName("verticalLayout") self.hdrPreviousTrack = QtWidgets.QLabel(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hdrPreviousTrack.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.hdrPreviousTrack.sizePolicy().hasHeightForWidth() + ) self.hdrPreviousTrack.setSizePolicy(sizePolicy) self.hdrPreviousTrack.setMinimumSize(QtCore.QSize(0, 0)) self.hdrPreviousTrack.setMaximumSize(QtCore.QSize(16777215, 16777215)) @@ -89,32 +126,43 @@ class Ui_MainWindow(object): font.setFamily("Sans") font.setPointSize(20) self.hdrPreviousTrack.setFont(font) - self.hdrPreviousTrack.setStyleSheet("background-color: #f8d7da;\n" -"border: 1px solid rgb(85, 87, 83);") + self.hdrPreviousTrack.setStyleSheet( + "background-color: #f8d7da;\n" "border: 1px solid rgb(85, 87, 83);" + ) self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setWordWrap(False) self.hdrPreviousTrack.setObjectName("hdrPreviousTrack") self.verticalLayout.addWidget(self.hdrPreviousTrack) self.hdrCurrentTrack = QtWidgets.QPushButton(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.hdrCurrentTrack.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.hdrCurrentTrack.sizePolicy().hasHeightForWidth() + ) self.hdrCurrentTrack.setSizePolicy(sizePolicy) font = QtGui.QFont() font.setPointSize(20) self.hdrCurrentTrack.setFont(font) - self.hdrCurrentTrack.setStyleSheet("background-color: #d4edda;\n" -"border: 1px solid rgb(85, 87, 83);\n" -"text-align: left;\n" -"padding-left: 8px;\n" -"") + self.hdrCurrentTrack.setStyleSheet( + "background-color: #d4edda;\n" + "border: 1px solid rgb(85, 87, 83);\n" + "text-align: left;\n" + "padding-left: 8px;\n" + "" + ) self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setFlat(True) self.hdrCurrentTrack.setObjectName("hdrCurrentTrack") self.verticalLayout.addWidget(self.hdrCurrentTrack) self.hdrNextTrack = QtWidgets.QPushButton(parent=self.centralwidget) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.hdrNextTrack.sizePolicy().hasHeightForWidth()) @@ -122,10 +170,12 @@ class Ui_MainWindow(object): font = QtGui.QFont() font.setPointSize(20) self.hdrNextTrack.setFont(font) - self.hdrNextTrack.setStyleSheet("background-color: #fff3cd;\n" -"border: 1px solid rgb(85, 87, 83);\n" -"text-align: left;\n" -"padding-left: 8px;") + self.hdrNextTrack.setStyleSheet( + "background-color: #fff3cd;\n" + "border: 1px solid rgb(85, 87, 83);\n" + "text-align: left;\n" + "padding-left: 8px;" + ) self.hdrNextTrack.setText("") self.hdrNextTrack.setFlat(True) self.hdrNextTrack.setObjectName("hdrNextTrack") @@ -172,7 +222,12 @@ class Ui_MainWindow(object): self.cartsWidget.setObjectName("cartsWidget") self.horizontalLayout_Carts = QtWidgets.QHBoxLayout(self.cartsWidget) self.horizontalLayout_Carts.setObjectName("horizontalLayout_Carts") - spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Policy.Expanding, QtWidgets.QSizePolicy.Policy.Minimum) + spacerItem = QtWidgets.QSpacerItem( + 40, + 20, + QtWidgets.QSizePolicy.Policy.Expanding, + QtWidgets.QSizePolicy.Policy.Minimum, + ) self.horizontalLayout_Carts.addItem(spacerItem) self.gridLayout_4.addWidget(self.cartsWidget, 2, 0, 1, 1) self.frame_6 = QtWidgets.QFrame(parent=self.centralwidget) @@ -217,7 +272,11 @@ class Ui_MainWindow(object): self.btnPreview = QtWidgets.QPushButton(parent=self.FadeStopInfoFrame) self.btnPreview.setMinimumSize(QtCore.QSize(132, 41)) icon1 = QtGui.QIcon() - icon1.addPixmap(QtGui.QPixmap(":/icons/headphones"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon1.addPixmap( + QtGui.QPixmap(":/icons/headphones"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnPreview.setIcon(icon1) self.btnPreview.setIconSize(QtCore.QSize(30, 30)) self.btnPreview.setCheckable(True) @@ -239,8 +298,16 @@ class Ui_MainWindow(object): self.btnPreviewArm.setMaximumSize(QtCore.QSize(44, 23)) self.btnPreviewArm.setText("") icon2 = QtGui.QIcon() - icon2.addPixmap(QtGui.QPixmap(":/icons/record-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) - icon2.addPixmap(QtGui.QPixmap(":/icons/record-red-button.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) + icon2.addPixmap( + QtGui.QPixmap(":/icons/record-button.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) + icon2.addPixmap( + QtGui.QPixmap(":/icons/record-red-button.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.On, + ) self.btnPreviewArm.setIcon(icon2) self.btnPreviewArm.setCheckable(True) self.btnPreviewArm.setObjectName("btnPreviewArm") @@ -261,8 +328,16 @@ class Ui_MainWindow(object): self.btnPreviewMark.setMaximumSize(QtCore.QSize(44, 23)) self.btnPreviewMark.setText("") icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(":/icons/star.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.On) - icon3.addPixmap(QtGui.QPixmap(":/icons/star_empty.png"), QtGui.QIcon.Mode.Disabled, QtGui.QIcon.State.Off) + icon3.addPixmap( + QtGui.QPixmap(":/icons/star.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.On, + ) + icon3.addPixmap( + QtGui.QPixmap(":/icons/star_empty.png"), + QtGui.QIcon.Mode.Disabled, + QtGui.QIcon.State.Off, + ) self.btnPreviewMark.setIcon(icon3) self.btnPreviewMark.setObjectName("btnPreviewMark") self.btnPreviewFwd = QtWidgets.QPushButton(parent=self.groupBoxIntroControls) @@ -363,10 +438,15 @@ class Ui_MainWindow(object): self.verticalLayout_7.addWidget(self.label_silent_timer) self.horizontalLayout.addWidget(self.frame_silent) self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred) + sizePolicy = QtWidgets.QSizePolicy( + QtWidgets.QSizePolicy.Policy.Preferred, + QtWidgets.QSizePolicy.Policy.Preferred, + ) sizePolicy.setHorizontalStretch(1) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth()) + sizePolicy.setHeightForWidth( + self.widgetFadeVolume.sizePolicy().hasHeightForWidth() + ) self.widgetFadeVolume.setSizePolicy(sizePolicy) self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0)) self.widgetFadeVolume.setObjectName("widgetFadeVolume") @@ -383,7 +463,11 @@ class Ui_MainWindow(object): self.btnFade.setMinimumSize(QtCore.QSize(132, 32)) self.btnFade.setMaximumSize(QtCore.QSize(164, 16777215)) icon4 = QtGui.QIcon() - icon4.addPixmap(QtGui.QPixmap(":/icons/fade"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon4.addPixmap( + QtGui.QPixmap(":/icons/fade"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnFade.setIcon(icon4) self.btnFade.setIconSize(QtCore.QSize(30, 30)) self.btnFade.setObjectName("btnFade") @@ -391,7 +475,11 @@ class Ui_MainWindow(object): self.btnStop = QtWidgets.QPushButton(parent=self.frame) self.btnStop.setMinimumSize(QtCore.QSize(0, 36)) icon5 = QtGui.QIcon() - icon5.addPixmap(QtGui.QPixmap(":/icons/stopsign"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon5.addPixmap( + QtGui.QPixmap(":/icons/stopsign"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.btnStop.setIcon(icon5) self.btnStop.setObjectName("btnStop") self.verticalLayout_5.addWidget(self.btnStop) @@ -417,39 +505,71 @@ class Ui_MainWindow(object): MainWindow.setStatusBar(self.statusbar) self.actionPlay_next = QtGui.QAction(parent=MainWindow) icon6 = QtGui.QIcon() - icon6.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon6.addPixmap( + QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-play.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionPlay_next.setIcon(icon6) self.actionPlay_next.setObjectName("actionPlay_next") self.actionSkipToNext = QtGui.QAction(parent=MainWindow) icon7 = QtGui.QIcon() - icon7.addPixmap(QtGui.QPixmap(":/icons/next"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon7.addPixmap( + QtGui.QPixmap(":/icons/next"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionSkipToNext.setIcon(icon7) self.actionSkipToNext.setObjectName("actionSkipToNext") self.actionInsertTrack = QtGui.QAction(parent=MainWindow) icon8 = QtGui.QIcon() - icon8.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_search_database.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon8.addPixmap( + QtGui.QPixmap( + "app/ui/../../../../../../.designer/backup/icon_search_database.png" + ), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionInsertTrack.setIcon(icon8) self.actionInsertTrack.setObjectName("actionInsertTrack") self.actionAdd_file = QtGui.QAction(parent=MainWindow) icon9 = QtGui.QIcon() - icon9.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon_open_file.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon9.addPixmap( + QtGui.QPixmap( + "app/ui/../../../../../../.designer/backup/icon_open_file.png" + ), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionAdd_file.setIcon(icon9) self.actionAdd_file.setObjectName("actionAdd_file") self.actionFade = QtGui.QAction(parent=MainWindow) icon10 = QtGui.QIcon() - icon10.addPixmap(QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon10.addPixmap( + QtGui.QPixmap("app/ui/../../../../../../.designer/backup/icon-fade.png"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionFade.setIcon(icon10) self.actionFade.setObjectName("actionFade") self.actionStop = QtGui.QAction(parent=MainWindow) icon11 = QtGui.QIcon() - icon11.addPixmap(QtGui.QPixmap(":/icons/stop"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon11.addPixmap( + QtGui.QPixmap(":/icons/stop"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.actionStop.setIcon(icon11) self.actionStop.setObjectName("actionStop") self.action_Clear_selection = QtGui.QAction(parent=MainWindow) self.action_Clear_selection.setObjectName("action_Clear_selection") self.action_Resume_previous = QtGui.QAction(parent=MainWindow) icon12 = QtGui.QIcon() - icon12.addPixmap(QtGui.QPixmap(":/icons/previous"), QtGui.QIcon.Mode.Normal, QtGui.QIcon.State.Off) + icon12.addPixmap( + QtGui.QPixmap(":/icons/previous"), + QtGui.QIcon.Mode.Normal, + QtGui.QIcon.State.Off, + ) self.action_Resume_previous.setIcon(icon12) self.action_Resume_previous.setObjectName("action_Resume_previous") self.actionE_xit = QtGui.QAction(parent=MainWindow) @@ -496,7 +616,9 @@ class Ui_MainWindow(object): self.actionImport = QtGui.QAction(parent=MainWindow) self.actionImport.setObjectName("actionImport") self.actionDownload_CSV_of_played_tracks = QtGui.QAction(parent=MainWindow) - self.actionDownload_CSV_of_played_tracks.setObjectName("actionDownload_CSV_of_played_tracks") + self.actionDownload_CSV_of_played_tracks.setObjectName( + "actionDownload_CSV_of_played_tracks" + ) self.actionSearch = QtGui.QAction(parent=MainWindow) self.actionSearch.setObjectName("actionSearch") self.actionInsertSectionHeader = QtGui.QAction(parent=MainWindow) @@ -524,9 +646,13 @@ class Ui_MainWindow(object): self.actionResume = QtGui.QAction(parent=MainWindow) self.actionResume.setObjectName("actionResume") self.actionSearch_title_in_Wikipedia = QtGui.QAction(parent=MainWindow) - self.actionSearch_title_in_Wikipedia.setObjectName("actionSearch_title_in_Wikipedia") + self.actionSearch_title_in_Wikipedia.setObjectName( + "actionSearch_title_in_Wikipedia" + ) self.actionSearch_title_in_Songfacts = QtGui.QAction(parent=MainWindow) - self.actionSearch_title_in_Songfacts.setObjectName("actionSearch_title_in_Songfacts") + self.actionSearch_title_in_Songfacts.setObjectName( + "actionSearch_title_in_Songfacts" + ) self.actionSelect_duplicate_rows = QtGui.QAction(parent=MainWindow) self.actionSelect_duplicate_rows.setObjectName("actionSelect_duplicate_rows") self.actionReplace_files = QtGui.QAction(parent=MainWindow) @@ -581,7 +707,7 @@ class Ui_MainWindow(object): self.retranslateUi(MainWindow) self.tabPlaylist.setCurrentIndex(-1) self.tabInfolist.setCurrentIndex(-1) - self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore + self.actionE_xit.triggered.connect(MainWindow.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): @@ -623,38 +749,58 @@ class Ui_MainWindow(object): self.actionFade.setShortcut(_translate("MainWindow", "Ctrl+Z")) self.actionStop.setText(_translate("MainWindow", "S&top")) self.actionStop.setShortcut(_translate("MainWindow", "Ctrl+Alt+S")) - self.action_Clear_selection.setText(_translate("MainWindow", "Clear &selection")) + self.action_Clear_selection.setText( + _translate("MainWindow", "Clear &selection") + ) self.action_Clear_selection.setShortcut(_translate("MainWindow", "Esc")) - self.action_Resume_previous.setText(_translate("MainWindow", "&Resume previous")) + self.action_Resume_previous.setText( + _translate("MainWindow", "&Resume previous") + ) self.actionE_xit.setText(_translate("MainWindow", "E&xit")) self.actionTest.setText(_translate("MainWindow", "&Test")) self.actionOpenPlaylist.setText(_translate("MainWindow", "O&pen...")) self.actionNewPlaylist.setText(_translate("MainWindow", "&New...")) self.actionTestFunction.setText(_translate("MainWindow", "&Test function")) - self.actionSkipToFade.setText(_translate("MainWindow", "&Skip to start of fade")) + self.actionSkipToFade.setText( + _translate("MainWindow", "&Skip to start of fade") + ) self.actionSkipToEnd.setText(_translate("MainWindow", "Skip to &end of track")) self.actionClosePlaylist.setText(_translate("MainWindow", "&Close")) self.actionRenamePlaylist.setText(_translate("MainWindow", "&Rename...")) self.actionDeletePlaylist.setText(_translate("MainWindow", "Dele&te...")) - self.actionMoveSelected.setText(_translate("MainWindow", "Mo&ve selected tracks to...")) + self.actionMoveSelected.setText( + _translate("MainWindow", "Mo&ve selected tracks to...") + ) self.actionExport_playlist.setText(_translate("MainWindow", "E&xport...")) self.actionSetNext.setText(_translate("MainWindow", "Set &next")) self.actionSetNext.setShortcut(_translate("MainWindow", "Ctrl+N")) - self.actionSelect_next_track.setText(_translate("MainWindow", "Select next track")) + self.actionSelect_next_track.setText( + _translate("MainWindow", "Select next track") + ) self.actionSelect_next_track.setShortcut(_translate("MainWindow", "J")) - self.actionSelect_previous_track.setText(_translate("MainWindow", "Select previous track")) + self.actionSelect_previous_track.setText( + _translate("MainWindow", "Select previous track") + ) self.actionSelect_previous_track.setShortcut(_translate("MainWindow", "K")) - self.actionSelect_played_tracks.setText(_translate("MainWindow", "Select played tracks")) - self.actionMoveUnplayed.setText(_translate("MainWindow", "Move &unplayed tracks to...")) + self.actionSelect_played_tracks.setText( + _translate("MainWindow", "Select played tracks") + ) + self.actionMoveUnplayed.setText( + _translate("MainWindow", "Move &unplayed tracks to...") + ) self.actionAdd_note.setText(_translate("MainWindow", "Add note...")) self.actionAdd_note.setShortcut(_translate("MainWindow", "Ctrl+T")) self.actionEnable_controls.setText(_translate("MainWindow", "Enable controls")) self.actionImport.setText(_translate("MainWindow", "Import track...")) self.actionImport.setShortcut(_translate("MainWindow", "Ctrl+Shift+I")) - self.actionDownload_CSV_of_played_tracks.setText(_translate("MainWindow", "Download CSV of played tracks...")) + self.actionDownload_CSV_of_played_tracks.setText( + _translate("MainWindow", "Download CSV of played tracks...") + ) self.actionSearch.setText(_translate("MainWindow", "Search...")) self.actionSearch.setShortcut(_translate("MainWindow", "/")) - self.actionInsertSectionHeader.setText(_translate("MainWindow", "Insert §ion header...")) + self.actionInsertSectionHeader.setText( + _translate("MainWindow", "Insert §ion header...") + ) self.actionInsertSectionHeader.setShortcut(_translate("MainWindow", "Ctrl+H")) self.actionRemove.setText(_translate("MainWindow", "&Remove track")) self.actionFind_next.setText(_translate("MainWindow", "Find next")) @@ -662,8 +808,12 @@ class Ui_MainWindow(object): self.actionFind_previous.setText(_translate("MainWindow", "Find previous")) self.actionFind_previous.setShortcut(_translate("MainWindow", "P")) self.action_About.setText(_translate("MainWindow", "&About")) - self.actionSave_as_template.setText(_translate("MainWindow", "Save as template...")) - self.actionNew_from_template.setText(_translate("MainWindow", "New from template...")) + self.actionSave_as_template.setText( + _translate("MainWindow", "Save as template...") + ) + self.actionNew_from_template.setText( + _translate("MainWindow", "New from template...") + ) self.actionDebug.setText(_translate("MainWindow", "Debug")) self.actionAdd_cart.setText(_translate("MainWindow", "Edit cart &1...")) self.actionMark_for_moving.setText(_translate("MainWindow", "Mark for moving")) @@ -672,11 +822,23 @@ class Ui_MainWindow(object): self.actionPaste.setShortcut(_translate("MainWindow", "Ctrl+V")) self.actionResume.setText(_translate("MainWindow", "Resume")) self.actionResume.setShortcut(_translate("MainWindow", "Ctrl+R")) - self.actionSearch_title_in_Wikipedia.setText(_translate("MainWindow", "Search title in Wikipedia")) - self.actionSearch_title_in_Wikipedia.setShortcut(_translate("MainWindow", "Ctrl+W")) - self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts")) - self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S")) - self.actionSelect_duplicate_rows.setText(_translate("MainWindow", "Select duplicate rows...")) + self.actionSearch_title_in_Wikipedia.setText( + _translate("MainWindow", "Search title in Wikipedia") + ) + self.actionSearch_title_in_Wikipedia.setShortcut( + _translate("MainWindow", "Ctrl+W") + ) + self.actionSearch_title_in_Songfacts.setText( + _translate("MainWindow", "Search title in Songfacts") + ) + self.actionSearch_title_in_Songfacts.setShortcut( + _translate("MainWindow", "Ctrl+S") + ) + self.actionSelect_duplicate_rows.setText( + _translate("MainWindow", "Select duplicate rows...") + ) self.actionReplace_files.setText(_translate("MainWindow", "Import files...")) + + from infotabs import InfoTabs # type: ignore from pyqtgraph import PlotWidget # type: ignore From 417bff86630757032af924ec1634ecd301a5d506 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Sun, 8 Dec 2024 22:36:05 +0000 Subject: [PATCH 5/6] Put mark/move on context menu --- app/musicmuster.py | 26 +++++++++++++------------- app/playlists.py | 21 +++++++++++++++++++++ 2 files changed, 34 insertions(+), 13 deletions(-) diff --git a/app/musicmuster.py b/app/musicmuster.py index d72be6b..24556e7 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -461,7 +461,7 @@ class Window(QMainWindow, Ui_MainWindow): self.actionImport.triggered.connect(self.import_track) self.actionInsertSectionHeader.triggered.connect(self.insert_header) self.actionInsertTrack.triggered.connect(self.insert_track) - self.actionMark_for_moving.triggered.connect(self.cut_rows) + self.actionMark_for_moving.triggered.connect(self.mark_rows_for_moving) self.actionMoveSelected.triggered.connect(self.move_selected) self.actionNew_from_template.triggered.connect(self.new_from_template) self.actionNewPlaylist.triggered.connect(self.create_and_show_playlist) @@ -565,18 +565,6 @@ class Window(QMainWindow, Ui_MainWindow): log.debug(f"create_playlist_tab() returned: {idx=}") return idx - def cut_rows(self) -> None: - """ - Cut rows ready for pasting. - """ - - # Save the selected PlaylistRows items ready for a later - # paste - self.move_source_rows = self.active_tab().get_selected_rows() - self.move_source_model = self.active_proxy_model() - - log.debug(f"cut_rows(): {self.move_source_rows=} {self.move_source_model=}") - def debug(self): """Invoke debugger""" @@ -930,6 +918,18 @@ class Window(QMainWindow, Ui_MainWindow): self.signals.search_wikipedia_signal.emit(track_info.title) + def mark_rows_for_moving(self) -> None: + """ + Cut rows ready for pasting. + """ + + # Save the selected PlaylistRows items ready for a later + # paste + self.move_source_rows = self.active_tab().get_selected_rows() + self.move_source_model = self.active_proxy_model() + + log.debug(f"mark_rows_for_moving(): {self.move_source_rows=} {self.move_source_model=}") + def move_playlist_rows(self, row_numbers: List[int]) -> None: """ Move passed playlist rows to another playlist diff --git a/app/playlists.py b/app/playlists.py index f816660..35ca3c8 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -483,6 +483,13 @@ class PlaylistTab(QTableView): self._add_context_menu( "Rescan track", lambda: self._rescan(model_row_number) ) + self._add_context_menu( + "Mark for moving", lambda: self._mark_for_moving() + ) + if self.musicmuster.move_source_rows: + self._add_context_menu( + "Move selected rows here", lambda: self._move_selected_rows() + ) # ---------------------- self.menu.addSeparator() @@ -740,6 +747,20 @@ class PlaylistTab(QTableView): self.source_model.mark_unplayed(row_numbers) self.clear_selection() + def _mark_for_moving(self) -> None: + """ + Mark selected rows for pasting + """ + + self.musicmuster.mark_rows_for_moving() + + def _move_selected_rows(self) -> None: + """ + Move selected rows here + """ + + self.musicmuster.paste_rows() + def _open_in_audacity(self, row_number: int) -> None: """ Open track in passed row in Audacity From 558554d086b36e17d7ca0f1582a95f1bc794c097 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Mon, 9 Dec 2024 08:45:41 +0000 Subject: [PATCH 6/6] Implement "remove comments" Fixes #185 --- app/playlistmodel.py | 55 ++++++++++++++++++++++++++++++++++++++------ app/playlists.py | 14 +++++++++++ 2 files changed, 62 insertions(+), 7 deletions(-) diff --git a/app/playlistmodel.py b/app/playlistmodel.py index afdf28b..9f6db70 100644 --- a/app/playlistmodel.py +++ b/app/playlistmodel.py @@ -40,6 +40,7 @@ from classes import ( ) from config import Config from helpers import ( + ask_yes_no, file_is_unreadable, get_embedded_time, get_relative_date, @@ -67,7 +68,7 @@ class PlaylistModel(QAbstractTableModel): database refresh_data() will repopulate all of playlist_rows from the - database + database. """ def __init__( @@ -993,10 +994,17 @@ class PlaylistModel(QAbstractTableModel): @line_profiler.profile def refresh_data(self, session: db.session, dummy_for_profiling=None) -> None: - """Populate self.playlist_rows with playlist data""" + """ + Populate self.playlist_rows with playlist data + + We used to clear self.playlist_rows each time but that's + expensive and slow on big playlists. Instead we track where rows + are in database versus self.playlist_rows and fixup the latter. + This works well for news rows added and for rows moved, but + doesn't work for changed comments so they must be handled using + refresh_row(). + """ - # We used to clear self.playlist_rows each time but that's - # expensive and slow on big playlists # Note where each playlist_id is plid_to_row: dict[int, int] = {} @@ -1016,12 +1024,13 @@ class PlaylistModel(QAbstractTableModel): # Copy to self.playlist_rows self.playlist_rows = new_playlist_rows - # Same as refresh data, but only used when creating playslit. - # Distinguishes profile time between initial load and other - # refreshes. def load_data(self, session: db.session, dummy_for_profiling=None) -> None: """Populate self.playlist_rows with playlist data""" + # Same as refresh data, but only used when creating playslit. + # Distinguishes profile time between initial load and other + # refreshes. + # We used to clear self.playlist_rows each time but that's # expensive and slow on big playlists @@ -1107,6 +1116,38 @@ class PlaylistModel(QAbstractTableModel): self.update_track_times() + def remove_comments(self, row_numbers: list[int]) -> None: + """ + Remove comments from passed rows + """ + + if not row_numbers: + return + + # Safety check + if not ask_yes_no( + title="Remove comments", + question=f"Remove comments from {len(row_numbers)} rows?" + ): + return + + with db.Session() as session: + for row_number in row_numbers: + playlist_row = session.get( + PlaylistRows, self.playlist_rows[row_number].playlistrow_id + ) + if playlist_row.track_id: + playlist_row.note = "" + # We can't use refresh_data() because its + # optimisations mean it won't update comments in + # self.playlist_rows + # The "correct" approach would be to re-read from the + # database but we optimise here by simply updating + # self.playlist_rows directly. + self.playlist_rows[row_number].note = "" + session.commit() + self.invalidate_rows(row_numbers) + def _reversed_contiguous_row_groups( self, row_numbers: list[int] ) -> list[list[int]]: diff --git a/app/playlists.py b/app/playlists.py index 35ca3c8..bd1a566 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -505,6 +505,9 @@ class PlaylistTab(QTableView): lambda: proxy_model.remove_track(model_row_number), ) + # Remove comments + self._add_context_menu("Remove comments", lambda: self._remove_comments()) + # Add track to section header (ie, make this a track row) if header_row: self._add_context_menu("Add a track", lambda: self._add_track()) @@ -778,6 +781,17 @@ class PlaylistTab(QTableView): except ApplicationError as e: show_warning(self.musicmuster, "Audacity error", str(e)) + def _remove_comments(self) -> None: + """ + Remove comments from selected rows + """ + + row_numbers = self.selected_model_row_numbers() + if not row_numbers: + return + + self.source_model.remove_comments(row_numbers) + def _rescan(self, row_number: int) -> None: """Rescan track"""