1
0
mirror of https://github.com/tomahawk-player/tomahawk.git synced 2025-03-25 02:09:48 +01:00

Add spotify context menu action for local playlists

This also adds a generic way for runtime components to register actions to
be shown for a certain category of items (e.g. playlists, tracks, artists, etc).
Initiating a sync from Tomahawk is still a TODO
This commit is contained in:
Leo Franchi 2012-04-08 22:43:11 -04:00
parent 6ffaa1450e
commit 03c57d3a0f
9 changed files with 430 additions and 25 deletions

View File

@ -19,13 +19,17 @@
#include "SpotifyAccount.h"
#include "playlist.h"
#include "utils/tomahawkutils.h"
#include "playlist/PlaylistUpdaterInterface.h"
#include "sourcelist.h"
#include "SpotifyAccountConfig.h"
#include "SpotifyPlaylistUpdater.h"
#include "resolvers/scriptresolver.h"
#include "utils/tomahawkutils.h"
#include "actioncollection.h"
#include <QPixmap>
#include <QAction>
using namespace Tomahawk;
using namespace Accounts;
@ -95,6 +99,83 @@ SpotifyAccount::init()
msg[ "_msgtype" ] = "getCredentials";
m_spotifyResolver.data()->sendMessage( msg );
}
QAction* action = new QAction( 0 );
action->setIcon( QIcon( RESPATH "images/spotify-logo.png" ) );
connect( action, SIGNAL( triggered( bool ) ), this, SLOT( syncActionTriggered( bool ) ) );
ActionCollection::instance()->addAction( ActionCollection::LocalPlaylists, action, this );
m_customActions.append( action );
}
void
SpotifyAccount::aboutToShow( QAction* action, const playlist_ptr& playlist )
{
if ( !m_customActions.contains( action ) )
return;
// If it's not being synced, allow the option to sync
SpotifyPlaylistUpdater* updater = qobject_cast< SpotifyPlaylistUpdater* >( playlist->updater() );
if ( !updater || !updater->sync() )
{
action->setText( tr( "Sync with Spotify" ) );
}
else
{
action->setText( tr( "Stop syncing with Spotify" ) );
}
}
void
SpotifyAccount::syncActionTriggered( bool checked )
{
Q_UNUSED( checked );
QAction* action = qobject_cast< QAction* >( sender() );
if ( !action || !m_customActions.contains( action ) )
return;
const playlist_ptr playlist = action->property( "payload" ).value< playlist_ptr >();
if ( playlist.isNull() )
{
qWarning() << "Got context menu spotify sync action triggered, but invalid playlist payload!";
Q_ASSERT( false );
return;
}
SpotifyPlaylistUpdater* updater = qobject_cast< SpotifyPlaylistUpdater* >( playlist->updater() );
if ( !updater )
{
// TODO
}
else
{
SpotifyPlaylistInfo* info = 0;
foreach ( SpotifyPlaylistInfo* ifo, m_allSpotifyPlaylists )
{
if ( ifo->plid == updater->spotifyId() )
{
info = ifo;
break;
}
}
if ( !updater->sync() )
{
info->sync = true;
if ( m_configWidget.data() )
m_configWidget.data()->setPlaylists( m_allSpotifyPlaylists );
startPlaylistSync( info );
}
else
{
stopPlaylistSync( info, true );
}
}
}
@ -303,12 +384,7 @@ SpotifyAccount::saveConfig()
if ( pl->sync )
{
// Fetch full playlist contents, then begin the sync
QVariantMap msg;
msg[ "_msgtype" ] = "getPlaylist";
msg[ "playlistid" ] = pl->plid;
msg[ "sync" ] = pl->sync;
sendMessage( msg, this, "startPlaylistSyncWithPlaylist" );
startPlaylistSync( pl );
}
else
stopPlaylistSync( pl );
@ -318,6 +394,18 @@ SpotifyAccount::saveConfig()
}
void
SpotifyAccount::startPlaylistSync( SpotifyPlaylistInfo* playlist )
{
QVariantMap msg;
msg[ "_msgtype" ] = "getPlaylist";
msg[ "playlistid" ] = playlist->plid;
msg[ "sync" ] = playlist->sync;
sendMessage( msg, this, "startPlaylistSyncWithPlaylist" );
}
void
SpotifyAccount::startPlaylistSyncWithPlaylist( const QString& msgType, const QVariantMap& msg )
{
@ -409,7 +497,7 @@ SpotifyAccount::deleteOnUnsync() const
}
void
SpotifyAccount::stopPlaylistSync( SpotifyPlaylistInfo* playlist )
SpotifyAccount::stopPlaylistSync( SpotifyPlaylistInfo* playlist, bool forceDontDelete )
{
QVariantMap msg;
msg[ "_msgtype" ] = "removeFromSyncList";
@ -422,7 +510,7 @@ SpotifyAccount::stopPlaylistSync( SpotifyPlaylistInfo* playlist )
SpotifyPlaylistUpdater* updater = m_updaters[ playlist->plid ];
updater->setSync( false );
if ( deleteOnUnsync() )
if ( deleteOnUnsync() && !forceDontDelete )
{
playlist_ptr tomahawkPl = updater->playlist();

View File

@ -20,11 +20,13 @@
#ifndef SpotifyAccount_H
#define SpotifyAccount_H
#include "accounts/ResolverAccount.h"
#include "sourcelist.h"
#include "playlist.h"
#include "utils/tomahawkutils.h"
#include "sourcelist.h"
#include "accounts/ResolverAccount.h"
#include "utils/SmartPointerList.h"
class QAction;
class SpotifyPlaylistUpdater;
class QTimer;
@ -91,6 +93,11 @@ public:
void unregisterUpdater( const QString& plid );
bool deleteOnUnsync() const;
public slots:
void aboutToShow( QAction* action, const Tomahawk::playlist_ptr& playlist );
void syncActionTriggered( bool );
private slots:
void resolverMessage( const QString& msgType, const QVariantMap& msg );
@ -102,7 +109,8 @@ private:
void init();
void loadPlaylists();
void stopPlaylistSync( SpotifyPlaylistInfo* playlist );
void startPlaylistSync( SpotifyPlaylistInfo* playlist );
void stopPlaylistSync( SpotifyPlaylistInfo* playlist, bool forceDontDelete = false );
void fetchFullPlaylist( SpotifyPlaylistInfo* playlist );
void setSyncForPlaylist( const QString& spotifyPlaylistId, bool sync );
@ -116,6 +124,7 @@ private:
QList< SpotifyPlaylistInfo* > m_allSpotifyPlaylists;
QHash< QString, SpotifyPlaylistUpdater* > m_updaters;
SmartPointerList< QAction > m_customActions;
friend class ::SpotifyPlaylistUpdater;
};

View File

@ -51,6 +51,8 @@ public:
bool sync() const;
void setSync( bool sync );
QString spotifyId() const { return m_spotifyId; }
/// Spotify callbacks when we are directly instructed from the resolver
void spotifyTracksAdded( const QVariantList& tracks, const QString& startPosId, const QString& newRev, const QString& oldRev );
void spotifyTracksRemoved( const QVariantList& tracks, const QString& newRev, const QString& oldRev );

View File

@ -121,6 +121,7 @@ set( libGuiSources
utils/tomahawkutilsgui.cpp
utils/closure.cpp
utils/PixmapDelegateFader.cpp
utils/SmartPointerList.h
widgets/animatedcounterlabel.cpp
widgets/checkdirtree.cpp

View File

@ -2,6 +2,7 @@
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -37,6 +38,14 @@ ActionCollection::ActionCollection( QObject *parent )
}
ActionCollection::~ActionCollection()
{
s_instance = 0;
foreach( QString key, m_actionCollection.keys() )
delete m_actionCollection[ key ];
}
void
ActionCollection::initActions()
{
@ -76,18 +85,54 @@ ActionCollection::initActions()
}
ActionCollection::~ActionCollection()
void
ActionCollection::addAction( ActionCollection::ActionDestination category, QAction* action, QObject* notify )
{
s_instance = 0;
foreach( QString key, m_actionCollection.keys() )
delete m_actionCollection[ key ];
QList< QAction* > actions = m_categoryActions.value( category );
actions.append( action );
m_categoryActions[ category ] = actions;
if ( notify )
m_actionNotifiers[ action ] = notify;
}
QAction*
ActionCollection::getAction( const QString& name )
{
return m_actionCollection.contains( name ) ? m_actionCollection[ name ] : 0;
return m_actionCollection.value( name, 0 );
}
QObject*
ActionCollection::actionNotifier( QAction* action )
{
return m_actionNotifiers.value( action, 0 );
}
QList< QAction* >
ActionCollection::getAction( ActionCollection::ActionDestination category )
{
return m_categoryActions.value( category );
}
void
ActionCollection::removeAction( QAction* action )
{
removeAction( action, LocalPlaylists );
}
void
ActionCollection::removeAction( QAction* action, ActionCollection::ActionDestination category )
{
QList< QAction* > actions = m_categoryActions.value( category );
actions.removeAll( action );
m_categoryActions[ category ] = actions;
m_actionNotifiers.remove( action );
}

View File

@ -1,7 +1,8 @@
/* === This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
*
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org>
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>
* Copyright 2010-2011, Christian Muehlhaeuser <muesli@tomahawk-player.org
* Copyright 2010-2012, Jeff Mitchell <jeff@tomahawk-player.org>>
* Copyright 2012, Leo Franchi <lfranchi@kde.org>
*
* Tomahawk is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@ -29,6 +30,12 @@ class DLLEXPORT ActionCollection : public QObject
Q_OBJECT
public:
// Categories for custom-registered actions
enum ActionDestination {
// Tracks, TODO
LocalPlaylists = 0
};
static ActionCollection* instance();
ActionCollection( QObject *parent);
@ -37,6 +44,34 @@ public:
void initActions();
QAction* getAction( const QString& name );
QList< QAction* > getAction( ActionDestination category );
QObject* actionNotifier( QAction* );
/**
* Add an action for a specific category. The action will show up
* where the relevant category is displayed.
*
* e.g. if you register a Playlist action, it will be shown when
* there is a context menu shown for a playlist.
*
* When the QAction* is shown, it will have a "payload" property that is set
* to the <specific type> that is being shown.
*
* Additionally you can pass a QObject* that will be notified before the given
* action is shown. The slot "aboutToShow( QAction*, <specific type> ) will be called,
*
*
* <specific type> corresponds to the category: playlist_ptr for Playlists, etc.
*
* The Action Collection takes ownership of the action. It's time to let go.
*/
void addAction( ActionDestination category, QAction* action, QObject* notify = 0 );
/**
* Remove an action from one or all specific categories
*/
void removeAction( QAction* action );
void removeAction( QAction* action, ActionDestination category );
public slots:
void togglePrivateListeningMode();
@ -48,6 +83,8 @@ private:
static ActionCollection* s_instance;
QHash< QString, QAction* > m_actionCollection;
QHash< ActionDestination, QList< QAction* > > m_categoryActions;
QHash< QAction*, QObject* > m_actionNotifiers;
};
#endif

