Initial commit
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
__pycache__
|
||||||
|
*.py[cd]
|
194
mainwindow.py
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
from funksnake import Funkwhale
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets, QtNetwork
|
||||||
|
|
||||||
|
from ui_mainwindow import UiMainWindow
|
||||||
|
from settingsdialog import SettingsDialog
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadWorker(QtCore.QObject):
|
||||||
|
"""
|
||||||
|
The :class:`~threads.ThreadWorker` class provides a base class for thread workers.
|
||||||
|
|
||||||
|
In Qt5, all work is done in workers, rather than actual QThread classes, and this case class provides
|
||||||
|
some utility signals and slots to make it easier to use threads and thread workers.
|
||||||
|
"""
|
||||||
|
quit = QtCore.pyqtSignal()
|
||||||
|
error = QtCore.pyqtSignal(str, str)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""
|
||||||
|
The start method is how the worker runs. Basically, put your code here.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError('Your class needs to override this method and run self.quit.emit() when complete')
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumArtWorker(ThreadWorker):
|
||||||
|
album_item = None
|
||||||
|
artwork_url = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not self.album_item:
|
||||||
|
self.error.emit('No album QListWidgetItem set', 'The album_item attribute was not set')
|
||||||
|
self.quit.emit()
|
||||||
|
return
|
||||||
|
if not self.artwork_url:
|
||||||
|
self.error.emit('No artwork URL set', 'The artwork_url attribute was not set')
|
||||||
|
self.quit.emit()
|
||||||
|
return
|
||||||
|
reply = window.network_manager.get(QtNetwork.QNetworkRequest(QtCore.QUrl(self.artwork_url)))
|
||||||
|
while not reply.isFinished():
|
||||||
|
print(reply.error())
|
||||||
|
sleep(0.1)
|
||||||
|
print(reply)
|
||||||
|
if reply.error() == QtNetwork.QNetworkReply.NoError:
|
||||||
|
jpeg_data = reply.readAll()
|
||||||
|
pixmap = QtGui.QPixmap()
|
||||||
|
pixmap.loadFromData(jpeg_data)
|
||||||
|
self.albumListWidget.addItem(QtWidgets.QListWidgetItem(QtGui.QIcon(pixmap), album['title']))
|
||||||
|
else:
|
||||||
|
self.albumListWidget.addItem(QtWidgets.QListWidgetItem(album['title']))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class AlbumWorker(ThreadWorker):
|
||||||
|
"""
|
||||||
|
A thread worker to fetch the album art
|
||||||
|
"""
|
||||||
|
window = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if not self.window:
|
||||||
|
self.error.emit('No window set', 'The window attribute was not set')
|
||||||
|
self.quit.emit()
|
||||||
|
return
|
||||||
|
self.window.albumListWidget.clear()
|
||||||
|
for album in self.window.funkwhale.albums.list()['results']:
|
||||||
|
# TODO: Cache this
|
||||||
|
album_item = QtWidgets.QListWidgetItem(album['title'])
|
||||||
|
self.window.albumListWidget.addItem(album_item)
|
||||||
|
artwork_worker = AlbumArtWorker()
|
||||||
|
artwork_worker.album_item = album_item
|
||||||
|
artwork_worker.artwork_url = album['cover']['urls']['medium_square_crop']
|
||||||
|
self.window.run_thread(artwork_worker, 'album-{}'.format(album['title']))
|
||||||
|
self.quit.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
class MainWindow(QtWidgets.QMainWindow, UiMainWindow):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
self.settingsAction.triggered.connect(self.on_settings_action_triggered)
|
||||||
|
self.settings = QtCore.QSettings('info.snyman.pyfunkwhale', 'PyFunkwhale')
|
||||||
|
self.network_manager = QtNetwork.QNetworkAccessManager()
|
||||||
|
self.settings_dialog = SettingsDialog(self)
|
||||||
|
self.funkwhale = None
|
||||||
|
self.threads = {}
|
||||||
|
if self.settings.contains('funkwhale/server_url'):
|
||||||
|
self.setup_funkwhale(self.settings.value('funkwhale/server_url'),
|
||||||
|
self.settings.value('funkwhale/username', None),
|
||||||
|
self.settings.value('funkwhale/password', None))
|
||||||
|
self.load_albums()
|
||||||
|
self.load_artists()
|
||||||
|
|
||||||
|
def run_thread(self, worker, thread_name):
|
||||||
|
"""
|
||||||
|
Create a thread and its worker. This gets rid of a lot of unnecessary boilerplate code.
|
||||||
|
|
||||||
|
:param ThreadWorker worker: A worker object that actually performs the work
|
||||||
|
:param str thread_name: The name of the thread, used to keep track of the thread.
|
||||||
|
:param bool can_start: Start the thread. Defaults to True.
|
||||||
|
"""
|
||||||
|
if not thread_name:
|
||||||
|
raise ValueError('A thread name is required')
|
||||||
|
if thread_name in self.threads:
|
||||||
|
raise KeyError('A thread with name "{}" already exists'.format(thread_name))
|
||||||
|
# Create the thread and add the thread and the worker to the thread tracker
|
||||||
|
thread = QtCore.QThread()
|
||||||
|
self.threads[thread_name] = {
|
||||||
|
'thread': thread,
|
||||||
|
'worker': worker
|
||||||
|
}
|
||||||
|
# Move the worker into the thread's context
|
||||||
|
worker.moveToThread(thread)
|
||||||
|
# Connect slots and signals
|
||||||
|
thread.started.connect(worker.start)
|
||||||
|
worker.quit.connect(thread.quit)
|
||||||
|
worker.quit.connect(worker.deleteLater)
|
||||||
|
thread.finished.connect(thread.deleteLater)
|
||||||
|
thread.finished.connect(self.make_remove_thread(thread_name))
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
def get_thread_worker(self, thread_name):
|
||||||
|
"""
|
||||||
|
Get the worker by the thread name
|
||||||
|
|
||||||
|
:param str thread_name: The name of the thread
|
||||||
|
:returns: The worker for this thread name, or None
|
||||||
|
"""
|
||||||
|
thread = self.threads.get(thread_name)
|
||||||
|
if not thread:
|
||||||
|
return
|
||||||
|
return thread.get('worker')
|
||||||
|
|
||||||
|
def is_thread_finished(self, thread_name):
|
||||||
|
"""
|
||||||
|
Check if a thread is finished running.
|
||||||
|
|
||||||
|
:param str thread_name: The name of the thread
|
||||||
|
:returns: True if the thread is finished, False if it is still running
|
||||||
|
"""
|
||||||
|
return thread_name not in self.threads or self.threads[thread_name]['thread'].isFinished()
|
||||||
|
|
||||||
|
def make_remove_thread(self, thread_name):
|
||||||
|
"""
|
||||||
|
Create a function to remove the thread once the thread is finished.
|
||||||
|
|
||||||
|
:param str thread_name: The name of the thread which should be removed from the thread registry.
|
||||||
|
:returns: A function which will remove the thread from the thread registry.
|
||||||
|
"""
|
||||||
|
def remove_thread():
|
||||||
|
"""
|
||||||
|
Stop and remove a registered thread
|
||||||
|
|
||||||
|
:param str thread_name: The name of the thread to stop and remove
|
||||||
|
"""
|
||||||
|
if thread_name in self.threads:
|
||||||
|
del self.threads[thread_name]
|
||||||
|
|
||||||
|
return remove_thread
|
||||||
|
|
||||||
|
def setup_funkwhale(self, server_url=None, username=None, password=None):
|
||||||
|
if not self.funkwhale and not server_url:
|
||||||
|
raise ValueError('A server url needs to be defined')
|
||||||
|
if not self.funkwhale and server_url:
|
||||||
|
self.funkwhale = Funkwhale(server_url)
|
||||||
|
if username and password:
|
||||||
|
self.funkwhale.login(username, password)
|
||||||
|
|
||||||
|
def load_albums(self):
|
||||||
|
if not self.funkwhale or not self.funkwhale.token:
|
||||||
|
return
|
||||||
|
|
||||||
|
def load_artists(self):
|
||||||
|
if not self.funkwhale or not self.funkwhale.token:
|
||||||
|
return
|
||||||
|
self.artistListWidget.clear()
|
||||||
|
for artist in self.funkwhale.artists.list()['results']:
|
||||||
|
self.artistListWidget.addItem(QtWidgets.QListWidgetItem(artist['name']))
|
||||||
|
|
||||||
|
def on_settings_action_triggered(self):
|
||||||
|
self.settings_dialog.server_url = self.settings.value('funkwhale/server_url', '')
|
||||||
|
self.settings_dialog.username = self.settings.value('funkwhale/username', '')
|
||||||
|
self.settings_dialog.password = self.settings.value('funkwhale/password', '')
|
||||||
|
if self.settings_dialog.exec() == QtWidgets.QDialog.Accepted:
|
||||||
|
self.settings.setValue('funkwhale/server_url', self.settings_dialog.server_url)
|
||||||
|
self.settings.setValue('funkwhale/username', self.settings_dialog.username)
|
||||||
|
self.settings.setValue('funkwhale/password', self.settings_dialog.password)
|
||||||
|
self.setup_funkwhale(self.settings.value('funkwhale/server_url'),
|
||||||
|
self.settings.value('funkwhale/username', None),
|
||||||
|
self.settings.value('funkwhale/password', None))
|
329
mainwindow.ui
Normal file
@ -0,0 +1,329 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>MainWindow</class>
|
||||||
|
<widget class="QMainWindow" name="MainWindow">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>719</width>
|
||||||
|
<height>574</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>MainWindow</string>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="centralwidget">
|
||||||
|
<layout class="QVBoxLayout" name="centralLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QWidget" name="playbackWidget" native="true">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="minimumSize">
|
||||||
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>36</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<layout class="QHBoxLayout" name="playbackLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="toggleButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="expand"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="previousButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-skip-backward"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="playButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-playback-start"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="nextButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-skip-forward"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="positionLabel">
|
||||||
|
<property name="text">
|
||||||
|
<string>01:43</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="positionSlider">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="shuffleButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-playlist-normal"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="repeatButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="media-repeat-none"/>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="muteButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="player-volume"/>
|
||||||
|
</property>
|
||||||
|
<property name="checkable">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSlider" name="volumeSlider">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="menuButton">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset theme="menu_new"/>
|
||||||
|
</property>
|
||||||
|
<property name="popupMode">
|
||||||
|
<enum>QToolButton::DelayedPopup</enum>
|
||||||
|
</property>
|
||||||
|
<property name="autoRaise">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QSplitter" name="splitter">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="Minimum">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="handleWidth">
|
||||||
|
<number>1</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QListWidget" name="viewListWidget">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Expanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStackedWidget" name="stackedWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="albumPage">
|
||||||
|
<layout class="QVBoxLayout" name="albumPageLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="albumListWidget"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="artistPage">
|
||||||
|
<layout class="QVBoxLayout" name="artistPageLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="artistListWidget"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="trackPage">
|
||||||
|
<layout class="QVBoxLayout" name="trackPageLayout">
|
||||||
|
<property name="spacing">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="leftMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="topMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="rightMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<property name="bottomMargin">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<item>
|
||||||
|
<widget class="QListWidget" name="trackListWidget"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
<widget class="QListWidget" name="playlistListWidget">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QMenuBar" name="menubar">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>719</width>
|
||||||
|
<height>30</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
<widget class="QStatusBar" name="statusbar"/>
|
||||||
|
<action name="actionPlay">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Play</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Play</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
|
</widget>
|
||||||
|
<resources>
|
||||||
|
<include location="resources/pyfunkwhale.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections/>
|
||||||
|
</ui>
|
14
pyfunkwhale.py
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
from PyQt5 import QtWidgets
|
||||||
|
from mainwindow import MainWindow
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Main"""
|
||||||
|
app = QtWidgets.QApplication([])
|
||||||
|
main_window = MainWindow()
|
||||||
|
main_window.show()
|
||||||
|
return app.exec()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
14
resources/media-album-cover-manager-amarok.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="M 2 3 L 2 19 L 3 19 L 20 19 L 20 3 L 3 3 L 2 3 z M 3 4 L 4 4 L 4 18 L 3 18 L 3 4 z M 5 4 L 6 4 A 1 1 0 0 0 7 5 A 1 1 0 0 0 8 4 L 12 4 L 16 4 A 1 1 0 0 0 17 5 A 1 1 0 0 0 18 4 L 19 4 L 19 11 L 19 18 L 18 18 A 1 1 0 0 0 17 17 A 1 1 0 0 0 16 18 L 12 18 L 8 18 A 1 1 0 0 0 7 17 A 1 1 0 0 0 6 18 L 5 18 L 5 11 L 5 4 z M 5 11 A 7 7 0 0 0 12 18 A 7 7 0 0 0 19 11 A 7 7 0 0 0 12 4 A 7 7 0 0 0 5 11 z M 12 5 A 6 6 0 0 1 18 11 A 6 6 0 0 1 12 17 A 6 6 0 0 1 6 11 A 6 6 0 0 1 12 5 z M 12 10 A 1 1 0 0 0 11 11 A 1 1 0 0 0 12 12 A 1 1 0 0 0 13 11 A 1 1 0 0 0 12 10 z "
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 895 B |
14
resources/media-album-track.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="M 2 3 L 2 19 L 3 19 L 11 19 L 11 18 L 8 18 A 1 1 0 0 0 7 17 A 1 1 0 0 0 6 18 L 5 18 L 5 11 L 5 4 L 6 4 A 1 1 0 0 0 7 5 A 1 1 0 0 0 8 4 L 12 4 L 16 4 A 1 1 0 0 0 17 5 A 1 1 0 0 0 18 4 L 19 4 L 19 7 L 20 7 L 20 3 L 3 3 L 2 3 z M 12 4 A 7 7 0 0 0 5 11 A 7 7 0 0 0 11 17.921875 L 11 16.910156 A 6 6 0 0 1 6 11 A 6 6 0 0 1 12 5 A 6 6 0 0 1 16.462891 7 L 17.742188 7 A 7 7 0 0 0 12 4 z M 3 4 L 4 4 L 4 18 L 3 18 L 3 4 z M 16 8 L 16 9 L 16 14.5 C 15.5818 14.1852 15.066 14 14.5 14 C 13.115 14 12 15.115 12 16.5 C 12 17.885 13.115 19 14.5 19 C 15.885 19 17 17.885 17 16.5 L 17 10 L 19 10 C 19 8.892 18.108 8 17 8 L 16 8 z "
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 956 B |
8
resources/media-playback-pause.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m3 3v16h6v-16zm10 0v16h6v-16z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 291 B |
8
resources/media-playback-start.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m3 3v16l16-8z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 275 B |
8
resources/media-playback-stop.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m3 3h16v16h-16z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 277 B |
14
resources/media-playlist-append.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="M 4.5 3 C 3.6715787 3 3.0000083 3.6715704 3 4.5 C 3.0000083 5.3284296 3.6715787 6 4.5 6 C 5.3284213 6 5.9999917 5.3284296 6 4.5 C 5.9999917 3.6715704 5.3284213 3 4.5 3 z M 7 4 L 7 5 L 8 5 L 8 4 L 7 4 z M 9 4 L 9 5 L 19 5 L 19 4 L 9 4 z M 9 6 L 9 7 L 12 7 L 12 6 L 9 6 z M 4.5 9 C 3.6715732 8.9999988 3 9.6715721 3 10.5 C 3 11.328428 3.6715732 12.000001 4.5 12 C 5.3284268 12.000001 6 11.328428 6 10.5 C 6 9.6715721 5.3284268 8.9999988 4.5 9 z M 7 10 L 7 11 L 8 11 L 8 10 L 7 10 z M 9 10 L 9 11 L 19 11 L 19 10 L 9 10 z M 15 12 L 15 15 L 12 15 L 12 16 L 15 16 L 15 19 L 16 19 L 16 16 L 19 16 L 19 15 L 16 15 L 16 12 L 15 12 z M 4.5 15 C 3.6715732 14.999999 3 15.671572 3 16.5 C 3 17.328428 3.6715732 18.000001 4.5 18 C 5.3284268 18.000001 6 17.328428 6 16.5 C 6 15.671572 5.3284268 14.999999 4.5 15 z M 7 16 L 7 17 L 8 17 L 8 16 L 7 16 z "
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 1.1 KiB |
14
resources/media-playlist-normal.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="M 16 5 L 16 7 L 3 7 L 3 8 L 16 8 L 16 10 L 19 7.5 L 16 5 z M 16 12 L 16 14 L 3 14 L 3 15 L 16 15 L 16 17 L 19 14.5 L 16 12 z "
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 465 B |
10
resources/media-playlist-repeat.svg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<svg version="1.1" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<defs>
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path class="ColorScheme-Text" d="m16 5v2h-10c-1.662 0-3 1.338-3 3v1h1v-1c0-1.108 0.892-2 2-2h10v2l3-2.5zm2 6v1c0 1.108-0.892 2-2 2h-10v-2l-3 2.5 3 2.5v-2h10c1.662 0 3-1.338 3-3v-1z" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 462 B |
14
resources/media-playlist-shuffle.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="M 16 5 L 16 7 L 13 7 C 10.784 7 9 8.784 9 11 C 9 12.662 7.662 14 6 14 L 3 14 L 3 15 L 6 15 C 8.216 15 10 13.216 10 11 C 10 9.338 11.338 8 13 8 L 16 8 L 16 10 L 19 7.5 L 16 5 z M 3 7 L 3 8 L 6 8 C 6.8536128 8 7.6168144 8.3572581 8.1621094 8.9257812 C 8.2578781 8.5814037 8.3956215 8.2568349 8.5742188 7.9570312 C 7.8779048 7.3680394 6.9875937 7 6 7 L 3 7 z M 16 12 L 16 14 L 13 14 C 12.146387 14 11.383186 13.642742 10.837891 13.074219 C 10.742121 13.418597 10.604379 13.743165 10.425781 14.042969 C 11.122095 14.631961 12.012406 15 13 15 L 16 15 L 16 17 L 19 14.5 L 16 12 z "
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 914 B |
12
resources/media-repeat-none.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
.ColorScheme-NegativeText {
|
||||||
|
color:#da4453;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m16 5v2h-10c-1.662 0-3 1.338-3 3v1h1v-1c0-1.108.892-2 2-2h10v2l3-2.5zm-3 9h-7v-2l-3 2.5 3 2.5v-2h7z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
<path d="m14.5 11.79296875-.70703125.70703125 2 2-2 2 .70703125.70703125 2-2 2 2 .70703125-.70703125-2-2 2-2-.70703125-.70703125-2 2z" class="ColorScheme-NegativeText" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 628 B |
12
resources/media-repeat-single.svg
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g class="ColorScheme-Text" fill="currentColor">
|
||||||
|
<path d="m6 12-3 2.5 3 2.5v-2h9v-1h-9z"/>
|
||||||
|
<path d="m16 5v2h-10c-1.662 0-3 1.338-3 3v1h1v-1c0-1.108.892-2 2-2h10v2l3-2.5z"/>
|
||||||
|
<path d="m17.29296875 11-1.5 1.5.70703125.70703125.5-.5v3.29296875h-1v1h3v-1h-1v-5z" fill-rule="evenodd"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 517 B |
8
resources/media-seek-backward.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg6">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m11 3-10 8 10 8v-8zm0 8 10 8v-16z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 305 B |
8
resources/media-seek-forward.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg6">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m1 3v16l10-8zm10 8v8l10-8-10-8z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 303 B |
8
resources/media-skip-backward.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg6">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m0 3v16h2v-16zm2 8 10 8v-16zm10 0 10 8v-16z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 315 B |
8
resources/media-skip-forward.svg
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" id="svg6">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<path d="m0 3v16l10-8zm10 8v8l10-8-10-8zm10 0v8h2v-16h-2z" class="ColorScheme-Text" fill="currentColor"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 320 B |
14
resources/menu_new.svg
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22">
|
||||||
|
<defs id="defs3051">
|
||||||
|
<style type="text/css" id="current-color-scheme">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</defs>
|
||||||
|
<path
|
||||||
|
style="fill:currentColor;fill-opacity:1;stroke:none"
|
||||||
|
d="m3 5v2h16v-2h-16m0 5v2h16v-2h-16m0 5v2h16v-2h-16"
|
||||||
|
class="ColorScheme-Text"
|
||||||
|
/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 397 B |
23
resources/pyfunkwhale.qrc
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="playback">
|
||||||
|
<file>media-playback-start.svg</file>
|
||||||
|
<file>media-playback-pause.svg</file>
|
||||||
|
<file>media-playback-stop.svg</file>
|
||||||
|
<file>media-playlist-normal.svg</file>
|
||||||
|
<file>media-playlist-repeat.svg</file>
|
||||||
|
<file>media-playlist-shuffle.svg</file>
|
||||||
|
<file>media-repeat-none.svg</file>
|
||||||
|
<file>media-repeat-single.svg</file>
|
||||||
|
<file>media-seek-backward.svg</file>
|
||||||
|
<file>media-seek-forward.svg</file>
|
||||||
|
<file>media-skip-backward.svg</file>
|
||||||
|
<file>media-skip-forward.svg</file>
|
||||||
|
<file>media-album-track.svg</file>
|
||||||
|
<file>media-album-cover-manager-amarok.svg</file>
|
||||||
|
<file>media-playlist-append.svg</file>
|
||||||
|
</qresource>
|
||||||
|
<qresource prefix="general">
|
||||||
|
<file>menu_new.svg</file>
|
||||||
|
<file>speaker.svg</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
13
resources/speaker.svg
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<svg viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<style id="current-color-scheme" type="text/css">
|
||||||
|
.ColorScheme-Text {
|
||||||
|
color:#eff0f1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<g class="ColorScheme-Text" fill="currentColor">
|
||||||
|
<path d="m14.324219 7.28125-.539063.8613281a4 4 0 0 1 1.214844 2.8574219 4 4 0 0 1 -1.210938 2.861328l.539063.863281a5 5 0 0 0 1.671875-3.724609 5 5 0 0 0 -1.675781-3.71875z"/>
|
||||||
|
<path d="m13.865234 3.5371094-.24414.9765625a7 7 0 0 1 4.378906 6.4863281 7 7 0 0 1 -4.380859 6.478516l.24414.974609a8 8 0 0 0 5.136719-7.453125 8 8 0 0 0 -5.134766-7.4628906z"/>
|
||||||
|
<path d="m3 8h2v6h-2z" fill-rule="evenodd"/>
|
||||||
|
<path d="m6 14 5 5h1v-16h-1l-5 5z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 732 B |
33
settingsdialog.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
from PyQt5 import QtWidgets
|
||||||
|
|
||||||
|
from ui_settingsdialog import UiSettingsDialog
|
||||||
|
|
||||||
|
|
||||||
|
class SettingsDialog(QtWidgets.QDialog, UiSettingsDialog):
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setup_ui()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def server_url(self):
|
||||||
|
return self.server_edit.text()
|
||||||
|
|
||||||
|
@server_url.setter
|
||||||
|
def server_url(self, url):
|
||||||
|
self.server_edit.setText(url)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def username(self):
|
||||||
|
return self.username_edit.text()
|
||||||
|
|
||||||
|
@username.setter
|
||||||
|
def username(self, username):
|
||||||
|
self.username_edit.setText(username)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def password(self):
|
||||||
|
return self.password_edit.text()
|
||||||
|
|
||||||
|
@password.setter
|
||||||
|
def password(self, password):
|
||||||
|
self.password_edit.setText(password)
|
107
settingsdialog.ui
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>SettingsDialog</class>
|
||||||
|
<widget class="QDialog" name="SettingsDialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>391</width>
|
||||||
|
<height>202</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QGroupBox" name="server_group_box">
|
||||||
|
<property name="title">
|
||||||
|
<string>Server Details</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QFormLayout" name="server_layout">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="server_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Server URL</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QLineEdit" name="server_edit"/>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="username_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>Username</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLineEdit" name="username_edit"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Password</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QLineEdit" name="password_edit">
|
||||||
|
<property name="echoMode">
|
||||||
|
<enum>QLineEdit::Password</enum>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>SettingsDialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>SettingsDialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
6
threads.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
"""
|
||||||
|
The :mod:`threads` module contains functions to make working with QThreads easier
|
||||||
|
"""
|
||||||
|
from PyQt5 import QtCore
|
||||||
|
|
||||||
|
|
217
ui_mainwindow.py
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
|
|
||||||
|
LIST_WIDGET_STYLES = """
|
||||||
|
QListWidget {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class UiMainWindow(object):
|
||||||
|
def setup_ui(self):
|
||||||
|
self.setObjectName("MainWindow")
|
||||||
|
self.resize(719, 574)
|
||||||
|
self.setStyleSheet(LIST_WIDGET_STYLES)
|
||||||
|
self.centralwidget = QtWidgets.QWidget(self)
|
||||||
|
self.centralwidget.setObjectName("centralwidget")
|
||||||
|
self.centralLayout = QtWidgets.QVBoxLayout(self.centralwidget)
|
||||||
|
self.centralLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.centralLayout.setSpacing(0)
|
||||||
|
self.centralLayout.setObjectName("centralLayout")
|
||||||
|
self.playbackWidget = QtWidgets.QWidget(self.centralwidget)
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.playbackWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.playbackWidget.setSizePolicy(sizePolicy)
|
||||||
|
self.playbackWidget.setMinimumSize(QtCore.QSize(0, 36))
|
||||||
|
self.playbackWidget.setObjectName("playbackWidget")
|
||||||
|
self.playbackLayout = QtWidgets.QHBoxLayout(self.playbackWidget)
|
||||||
|
self.playbackLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.playbackLayout.setSpacing(0)
|
||||||
|
self.playbackLayout.setObjectName("playbackLayout")
|
||||||
|
self.toggleButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.toggleButton.setIcon(QtGui.QIcon.fromTheme("expand"))
|
||||||
|
self.toggleButton.setAutoRaise(True)
|
||||||
|
self.toggleButton.setObjectName("toggleButton")
|
||||||
|
self.playbackLayout.addWidget(self.toggleButton)
|
||||||
|
self.previousButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.previousButton.setIcon(QtGui.QIcon.fromTheme("media-skip-backward"))
|
||||||
|
self.previousButton.setAutoRaise(True)
|
||||||
|
self.previousButton.setObjectName("previousButton")
|
||||||
|
self.playbackLayout.addWidget(self.previousButton)
|
||||||
|
self.playButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.playButton.setIcon(QtGui.QIcon.fromTheme("media-playback-start"))
|
||||||
|
self.playButton.setAutoRaise(True)
|
||||||
|
self.playButton.setObjectName("playButton")
|
||||||
|
self.playbackLayout.addWidget(self.playButton)
|
||||||
|
self.nextButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.nextButton.setIcon(QtGui.QIcon.fromTheme("media-skip-forward"))
|
||||||
|
self.nextButton.setAutoRaise(True)
|
||||||
|
self.nextButton.setObjectName("nextButton")
|
||||||
|
self.playbackLayout.addWidget(self.nextButton)
|
||||||
|
self.positionLabel = QtWidgets.QLabel(self.playbackWidget)
|
||||||
|
self.positionLabel.setAlignment(QtCore.Qt.AlignCenter)
|
||||||
|
self.positionLabel.setObjectName("positionLabel")
|
||||||
|
self.playbackLayout.addWidget(self.positionLabel)
|
||||||
|
self.positionSlider = QtWidgets.QSlider(self.playbackWidget)
|
||||||
|
self.positionSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.positionSlider.setObjectName("positionSlider")
|
||||||
|
self.playbackLayout.addWidget(self.positionSlider)
|
||||||
|
self.shuffleButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.shuffleButton.setIcon(QtGui.QIcon.fromTheme("media-playlist-normal"))
|
||||||
|
self.shuffleButton.setAutoRaise(True)
|
||||||
|
self.shuffleButton.setObjectName("shuffleButton")
|
||||||
|
self.playbackLayout.addWidget(self.shuffleButton)
|
||||||
|
self.repeatButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.repeatButton.setIcon(QtGui.QIcon.fromTheme("media-repeat-none"))
|
||||||
|
self.repeatButton.setAutoRaise(True)
|
||||||
|
self.repeatButton.setObjectName("repeatButton")
|
||||||
|
self.playbackLayout.addWidget(self.repeatButton)
|
||||||
|
self.muteButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.muteButton.setIcon(QtGui.QIcon.fromTheme("player-volume"))
|
||||||
|
self.muteButton.setCheckable(True)
|
||||||
|
self.muteButton.setAutoRaise(True)
|
||||||
|
self.muteButton.setObjectName("muteButton")
|
||||||
|
self.playbackLayout.addWidget(self.muteButton)
|
||||||
|
self.volumeSlider = QtWidgets.QSlider(self.playbackWidget)
|
||||||
|
self.volumeSlider.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.volumeSlider.setObjectName("volumeSlider")
|
||||||
|
self.playbackLayout.addWidget(self.volumeSlider)
|
||||||
|
self.menuButton = QtWidgets.QToolButton(self.playbackWidget)
|
||||||
|
self.menuButton.setIcon(QtGui.QIcon.fromTheme("application-menu"))
|
||||||
|
self.menuButton.setPopupMode(QtWidgets.QToolButton.DelayedPopup)
|
||||||
|
self.menuButton.setAutoRaise(True)
|
||||||
|
self.menuButton.setObjectName("menuButton")
|
||||||
|
self.playbackLayout.addWidget(self.menuButton)
|
||||||
|
self.centralLayout.addWidget(self.playbackWidget)
|
||||||
|
self.splitter = QtWidgets.QSplitter(self.centralwidget)
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.splitter.sizePolicy().hasHeightForWidth())
|
||||||
|
self.splitter.setSizePolicy(sizePolicy)
|
||||||
|
self.splitter.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.splitter.setHandleWidth(1)
|
||||||
|
self.splitter.setObjectName("splitter")
|
||||||
|
self.viewListWidget = QtWidgets.QListWidget(self.splitter)
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.viewListWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.viewListWidget.setSizePolicy(sizePolicy)
|
||||||
|
self.viewListWidget.setObjectName("viewListWidget")
|
||||||
|
self.stackedWidget = QtWidgets.QStackedWidget(self.splitter)
|
||||||
|
self.stackedWidget.setObjectName("stackedWidget")
|
||||||
|
self.albumPage = QtWidgets.QWidget()
|
||||||
|
self.albumPage.setObjectName("albumPage")
|
||||||
|
self.albumPageLayout = QtWidgets.QVBoxLayout(self.albumPage)
|
||||||
|
self.albumPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.albumPageLayout.setSpacing(0)
|
||||||
|
self.albumPageLayout.setObjectName("albumPageLayout")
|
||||||
|
self.albumPageTitleLayout = QtWidgets.QHBoxLayout()
|
||||||
|
self.albumPageTitleLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.albumPageTitleLayout.setSpacing(0)
|
||||||
|
self.albumPageTitleLayout.setObjectName("albumPageTitleLayout")
|
||||||
|
self.albumPageIconLabel = QtWidgets.QLabel(self.albumPage)
|
||||||
|
self.albumPageIconLabel.setPixmap(
|
||||||
|
QtGui.QIcon.fromTheme('view-media-album-cover').pixmap(self.albumPageIconLabel.geometry().height()))
|
||||||
|
self.albumPageTitleLayout.addWidget(self.albumPageIconLabel)
|
||||||
|
self.albumPageTitleLabel = QtWidgets.QLabel(self.albumPage)
|
||||||
|
self.albumPageTitleLayout.addWidget(self.albumPageTitleLabel)
|
||||||
|
self.albumPageLayout.addLayout(self.albumPageTitleLayout)
|
||||||
|
self.albumListWidget = QtWidgets.QListWidget(self.albumPage)
|
||||||
|
self.albumListWidget.setObjectName("albumListWidget")
|
||||||
|
self.albumListWidget.setSortingEnabled(True)
|
||||||
|
# self.albumListWidget.setViewMode(QtWidgets.QListView.IconMode)
|
||||||
|
# self.albumListWidget.setMovement(QtWidgets.QListView.Static)
|
||||||
|
# self.albumListWidget.setGridSize(QtCore.QSize(128, 128))
|
||||||
|
self.albumPageLayout.addWidget(self.albumListWidget)
|
||||||
|
self.stackedWidget.addWidget(self.albumPage)
|
||||||
|
self.artistPage = QtWidgets.QWidget()
|
||||||
|
self.artistPage.setObjectName("artistPage")
|
||||||
|
self.artistPageLayout = QtWidgets.QVBoxLayout(self.artistPage)
|
||||||
|
self.artistPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.artistPageLayout.setSpacing(0)
|
||||||
|
self.artistPageLayout.setObjectName("artistPageLayout")
|
||||||
|
self.artistListWidget = QtWidgets.QListWidget(self.artistPage)
|
||||||
|
self.artistListWidget.setObjectName("artistListWidget")
|
||||||
|
self.artistListWidget.setSortingEnabled(True)
|
||||||
|
self.artistListWidget.setViewMode(QtWidgets.QListView.IconMode)
|
||||||
|
self.artistListWidget.setMovement(QtWidgets.QListView.Static)
|
||||||
|
self.artistPageLayout.addWidget(self.artistListWidget)
|
||||||
|
self.stackedWidget.addWidget(self.artistPage)
|
||||||
|
self.trackPage = QtWidgets.QWidget()
|
||||||
|
self.trackPage.setObjectName("trackPage")
|
||||||
|
self.trackPageLayout = QtWidgets.QVBoxLayout(self.trackPage)
|
||||||
|
self.trackPageLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.trackPageLayout.setSpacing(0)
|
||||||
|
self.trackPageLayout.setObjectName("trackPageLayout")
|
||||||
|
self.trackListWidget = QtWidgets.QListWidget(self.trackPage)
|
||||||
|
self.trackListWidget.setObjectName("trackListWidget")
|
||||||
|
self.trackListWidget.setSortingEnabled(True)
|
||||||
|
self.trackListWidget.setViewMode(QtWidgets.QListView.IconMode)
|
||||||
|
self.trackListWidget.setMovement(QtWidgets.QListView.Static)
|
||||||
|
self.trackPageLayout.addWidget(self.trackListWidget)
|
||||||
|
self.stackedWidget.addWidget(self.trackPage)
|
||||||
|
self.playlistListWidget = QtWidgets.QListWidget(self.splitter)
|
||||||
|
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding)
|
||||||
|
sizePolicy.setHorizontalStretch(0)
|
||||||
|
sizePolicy.setVerticalStretch(0)
|
||||||
|
sizePolicy.setHeightForWidth(self.playlistListWidget.sizePolicy().hasHeightForWidth())
|
||||||
|
self.playlistListWidget.setSizePolicy(sizePolicy)
|
||||||
|
self.playlistListWidget.setObjectName("playlistListWidget")
|
||||||
|
self.centralLayout.addWidget(self.splitter)
|
||||||
|
self.setCentralWidget(self.centralwidget)
|
||||||
|
self.statusbar = QtWidgets.QStatusBar(self)
|
||||||
|
self.statusbar.setObjectName("statusbar")
|
||||||
|
self.setStatusBar(self.statusbar)
|
||||||
|
self.actionPlay = QtWidgets.QAction(self)
|
||||||
|
self.actionPlay.setObjectName("actionPlay")
|
||||||
|
|
||||||
|
self.mainMenu = QtWidgets.QMenu(self)
|
||||||
|
self.settingsAction = self.mainMenu.addAction(QtGui.QIcon.fromTheme('configure'), '')
|
||||||
|
|
||||||
|
self.albumsListItem = QtWidgets.QListWidgetItem(QtGui.QIcon.fromTheme('view-media-album-cover'), '')
|
||||||
|
self.artistsListItem = QtWidgets.QListWidgetItem(QtGui.QIcon.fromTheme('view-media-artist'), '')
|
||||||
|
self.tracksListItem = QtWidgets.QListWidgetItem(QtGui.QIcon.fromTheme('view-media-track'), '')
|
||||||
|
for item in [self.albumsListItem, self.artistsListItem, self.tracksListItem]:
|
||||||
|
self.viewListWidget.addItem(item)
|
||||||
|
|
||||||
|
self.splitter.setStretchFactor(1, 1)
|
||||||
|
self.retranslate_ui()
|
||||||
|
self.menuButton.clicked.connect(self.on_menu_button_clicked)
|
||||||
|
self.viewListWidget.currentRowChanged.connect(self.on_view_list_current_row_changed)
|
||||||
|
self.viewListWidget.setCurrentRow(0)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(self)
|
||||||
|
|
||||||
|
self.viewListWidget.setFocus()
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
self.setWindowTitle(_translate("MainWindow", "PyFunkwhale"))
|
||||||
|
self.toggleButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.previousButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.playButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.nextButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.positionLabel.setText(_translate("MainWindow", "01:43"))
|
||||||
|
self.shuffleButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.repeatButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.muteButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.menuButton.setText(_translate("MainWindow", "..."))
|
||||||
|
self.actionPlay.setText(_translate("MainWindow", "&Play"))
|
||||||
|
self.actionPlay.setToolTip(_translate("MainWindow", "Play"))
|
||||||
|
self.settingsAction.setText(_translate("MainWindow", "&Configure"))
|
||||||
|
self.albumsListItem.setText(_translate("MainWindow", "Albums"))
|
||||||
|
self.albumPageTitleLabel.setText(_translate("MainWindow", "Albums"))
|
||||||
|
self.artistsListItem.setText(_translate("MainWindow", "Artists"))
|
||||||
|
self.tracksListItem.setText(_translate("MainWindow", "Tracks"))
|
||||||
|
|
||||||
|
def on_menu_button_clicked(self):
|
||||||
|
pos = self.mapToGlobal(self.menuButton.geometry().bottomRight())
|
||||||
|
pos.setX(pos.x() - self.mainMenu.geometry().width())
|
||||||
|
self.mainMenu.exec(pos)
|
||||||
|
|
||||||
|
def on_view_list_current_row_changed(self, current_row):
|
||||||
|
self.stackedWidget.setCurrentIndex(current_row)
|
52
ui_settingsdialog.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
from PyQt5 import QtCore, QtWidgets
|
||||||
|
|
||||||
|
|
||||||
|
class UiSettingsDialog(object):
|
||||||
|
def setup_ui(self):
|
||||||
|
self.setObjectName("SettingsDialog")
|
||||||
|
self.resize(391, 202)
|
||||||
|
self.verticalLayout = QtWidgets.QVBoxLayout(self)
|
||||||
|
self.verticalLayout.setObjectName("verticalLayout")
|
||||||
|
self.server_group_box = QtWidgets.QGroupBox(self)
|
||||||
|
self.server_group_box.setObjectName("server_group_box")
|
||||||
|
self.server_layout = QtWidgets.QFormLayout(self.server_group_box)
|
||||||
|
self.server_layout.setObjectName("server_layout")
|
||||||
|
self.server_label = QtWidgets.QLabel(self.server_group_box)
|
||||||
|
self.server_label.setObjectName("server_label")
|
||||||
|
self.server_layout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.server_label)
|
||||||
|
self.server_edit = QtWidgets.QLineEdit(self.server_group_box)
|
||||||
|
self.server_edit.setObjectName("server_edit")
|
||||||
|
self.server_layout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.server_edit)
|
||||||
|
self.username_label = QtWidgets.QLabel(self.server_group_box)
|
||||||
|
self.username_label.setObjectName("username_label")
|
||||||
|
self.server_layout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.username_label)
|
||||||
|
self.username_edit = QtWidgets.QLineEdit(self.server_group_box)
|
||||||
|
self.username_edit.setObjectName("username_edit")
|
||||||
|
self.server_layout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.username_edit)
|
||||||
|
self.label_3 = QtWidgets.QLabel(self.server_group_box)
|
||||||
|
self.label_3.setObjectName("label_3")
|
||||||
|
self.server_layout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.label_3)
|
||||||
|
self.password_edit = QtWidgets.QLineEdit(self.server_group_box)
|
||||||
|
self.password_edit.setEchoMode(QtWidgets.QLineEdit.Password)
|
||||||
|
self.password_edit.setObjectName("password_edit")
|
||||||
|
self.server_layout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.password_edit)
|
||||||
|
self.verticalLayout.addWidget(self.server_group_box)
|
||||||
|
self.buttonBox = QtWidgets.QDialogButtonBox(self)
|
||||||
|
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
|
||||||
|
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
|
||||||
|
self.buttonBox.setObjectName("buttonBox")
|
||||||
|
self.verticalLayout.addWidget(self.buttonBox)
|
||||||
|
|
||||||
|
self.retranslate_ui()
|
||||||
|
self.buttonBox.accepted.connect(self.accept)
|
||||||
|
self.buttonBox.rejected.connect(self.reject)
|
||||||
|
QtCore.QMetaObject.connectSlotsByName(self)
|
||||||
|
|
||||||
|
def retranslate_ui(self):
|
||||||
|
_translate = QtCore.QCoreApplication.translate
|
||||||
|
self.setWindowTitle(_translate("SettingsDialog", "Settings"))
|
||||||
|
self.server_group_box.setTitle(_translate("SettingsDialog", "Server Details"))
|
||||||
|
self.server_label.setText(_translate("SettingsDialog", "Server URL"))
|
||||||
|
self.username_label.setText(_translate("SettingsDialog", "Username"))
|
||||||
|
self.label_3.setText(_translate("SettingsDialog", "Password"))
|