View File

@ -33,9 +33,8 @@
namespace Tomahawk
{
/**
* If a playlist needs updating, implement a updater interface.
*
* Default is auto-updating and on a periodic timer.
* PlaylistUpdaters are attached to playlists. They usually manipulate the playlist in some way
* due to external input (spotify syncing) or timers (xspf updating)
*/
class PlaylistUpdaterFactory;

View File

@ -0,0 +1,204 @@
/****************************************************************************************
* Copyright (c) 2009 Mark Kretschmann <kretschmann@kde.org> *
* Copyright (c) 2009 Ian Monroe <ian@monroe.nu> *
* Copyright (c) 2009 Max Howell <max@last.fm> *
* *
* This program is free software; you can redistribute it and/or modify it under *
* the terms of the GNU General Public License as published by the Free Software *
* Foundation; either version 2 of the License, or (at your option) any later *
* version. *
* *
* This program is distributed in the hope that it will be useful, but WITHOUT ANY *
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A *
* PARTICULAR PURPOSE. See the GNU General Public License for more details. *
* *
* You should have received a copy of the GNU General Public License along with *
* this program. If not, see <http://www.gnu.org/licenses/>. *
****************************************************************************************/
#ifndef SMART_POINTER_LIST_H
#define SMART_POINTER_LIST_H
#include <QList> //baseclass
#include <QObject> //baseclass
#include "dllmacro.h"
class DLLEXPORT SmartPointerListDaddy : public QObject
{
Q_OBJECT
QList<QObject*>& m_list;
public:
SmartPointerListDaddy( QList<QObject*>* list ) : m_list( *list )
{}
private slots:
void onDestroyed()
{
m_list.removeAll( sender() );
}
};
/** QList has no virtual functions, so we inherit privately and define the
* interface exactly to ensure users can't write code that breaks the
* class's internal behaviour.
*
* I deliberately didn't define clear. I worried people would assume it
* deleted the pointers. Or assume it didn't. I didn't expose a few other
* functions for that reason.
*
* non-const iterator functions are not exposed as they access the QList
* baseclass, and then the Daddy wouldn't be watching newly inserted items.
*
* --mxcl
* Exposed clear. This class doesn't have a QPtrList autodelete functionality
* ever, so if people think that, they're really confused! -- Ian Monroe
*
*/
template <class T> class SmartPointerList : private QList<T*>
{
class SmartPointerListDaddy* m_daddy;
public:
SmartPointerList() : m_daddy( new SmartPointerListDaddy( (QList<QObject*>*)this ) )
{}
~SmartPointerList()
{
delete m_daddy;
}
SmartPointerList( const SmartPointerList<T>& that )
: QList<T*>()
, m_daddy( new SmartPointerListDaddy( (QList<QObject*>*)this ) )
{
QListIterator<T*> i( that );
while (i.hasNext())
append( i.next() );
}
SmartPointerList& operator=( const SmartPointerList<T>& that )
{
QListIterator<T*> i( *this);
while (i.hasNext())
QObject::disconnect( m_daddy, 0, i.next(), 0 );
QList<T*>::operator=( that );
if (this != &that) {
QListIterator<T*> i( that );
while (i.hasNext())
m_daddy->connect( i.next(), SIGNAL(destroyed()), SLOT(onDestroyed()) );
}
return *this;
}
// keep same function names as Qt
void append( T* o )
{
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
QList<T*>::append( o );
}
void prepend( T* o )
{
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
QList<T*>::prepend( o );
}
SmartPointerList& operator+=( T* o )
{
append( o );
return *this;
}
SmartPointerList& operator<<( T* o )
{
return operator+=( o );
}
SmartPointerList operator+( const SmartPointerList that )
{
SmartPointerList<T> copy = *this;
QListIterator<T*> i( that );
while (i.hasNext())
copy.append( i.next() );
return copy;
}
SmartPointerList& operator+=( const SmartPointerList that )
{
QListIterator<T*> i( that );
while (i.hasNext())
append( i.next() );
return *this;
}
void push_back( T* o )
{
append( o );
}
void push_front( T* o )
{
prepend( o );
}
void replace( int i, T* o )
{
QList<T*>::replace( i, o );
m_daddy->connect( o, SIGNAL(destroyed()), SLOT(onDestroyed()) );
}
/** this is a "safe" class. We always bounds check */
inline T* operator[]( int index ) const { return QList<T*>::value( index ); }
inline T* at( int index ) const { return QList<T*>::value( index ); }
// make public safe functions again
using QList<T*>::back;
using QList<T*>::constBegin;
using QList<T*>::constEnd;
using typename QList<T*>::const_iterator;
using QList<T*>::contains;
using QList<T*>::count;
using QList<T*>::empty;
using QList<T*>::erase;
using QList<T*>::first;
using QList<T*>::front;
using QList<T*>::indexOf;
using QList<T*>::insert;
using QList<T*>::isEmpty;
using QList<T*>::last;
using QList<T*>::lastIndexOf;
using QList<T*>::mid;
using QList<T*>::move;
using QList<T*>::pop_back;
using QList<T*>::pop_front;
using QList<T*>::size;
using QList<T*>::swap;
using QList<T*>::value;
using QList<T*>::operator!=;
using QList<T*>::operator==;
// can't use using directive here since we only want the const versions
typename QList<T*>::const_iterator begin() const { return QList<T*>::constBegin(); }
typename QList<T*>::const_iterator end() const { return QList<T*>::constEnd(); }
// it can lead to poor performance situations if we don't disconnect
// but I think it's not worth making this class more complicated for such
// an edge case
using QList<T*>::clear;
using QList<T*>::removeAll;
using QList<T*>::removeAt;
using QList<T*>::removeFirst;
using QList<T*>::removeLast;
using QList<T*>::removeOne;
using QList<T*>::takeAt;
using QList<T*>::takeFirst;
using QList<T*>::takeLast;
};
#endif //HEADER_GUARD

View File

@ -143,11 +143,13 @@ SourceTreeView::setupMenus()
bool readonly = true;
SourcesModel::RowType type = ( SourcesModel::RowType )model()->data( m_contextMenuIndex, SourcesModel::SourceTreeItemTypeRole ).toInt();
const PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex );
const playlist_ptr playlist = item->playlist();
if ( type == SourcesModel::StaticPlaylist || type == SourcesModel::AutomaticPlaylist || type == SourcesModel::Station )
{
PlaylistItem* item = itemFromIndex< PlaylistItem >( m_contextMenuIndex );
playlist_ptr playlist = item->playlist();
if ( !playlist.isNull() )
{
readonly = !playlist->author()->isLocal();
@ -190,7 +192,7 @@ SourceTreeView::setupMenus()
QString addToText = QString( "Add to my %1" );
if ( type == SourcesModel::StaticPlaylist )
addToText = addToText.arg( "Playlists" );
addToText = addToText.arg( "playlists" );
if ( type == SourcesModel::AutomaticPlaylist )
addToText = addToText.arg( "Automatic Playlists" );
else if ( type == SourcesModel::Station )
@ -203,6 +205,24 @@ SourceTreeView::setupMenus()
renamePlaylistAction->setEnabled( !readonly );
addToLocalAction->setEnabled( readonly );
// Handle any custom actions registered for playlists
if ( !ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ).isEmpty() )
{
m_playlistMenu.addSeparator();
foreach ( QAction* action, ActionCollection::instance()->getAction( ActionCollection::LocalPlaylists ) )
{
if ( QObject* notifier = ActionCollection::instance()->actionNotifier( action ) )
{
QMetaObject::invokeMethod( notifier, "aboutToShow", Qt::DirectConnection, Q_ARG( QAction*, action ), Q_ARG( Tomahawk::playlist_ptr, playlist ) );
}
action->setProperty( "payload", QVariant::fromValue< playlist_ptr >( playlist ) );
m_playlistMenu.addAction( action );
}
}
if ( type == SourcesModel::StaticPlaylist )
copyPlaylistAction->setText( tr( "&Export Playlist" ) );