Make playlist items use the full width available to them and adjust correctly when...
[amarok.git] / src / playlistbrowser.cpp
blob7d1597400802d1ac77701aba906f138ffb2cf182
1 /***************************************************************************
2 * copyright : (c) 2004 Pierpaolo Di Panfilo *
3 * (c) 2004 Mark Kretschmann <markey@web.de> *
4 * (c) 2005-2007 Seb Ruiz <me@sebruiz.net> *
5 * (c) 2005 Gábor Lehel <illissius@gmail.com> *
6 * (c) 2005 Christian Muehlhaeuser <chris@chris.de> *
7 * (c) 2006 Alexandre Oliveira <aleprj@gmail.com> *
8 * (c) 2006 Adam Pigg <adam@piggz.co.uk> *
9 * See COPYING file for licensing information *
10 ***************************************************************************/
12 #define DEBUG_PREFIX "PlaylistBrowser"
14 #include "playlistbrowser.h"
16 #include "amarok.h" //actionCollection()
17 #include "browserToolBar.h"
18 #include "debug.h"
19 #include "dynamicmode.h"
20 #include "k3bexporter.h"
21 #include "lastfm.h"
22 #include "mediabrowser.h"
23 #include "playlistbrowseritem.h"
24 #include "playlist.h"
25 #include "playlist/PlaylistModel.h"
26 #include "playlistselection.h"
27 #include "podcastbundle.h"
28 #include "PodcastCollection.h"
29 #include "podcastsettings.h"
30 #include "querybuilder.h" //smart playlists
31 #include "scancontroller.h"
32 #include "smartplaylisteditor.h"
33 #include "statusbar.h"
34 #include "tagdialog.h" //showContextMenu()
35 #include "TheInstances.h"
36 #include "threadmanager.h"
37 #include "xspfplaylist.h"
38 #include "playlistmanager/PlaylistManager.h"
40 #include <k3multipledrag.h> //dragObject()
41 #include <k3urldrag.h> //dragObject()
42 #include <KAction>
43 #include <KActionCollection>
44 #include <KActionMenu>
45 #include <KApplication>
46 #include <KFileDialog> //openPlaylist()
47 #include <KIconLoader> //smallIcon
48 #include <KInputDialog>
49 #include <KIO/DeleteJob> //deleteSelectedPlaylists()
50 #include <KLineEdit> //rename()
51 #include <KLocale>
52 #include <KMenu>
53 #include <KMessageBox> //renamePlaylist(), deleteSelectedPlaylist()
54 #include <KMimeType>
55 #include <KPushButton>
56 #include <KStandardDirs> //KGlobal::dirs()
57 #include <KVBox>
59 #include <q3header.h> //mousePressed()
60 #include <Q3PtrList>
61 #include <q3textstream.h> //loadPlaylists(), saveM3U(), savePLS()
62 #include <QDragEnterEvent>
63 #include <QDragLeaveEvent>
64 #include <QDragMoveEvent>
65 #include <QDropEvent>
66 #include <QEvent> //customEvent()
67 #include <QKeyEvent>
68 #include <QLabel>
69 #include <QLinkedList>
70 #include <QPainter> //paintCell()
71 #include <QPaintEvent>
72 #include <QPixmap> //paintCell()
73 #include <QResizeEvent>
74 #include <QSplitter>
76 #include <cstdio> //rename() in renamePlaylist()
80 namespace Amarok {
81 Q3ListViewItem*
82 findItemByPath( Q3ListView *view, QString name )
84 const static QString escaped( "\\/" );
85 const static QChar sep( '/' );
87 debug() << "Searching " << name;
88 QStringList path = splitPath( name );
90 Q3ListViewItem *prox = view->firstChild();
91 Q3ListViewItem *item = 0;
93 foreach( QString text, path ) {
94 item = prox;
95 text.replace( escaped, sep );
97 for ( ; item; item = item->nextSibling() ) {
98 if ( text == item->text(0) ) {
99 break;
103 if ( !item )
104 return 0;
105 prox = item->firstChild();
107 return item;
110 QStringList
111 splitPath( QString path ) {
112 QStringList list;
114 const static QChar sep( '/' );
115 int bOffset = 0, sOffset = 0;
117 int pos = path.indexOf( sep, bOffset );
119 while ( pos != -1 ) {
120 if ( pos > sOffset && pos <= (int)path.length() ) {
121 if ( pos > 0 && path[pos-1] != '\\' ) {
122 list << path.mid( sOffset, pos - sOffset );
123 sOffset = pos + 1;
126 bOffset = pos + 1;
127 pos = path.indexOf( sep, bOffset );
130 int length = path.length() - 1;
131 if ( path.mid( sOffset, length - sOffset + 1 ).length() > 0 )
132 list << path.mid( sOffset, length - sOffset + 1 );
134 return list;
139 inline QString
140 fileExtension( const QString &fileName )
142 return Amarok::extension( fileName );
145 PlaylistBrowser *PlaylistBrowser::s_instance = 0;
148 PlaylistBrowser::PlaylistBrowser( const char *name )
149 : KVBox( 0 )
150 , m_polished( false )
151 , m_playlistCategory( 0 )
152 , m_streamsCategory( 0 )
153 , m_smartCategory( 0 )
154 , m_dynamicCategory( 0 )
155 , m_podcastCategory( 0 )
156 , m_coolStreams( 0 )
157 , m_smartDefaults( 0 )
158 , m_lastfmCategory( 0 )
159 , m_shoutcastCategory( 0 )
160 , m_lastPlaylist( 0 )
161 , m_coolStreamsOpen( false )
162 , m_smartDefaultsOpen( false )
163 , m_lastfmOpen( false )
164 , m_ac( new KActionCollection( this ) )
165 , m_podcastTimer( new QTimer( this ) )
169 s_instance = this;
171 KVBox *browserBox = new KVBox( this );
172 // browserBox->setSpacing( 3 );
174 //<Toolbar>
175 addMenuButton = new KActionMenu( KIcon( Amarok::icon( "add_playlist" ) ), i18n("Add"), m_ac );
176 addMenuButton->setDelayed( false );
178 KMenu *playlistMenu = new KMenu( browserBox );
179 playlistMenu->insertItem( i18n("New..."), PLAYLIST );
180 playlistMenu->insertItem( i18n("Import Existing..."), PLAYLIST_IMPORT );
181 connect( playlistMenu, SIGNAL( activated(int) ), SLOT( slotAddPlaylistMenu(int) ) );
183 KMenu *addMenu = addMenuButton->popupMenu();
184 addMenu->insertItem( i18n("Playlist"), playlistMenu );
185 addMenu->insertItem( i18n("Smart Playlist..."), SMARTPLAYLIST );
186 addMenu->insertItem( i18n("Dynamic Playlist..."), ADDDYNAMIC);
187 addMenu->insertItem( i18n("Radio Stream..."), STREAM );
188 addMenu->insertItem( i18n("Podcast..."), PODCAST );
189 connect( addMenu, SIGNAL( activated(int) ), SLOT( slotAddMenu(int) ) );
191 renameButton = new KAction( KIcon( "edit-clear" ), i18n("Rename"), m_ac );
192 connect( renameButton, SIGNAL( triggered( bool ) ), this, SLOT( renameSelectedItem() ) );
193 removeButton = new KAction( KIcon( Amarok::icon( "remove" ) ), i18n("Delete"), m_ac );
194 connect( removeButton, SIGNAL( triggered( bool ) ), this, SLOT( removeSelectedItems() ) );
196 m_toolbar = new Browser::ToolBar( browserBox );
197 m_toolbar->setToolButtonStyle( Qt::ToolButtonIconOnly );
198 m_toolbar->setSizePolicy( QSizePolicy::Preferred, QSizePolicy::Preferred );
199 m_toolbar->addAction( addMenuButton );
201 m_toolbar->setToolButtonStyle( Qt::ToolButtonIconOnly ); //default appearance
202 m_toolbar->addSeparator();
203 m_toolbar->addAction( renameButton );
204 m_toolbar->addAction( removeButton );
206 renameButton->setEnabled( false );
207 removeButton->setEnabled( false );
208 //</Toolbar>
210 m_splitter = new QSplitter( Qt::Vertical, browserBox );
211 m_splitter->setChildrenCollapsible( false ); // hiding the InfoPane entirely can only be confusing
213 m_listview = new PlaylistBrowserView( m_splitter );
215 int sort = Amarok::config( "PlaylistBrowser" ).readEntry( "Sorting", int(Qt::Ascending) );
216 m_listview->setSorting( 0, sort == Qt::Ascending ? true : false );
218 m_podcastTimerInterval = Amarok::config( "PlaylistBrowser" ).readEntry( "Podcast Interval", 14400000 );
219 connect( m_podcastTimer, SIGNAL(timeout()), this, SLOT(scanPodcasts()) );
221 // signals and slots connections
222 connect( m_listview, SIGNAL( contextMenuRequested( Q3ListViewItem *, const QPoint &, int ) ),
223 this, SLOT( showContextMenu( Q3ListViewItem *, const QPoint &, int ) ) );
224 connect( m_listview, SIGNAL( doubleClicked( Q3ListViewItem *, const QPoint &, int ) ),
225 this, SLOT( invokeItem( Q3ListViewItem *, const QPoint &, int ) ) );
226 connect( m_listview, SIGNAL( itemRenamed( Q3ListViewItem*, const QString&, int ) ),
227 this, SLOT( renamePlaylist( Q3ListViewItem*, const QString&, int ) ) );
228 connect( m_listview, SIGNAL( currentChanged( Q3ListViewItem * ) ),
229 this, SLOT( currentItemChanged( Q3ListViewItem * ) ) );
230 connect( CollectionDB::instance(), SIGNAL( scanDone( bool ) ), SLOT( collectionScanDone() ) );
232 // setMinimumWidth( m_toolbar->sizeHint().width() );
234 m_infoPane = new InfoPane( m_splitter );
236 m_podcastCategory = loadPodcasts();
238 polish();
240 setSpacing( 4 );
241 setFocusProxy( m_listview );
245 void
246 PlaylistBrowser::polish()
248 // we make startup faster by doing the slow bits for this
249 // only when we are shown on screen
251 DEBUG_FUNC_INFO
253 Amarok::OverrideCursor cursor;
255 // blockSignals( true );
256 // BrowserBar::instance()->restoreWidth();
257 // blockSignals( false );
259 KVBox::polish();
261 /// Podcasting is always initialised in the ctor because of autoscanning
263 m_polished = true;
265 m_playlistCategory = loadPlaylists();
266 if( !CollectionDB::instance()->isEmpty() )
268 m_smartCategory = loadSmartPlaylists();
269 loadDefaultSmartPlaylists();
271 #define config Amarok::config( "PlaylistBrowser" )
273 m_dynamicCategory = loadDynamics();
274 m_randomDynamic = new DynamicEntry( m_dynamicCategory, 0, i18n("Random Mix") );
275 m_randomDynamic->setKept( false ); //don't save it
276 m_randomDynamic->setCycleTracks( config.readEntry( "Dynamic Random Remove Played", true ) );
277 m_randomDynamic->setUpcomingCount( config.readEntry ( "Dynamic Random Upcoming Count", 15 ) );
278 m_randomDynamic->setPreviousCount( config.readEntry ( "Dynamic Random Previous Count", 5 ) );
280 m_suggestedDynamic = new DynamicEntry( m_dynamicCategory, m_randomDynamic, i18n("Suggested Songs" ) );
281 m_suggestedDynamic->setKept( false ); //don't save it
282 m_suggestedDynamic->setAppendType( DynamicMode::SUGGESTION );
283 m_suggestedDynamic->setCycleTracks( config.readEntry( "Dynamic Suggest Remove Played", true ) );
284 m_suggestedDynamic->setUpcomingCount( config.readEntry ( "Dynamic Suggest Upcoming Count", 15 ) );
285 m_suggestedDynamic->setPreviousCount( config.readEntry ( "Dynamic Suggest Previous Count", 5 ) );
287 #undef config
289 m_streamsCategory = loadStreams();
290 loadCoolStreams();
291 m_shoutcastCategory = new ShoutcastBrowser( m_streamsCategory );
293 if( !AmarokConfig::scrobblerUsername().isEmpty() )
295 const bool subscriber = Amarok::config( "Scrobbler" ).readEntry( "Subscriber", false );
296 loadLastfmStreams( subscriber );
299 markDynamicEntries();
301 // ListView item state restoration:
302 // First we check if the number of items in the listview is the same as it was on last
303 // application exit. If true, we iterate over all items and restore their open/closed state.
304 // Note: We ignore podcast items, because they are added dynamically added to the ListView.
305 QList<int> stateList = Amarok::config( "PlaylistBrowser" ).readEntry( "Item State", QList<int>() );
306 Q3ListViewItemIterator it( m_listview );
307 uint count = 0;
308 while ( it.current() ) {
309 if( !isPodcastEpisode( it.current() ) )
310 ++count;
311 ++it;
314 if ( count == stateList.count() ) {
315 uint index = 0;
316 it = Q3ListViewItemIterator( m_listview );
317 while ( it.current() ) {
318 if( !isPodcastEpisode( it.current() ) ) {
319 it.current()->setOpen( stateList[index] );
320 ++index;
322 ++it;
326 // Set height of InfoPane
327 m_infoPane->setStoredHeight( Amarok::config( "PlaylistBrowser" ).readEntry( "InfoPane Height", 200 ) );
331 PlaylistBrowser::~PlaylistBrowser()
333 DEBUG_BLOCK
335 s_instance = 0;
337 if( m_polished )
339 savePodcastFolderStates( m_podcastCategory );
341 QStringList list;
342 for( uint i=0; i < m_dynamicEntries.count(); i++ )
344 Q3ListViewItem *item = m_dynamicEntries.at( i );
345 list.append( item->text(0) );
347 int sortorder = m_listview->sortOrder() == Qt::AscendingOrder ? 0 : 1;
348 Amarok::config( "PlaylistBrowser" ).writeEntry( "Sorting", sortorder );
349 Amarok::config( "PlaylistBrowser" ).writeEntry( "Podcast Interval", m_podcastTimerInterval );
350 Amarok::config( "PlaylistBrowser" ).writeEntry( "Podcast Folder Open", m_podcastCategory->isOpen() );
351 Amarok::config( "PlaylistBrowser" ).writeEntry( "InfoPane Height", m_infoPane->getHeight() );
356 void
357 PlaylistBrowser::setInfo( const QString &title, const QString &info )
359 m_infoPane->setInfo( title, info );
362 void
363 PlaylistBrowser::resizeEvent( QResizeEvent * )
365 if( m_infoPane->findChild<QWidget*>( "container" )->isShown() )
366 m_infoPane->setMaximumHeight( ( int )( m_splitter->height() / 1.5 ) );
370 void
371 PlaylistBrowser::markDynamicEntries()
373 if( Amarok::dynamicMode() )
375 QStringList playlists = Amarok::dynamicMode()->items();
377 for( uint i=0; i < playlists.count(); i++ )
379 PlaylistBrowserEntry *item = dynamic_cast<PlaylistBrowserEntry*>( Amarok::findItemByPath( m_listview, playlists[i] ) );
381 if( item )
383 m_dynamicEntries.append( item );
384 if( item->rtti() == PlaylistEntry::RTTI )
385 static_cast<PlaylistEntry*>( item )->setDynamic( true );
386 if( item->rtti() == SmartPlaylist::RTTI )
387 static_cast<SmartPlaylist*>( item )->setDynamic( true );
394 *************************************************************************
395 * STREAMS
396 *************************************************************************
399 QString PlaylistBrowser::streamBrowserCache() const
401 return Amarok::saveLocation() + "streambrowser_save.xml";
405 PlaylistCategory* PlaylistBrowser::loadStreams()
407 QFile file( streamBrowserCache() );
409 QTextStream stream( &file );
410 stream.setCodec( "UTF8" );
412 QDomDocument d;
413 QDomElement e;
415 Q3ListViewItem *after = m_dynamicCategory;
417 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
418 { /*Couldn't open the file or it had invalid content, so let's create an empty element*/
419 return new PlaylistCategory( m_listview, after, i18n("Radio Streams") );
421 else {
422 e = d.namedItem( "category" ).toElement();
423 if ( e.attribute("formatversion") =="1.1" ) {
424 PlaylistCategory* p = new PlaylistCategory( m_listview, after, e );
425 p->setText(0, i18n("Radio Streams") );
426 return p;
428 else { // Old unversioned format
429 PlaylistCategory* p = new PlaylistCategory( m_listview, after, i18n("Radio Streams") );
430 Q3ListViewItem *last = 0;
431 QDomNode n = d.namedItem( "streambrowser" ).namedItem("stream");
432 for( ; !n.isNull(); n = n.nextSibling() ) {
433 last = new StreamEntry( p, last, n.toElement() );
435 return p;
440 void PlaylistBrowser::loadCoolStreams()
442 QFile file( KStandardDirs::locate( "data","amarok/data/Cool-Streams.xml" ) );
443 if( !file.open( QIODevice::ReadOnly ) )
444 return;
446 QTextStream stream( &file );
447 stream.setCodec( "UTF8" );
449 QDomDocument d;
451 if( !d.setContent( stream.read() ) )
453 error() << "Bad Cool Streams XML file" << endl;
454 return;
457 m_coolStreams = new PlaylistCategory( m_streamsCategory, 0, i18n("Cool-Streams") );
458 m_coolStreams->setOpen( m_coolStreamsOpen );
459 m_coolStreams->setKept( false );
460 StreamEntry *last = 0;
462 QDomNode n = d.namedItem( "coolstreams" ).firstChild();
464 for( ; !n.isNull(); n = n.nextSibling() )
466 QDomElement e = n.toElement();
467 QString name = e.attribute( "name" );
468 e = n.namedItem( "url" ).toElement();
469 KUrl url = KUrl::KUrl( e.text() );
470 last = new StreamEntry( m_coolStreams, last, url, name );
471 last->setKept( false );
475 void PlaylistBrowser::addStream( Q3ListViewItem *parent )
477 StreamEditor dialog( this, i18n( "Radio Stream" ), QString() );
478 dialog.setCaption( i18n( "Add Radio Stream" ) );
480 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_streamsCategory);
482 if( dialog.exec() == QDialog::Accepted )
484 new StreamEntry( parent, 0, dialog.url(), dialog.name() );
485 parent->sortChildItems( 0, true );
486 parent->setOpen( true );
488 saveStreams();
493 void PlaylistBrowser::editStreamURL( StreamEntry *item, const bool readonly )
495 StreamEditor dialog( this, item->title(), item->url().prettyUrl(), readonly );
496 dialog.setCaption( readonly ? i18n( "Radio Stream" ) : i18n( "Edit Radio Stream" ) );
498 if( dialog.exec() == QDialog::Accepted )
500 item->setTitle( dialog.name() );
501 item->setUrl( dialog.url() );
502 item->setText(0, dialog.name() );
506 void PlaylistBrowser::saveStreams()
508 QFile file( streamBrowserCache() );
510 QDomDocument doc;
511 QDomElement streamB = m_streamsCategory->xml();
512 streamB.setAttribute( "product", "Amarok" );
513 streamB.setAttribute( "version", APP_VERSION );
514 streamB.setAttribute( "formatversion", "1.1" );
515 QDomNode streamsNode = doc.importNode( streamB, true );
516 doc.appendChild( streamsNode );
518 QString temp( doc.toString() );
520 // Only open the file after all data is ready. If it crashes, data is not lost!
521 if ( !file.open( QIODevice::WriteOnly ) ) return;
523 QTextStream stream( &file );
524 stream.setCodec( "UTF8" );
525 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
526 stream << temp;
530 *************************************************************************
531 * LAST.FM
532 *************************************************************************
535 void PlaylistBrowser::loadLastfmStreams( const bool subscriber /*false*/ )
537 QFile file( Amarok::saveLocation() + "lastfmbrowser_save.xml" );
539 QTextStream stream( &file );
540 stream.setCodec( "UTF8" );
542 QDomDocument d;
543 QDomElement e;
545 Q3ListViewItem *after = m_streamsCategory;
547 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
548 { /*Couldn't open the file or it had invalid content, so let's create an empty element*/
549 m_lastfmCategory = new PlaylistCategory( m_listview, after , i18n("Last.fm Radio") );
551 else {
552 e = d.namedItem( "category" ).toElement();
553 m_lastfmCategory = new PlaylistCategory( m_listview, after, e );
554 m_lastfmCategory->setText( 0, i18n("Last.fm Radio") );
557 /// Load the default items
559 QStringList globaltags;
560 globaltags << "Alternative" << "Ambient" << "Chill Out" << "Classical" << "Dance"
561 << "Electronica" << "Favorites" << "Heavy Metal" << "Hip Hop" << "Indie Rock"
562 << "Industrial" << "Japanese" << "Pop" << "Psytrance" << "Rap" << "Rock"
563 << "Soundtrack" << "Techno" << "Trance";
565 PlaylistCategory *tagsFolder = new PlaylistCategory( m_lastfmCategory, 0, i18n("Global Tags") );
566 tagsFolder->setKept( false );
567 LastFmEntry *last = 0;
569 foreach( QString str, globaltags )
571 const KUrl url( "lastfm://globaltags/" + str );
572 last = new LastFmEntry( tagsFolder, last, url, str );
573 last->setKept( false );
576 QString user = AmarokConfig::scrobblerUsername();
577 KUrl url( QString("lastfm://user/%1/neighbours").arg( user ) );
578 last = new LastFmEntry( m_lastfmCategory, tagsFolder, url, i18n( "Neighbor Radio" ) );
579 last->setKept( false );
581 if( subscriber )
583 url = KUrl( QString("lastfm://user/%1/personal").arg( user ) );
584 last = new LastFmEntry( m_lastfmCategory, last, url, i18n( "Personal Radio" ) );
585 last->setKept( false );
587 url = KUrl( QString("lastfm://user/%1/loved").arg( user ) );
588 last = new LastFmEntry( m_lastfmCategory, last, url, i18n( "Loved Radio" ) );
589 last->setKept( false );
593 void PlaylistBrowser::addLastFmRadio( Q3ListViewItem *parent )
595 StreamEditor dialog( this, i18n( "Last.fm Radio" ), QString() );
596 dialog.setCaption( i18n( "Add Last.fm Radio" ) );
598 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_lastfmCategory);
600 if( dialog.exec() == QDialog::Accepted )
602 new LastFmEntry( parent, 0, dialog.url(), dialog.name() );
603 parent->sortChildItems( 0, true );
604 parent->setOpen( true );
605 saveLastFm();
609 void PlaylistBrowser::addLastFmCustomRadio( Q3ListViewItem *parent )
611 QString token = LastFm::Controller::createCustomStation();
612 if( token.isEmpty() ) return;
613 token.replace( "/", "%252" );
615 const QString text = "lastfm://artistnames/" + token;
616 const KUrl url( text );
618 QString name = LastFm::Controller::stationDescription( text );
619 name.replace( "%252", "/" );
620 new LastFmEntry( parent, 0, url, name );
621 saveLastFm();
624 void PlaylistBrowser::saveLastFm()
626 if ( !m_lastfmCategory )
627 return;
629 QFile file( Amarok::saveLocation() + "lastfmbrowser_save.xml" );
631 QDomDocument doc;
632 QDomElement lastfmB = m_lastfmCategory->xml();
633 lastfmB.setAttribute( "product", "Amarok" );
634 lastfmB.setAttribute( "version", APP_VERSION );
635 lastfmB.setAttribute( "formatversion", "1.1" );
636 QDomNode lastfmNode = doc.importNode( lastfmB, true );
637 doc.appendChild( lastfmNode );
639 QString temp( doc.toString() );
641 // Only open the file after all data is ready. If it crashes, data is not lost!
642 if ( !file.open( QIODevice::WriteOnly ) ) return;
644 QTextStream stream( &file );
645 stream.setCodec( "UTF8" );
646 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
647 stream << temp;
652 *************************************************************************
653 * SMART-PLAYLISTS
654 *************************************************************************
657 QString PlaylistBrowser::smartplaylistBrowserCache() const
659 return Amarok::saveLocation() + "smartplaylistbrowser_save.xml";
662 void PlaylistBrowser::addSmartPlaylist( Q3ListViewItem *parent ) //SLOT
664 if( CollectionDB::instance()->isEmpty() || !m_smartCategory )
665 return;
667 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_smartCategory);
670 SmartPlaylistEditor dialog( i18n("Untitled"), this );
671 if( dialog.exec() == QDialog::Accepted ) {
673 PlaylistCategory *category = dynamic_cast<PlaylistCategory*>(parent);
674 if( !category )
675 return; //this should never happen, but let's make sure amarok doesn't crash
676 for( Q3ListViewItem *item = category->firstChild(); item; item = item->nextSibling() ) {
677 SmartPlaylist *sp = dynamic_cast<SmartPlaylist*>(item);
678 if ( sp && sp->title() == dialog.name() ) {
679 if( KMessageBox::warningContinueCancel(
680 MainWindow::self(),
681 i18n( "A Smart Playlist named \"%1\" already exists. Do you want to overwrite it?", dialog.name() ),
682 i18n( "Overwrite Playlist?" ), KGuiItem( i18n( "Overwrite" ) ) ) ==
683 KMessageBox::Continue )
685 delete item;
686 break;
688 else
689 return;
692 new SmartPlaylist( parent, 0, dialog.result() );
693 parent->sortChildItems( 0, true );
694 parent->setOpen( true );
696 saveSmartPlaylists();
700 PlaylistCategory* PlaylistBrowser::loadSmartPlaylists()
703 QFile file( smartplaylistBrowserCache() );
704 QTextStream stream( &file );
705 stream.setCodec( "UTF8" );
706 Q3ListViewItem *after = m_playlistCategory;
708 QDomDocument d;
709 QDomElement e;
711 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
712 { /*Couldn't open the file or it had invalid content, so let's create an empty element*/
713 return new PlaylistCategory(m_listview, after, i18n("Smart Playlists") );
715 else {
716 e = d.namedItem( "category" ).toElement();
717 QString version = e.attribute("formatversion");
718 float fversion = e.attribute("formatversion").toFloat();
719 if ( version == "1.8" )
721 PlaylistCategory* p = new PlaylistCategory(m_listview, after, e );
722 p->setText( 0, i18n("Smart Playlists") );
723 return p;
725 else if ( fversion > 1.0f )
727 PlaylistCategory* p = new PlaylistCategory(m_listview, after, e );
728 p->setText( 0, i18n("Smart Playlists") );
729 debug() << "loading old format smart playlists, converted to new format";
730 updateSmartPlaylists( p );
731 saveSmartPlaylists( p );
732 return p;
734 else { // Old unversioned format
735 PlaylistCategory* p = new PlaylistCategory(m_listview, after , i18n("Smart Playlists") );
736 Q3ListViewItem *last = 0;
737 QDomNode n = d.namedItem( "smartplaylists" ).namedItem("smartplaylist");
738 for( ; !n.isNull(); n = n.nextSibling() ) {
739 last = new SmartPlaylist( p, last, n.toElement() );
741 return p;
746 void PlaylistBrowser::updateSmartPlaylists( Q3ListViewItem *p )
748 if( !p )
749 return;
751 for( Q3ListViewItem *it = p->firstChild();
753 it = it->nextSibling() )
755 SmartPlaylist *spl = dynamic_cast<SmartPlaylist *>( it );
756 if( spl )
758 QDomElement xml = spl->xml();
759 QDomElement query = xml.namedItem( "sqlquery" ).toElement();
760 QDomElement expandBy = xml.namedItem( "expandby" ).toElement();
761 updateSmartPlaylistElement( query );
762 updateSmartPlaylistElement( expandBy );
763 spl->setXml( xml );
765 else
766 updateSmartPlaylists( it );
770 void PlaylistBrowser::updateSmartPlaylistElement( QDomElement& query )
772 QRegExp limitSearch( "LIMIT.*(\\d+)\\s*,\\s*(\\d+)" );
773 QRegExp selectFromSearch( "SELECT[^'\"]*FROM" );
774 for(QDomNode child = query.firstChild();
775 !child.isNull();
776 child = child.nextSibling() )
778 if( child.isText() )
780 //HACK this should be refactored to just regenerate the SQL from the <criteria>'s
781 QDomText text = child.toText();
782 QString sql = text.data();
783 if ( selectFromSearch.search( sql ) != -1 )
784 sql.replace( selectFromSearch, "SELECT (*ListOfFields*) FROM" );
785 if ( limitSearch.search( sql ) != -1 )
786 sql.replace( limitSearch,
787 QString( "LIMIT %1 OFFSET %2").arg( limitSearch.capturedTexts()[2].toInt() ).arg( limitSearch.capturedTexts()[1].toInt() ) );
789 text.setData( sql );
790 break;
795 void PlaylistBrowser::loadDefaultSmartPlaylists()
797 DEBUG_BLOCK
799 const QStringList genres = CollectionDB::instance()->query( "SELECT DISTINCT name FROM genre;" );
800 const QStringList artists = CollectionDB::instance()->artistList();
801 SmartPlaylist *item;
802 QueryBuilder qb;
803 SmartPlaylist *last = 0;
804 m_smartDefaults = new PlaylistCategory( m_smartCategory, 0, i18n("Collection") );
805 m_smartDefaults->setOpen( m_smartDefaultsOpen );
806 m_smartDefaults->setKept( false );
807 /********** All Collection **************/
808 qb.initSQLDrag();
809 qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName );
810 qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
811 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
813 item = new SmartPlaylist( m_smartDefaults, 0, i18n( "All Collection" ), qb.query() );
814 item->setPixmap( 0, SmallIcon( Amarok::icon( "collection" ) ) );
815 item->setKept( false );
816 /********** Favorite Tracks **************/
817 qb.initSQLDrag();
818 qb.sortByFavorite();
819 qb.setLimit( 0, 15 );
820 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Favorite Tracks" ), qb.query() );
821 item->setKept( false );
822 last = 0;
824 qb.initSQLDrag();
825 qb.sortByFavorite();
826 qb.setLimit( 0, 15 );
827 foreach( QString str, artists ) {
828 QueryBuilder qbTemp( qb );
829 qbTemp.addMatch( QueryBuilder::tabArtist, str );
831 last = new SmartPlaylist( item, last, i18n( "By %1", str ), qbTemp.query() );
832 last->setKept( false );
835 /********** Most Played **************/
836 qb.initSQLDrag();
837 qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, true );
838 qb.setLimit( 0, 15 );
840 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Most Played" ), qb.query() );
841 item->setKept( false );
842 last = 0;
844 qb.initSQLDrag();
845 qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, true );
846 qb.setLimit( 0, 15 );
847 foreach( QString str, artists ) {
848 QueryBuilder qbTemp( qb );
849 qbTemp.addMatch( QueryBuilder::tabArtist, str );
851 last = new SmartPlaylist( item, last, i18n( "By %1", str ), qbTemp.query() );
852 last->setKept( false );
855 /********** Newest Tracks **************/
856 qb.initSQLDrag();
857 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valCreateDate, true );
858 qb.setLimit( 0, 15 );
860 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Newest Tracks" ), qb.query() );
861 item->setKept( false );
862 last = 0;
864 qb.initSQLDrag();
865 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valCreateDate, true );
866 qb.setLimit( 0, 15 );
867 foreach( QString str, artists ) {
868 QueryBuilder qbTemp( qb );
869 qbTemp.addMatch( QueryBuilder::tabArtist, str );
871 last = new SmartPlaylist( item, last, i18n( "By %1", str ), qbTemp.query( true ));
872 last->setKept( false );
875 /********** Last Played **************/
876 qb.initSQLDrag();
877 qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valAccessDate, true );
878 qb.setLimit( 0, 15 );
880 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Last Played" ), qb.query( true ) );
881 item->setKept( false );
883 /********** Never Played **************/
884 qb.initSQLDrag();
885 qb.addNumericFilter( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, "0" );
886 qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName );
887 qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
888 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
890 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Never Played" ), qb.query( true ) );
891 item->setKept( false );
893 /********** Ever Played **************/
894 qb.initSQLDrag();
895 qb.excludeFilter( QueryBuilder::tabStats, QueryBuilder::valPlayCounter, "1", QueryBuilder::modeLess );
896 qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName );
897 qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
898 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
899 qb.sortBy( QueryBuilder::tabStats, QueryBuilder::valScore );
901 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Ever Played" ), qb.query( true ) );
902 item->setKept( false );
904 /********** Genres **************/
905 item = new SmartPlaylist( m_smartDefaults, item, i18n( "Genres" ), QString() );
906 item->setKept( false );
907 last = 0;
909 qb.initSQLDrag();
910 qb.sortBy( QueryBuilder::tabArtist, QueryBuilder::valName );
911 qb.sortBy( QueryBuilder::tabAlbum, QueryBuilder::valName );
912 qb.sortBy( QueryBuilder::tabSong, QueryBuilder::valTrack );
913 foreach( QString str, genres ) {
914 QueryBuilder qbTemp( qb );
915 qbTemp.addMatch( QueryBuilder::tabGenre, str );
917 last = new SmartPlaylist( item, last, i18n( "%1", str ), qbTemp.query( true ) );
918 last->setKept( false );
921 /********** 50 Random Tracks **************/
922 qb.initSQLDrag();
923 qb.setOptions( QueryBuilder::optRandomize );
924 qb.setLimit( 0, 50 );
925 item = new SmartPlaylist( m_smartDefaults, item, i18n( "50 Random Tracks" ), qb.query( true ) );
926 item->setKept( false );
929 void PlaylistBrowser::editSmartPlaylist( SmartPlaylist* item )
931 SmartPlaylistEditor dialog( this, item->xml() );
933 if( dialog.exec() == QDialog::Accepted )
935 item->setXml ( dialog.result() );
936 item->setText( 0, dialog.name() );
938 if( item->isDynamic() ) // rebuild the cache if the smart playlist has changed
939 Playlist::instance()->rebuildDynamicModeCache();
943 void PlaylistBrowser::saveSmartPlaylists( PlaylistCategory *smartCategory )
945 QFile file( smartplaylistBrowserCache() );
947 if( !smartCategory )
948 smartCategory = m_smartCategory;
950 // If the user hadn't set a collection, we didn't create the Smart Playlist Item
951 if( !smartCategory ) return;
953 QDomDocument doc;
954 QDomElement smartB = smartCategory->xml();
955 smartB.setAttribute( "product", "Amarok" );
956 smartB.setAttribute( "version", APP_VERSION );
957 smartB.setAttribute( "formatversion", "1.8" );
958 QDomNode smartplaylistsNode = doc.importNode( smartB, true );
959 doc.appendChild( smartplaylistsNode );
961 QString temp( doc.toString() );
963 // Only open the file after all data is ready. If it crashes, data is not lost!
964 if ( !file.open( QIODevice::WriteOnly ) ) return;
966 QTextStream smart( &file );
967 smart.setEncoding( QTextStream::UnicodeUTF8 );
968 smart << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
969 smart << temp;
973 *************************************************************************
974 * PARTIES
975 *************************************************************************
978 QString PlaylistBrowser::dynamicBrowserCache() const
980 return Amarok::saveLocation() + "dynamicbrowser_save.xml";
983 PlaylistCategory* PlaylistBrowser::loadDynamics()
985 QFile file( dynamicBrowserCache() );
987 QTextStream stream( &file );
988 stream.setCodec( "UTF8" );
990 QDomDocument d;
991 QDomElement e;
993 PlaylistCategory *after = m_smartCategory;
994 if( CollectionDB::instance()->isEmpty() || !m_smartCategory ) // incase of no collection
995 after = m_playlistCategory;
997 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
998 { /*Couldn't open the file or it had invalid content, so let's create some defaults*/
999 PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Dynamic Playlists") );
1000 return p;
1002 else {
1003 e = d.namedItem( "category" ).toElement();
1004 QString version = e.attribute("formatversion");
1005 if ( version == "1.2" ) {
1006 PlaylistCategory* p = new PlaylistCategory( m_listview, after, e );
1007 p->setText( 0, i18n("Dynamic Playlists") );
1008 return p;
1010 else if ( version == "1.1" ) {
1011 // In 1.1, playlists would be referred only by its name.
1012 // TODO: We can *try* to convert by using findItem
1013 PlaylistCategory* p = new PlaylistCategory( m_listview, after, e );
1014 p->setText( 0, i18n("Dynamic Playlists") );
1015 fixDynamicPlaylistPath( p );
1016 return p;
1018 else { // Old unversioned format
1019 PlaylistCategory* p = new PlaylistCategory( m_listview, after, i18n("Dynamic Playlists") );
1020 Q3ListViewItem *last = 0;
1021 QDomNode n = d.namedItem( "dynamicbrowser" ).namedItem("dynamic");
1022 for( ; !n.isNull(); n = n.nextSibling() ) {
1023 last = new DynamicEntry( p, last, n.toElement() );
1025 return p;
1031 void
1032 PlaylistBrowser::fixDynamicPlaylistPath( Q3ListViewItem *item )
1034 DynamicEntry *entry = dynamic_cast<DynamicEntry*>( item );
1035 if ( entry ) {
1036 QStringList names = entry->items();
1037 QStringList paths;
1038 foreach( QString str, names ) {
1039 QString path = guessPathFromPlaylistName( str );
1040 if ( !path.isNull() )
1041 paths+=path;
1043 entry->setItems( paths );
1045 PlaylistCategory *cat = dynamic_cast<PlaylistCategory*>( item );
1046 if ( cat ) {
1047 Q3ListViewItem *it = cat->firstChild();
1048 for( ; it; it = it->nextSibling() ) {
1049 fixDynamicPlaylistPath( it );
1054 QString
1055 PlaylistBrowser::guessPathFromPlaylistName( QString name )
1057 Q3ListViewItem *item = m_listview->findItem( name, 0, Q3ListView::ExactMatch );
1058 PlaylistBrowserEntry *entry = dynamic_cast<PlaylistBrowserEntry*>( item );
1059 if ( entry )
1060 return entry->name();
1061 return QString();
1064 void PlaylistBrowser::saveDynamics()
1066 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Random Remove Played", m_randomDynamic->cycleTracks() );
1067 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Random Upcoming Count", m_randomDynamic->upcomingCount() );
1068 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Random Previous Count", m_randomDynamic->previousCount() );
1070 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Suggest Remove Played", m_suggestedDynamic->cycleTracks() );
1071 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Suggest Upcoming Count", m_suggestedDynamic->upcomingCount() );
1072 Amarok::config( "PlaylistBrowser" ).writeEntry( "Dynamic Suggest Previous Count", m_suggestedDynamic->previousCount() );
1074 QFile file( dynamicBrowserCache() );
1075 QTextStream stream( &file );
1077 QDomDocument doc;
1078 QDomElement dynamicB = m_dynamicCategory->xml();
1079 dynamicB.setAttribute( "product", "Amarok" );
1080 dynamicB.setAttribute( "version", APP_VERSION );
1081 dynamicB.setAttribute( "formatversion", "1.2" );
1082 QDomNode dynamicsNode = doc.importNode( dynamicB, true );
1083 doc.appendChild( dynamicsNode );
1085 QString temp( doc.toString() );
1087 // Only open the file after all data is ready. If it crashes, data is not lost!
1088 if ( !file.open( QIODevice::WriteOnly ) ) return;
1090 stream.setCodec( "UTF8" );
1091 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1092 stream << temp;
1095 void PlaylistBrowser::loadDynamicItems()
1097 // Make sure all items are unmarked
1098 for( uint i=0; i < m_dynamicEntries.count(); i++ )
1100 Q3ListViewItem *it = m_dynamicEntries.at( i );
1102 if( it )
1103 static_cast<PlaylistBrowserEntry*>(it)->setDynamic( false );
1105 m_dynamicEntries.clear(); // Don't use remove(), since we do i++, which would cause skip overs!!!
1107 // Mark appropriate items as used
1108 markDynamicEntries();
1112 *************************************************************************
1113 * PODCASTS
1114 *************************************************************************
1117 QString PlaylistBrowser::podcastBrowserCache() const
1119 //returns the playlists stats cache file
1120 return Amarok::saveLocation() + "podcastbrowser_save.xml";
1123 PlaylistCategory* PlaylistBrowser::loadPodcasts()
1125 DEBUG_BLOCK
1127 QFile file( podcastBrowserCache() );
1128 QTextStream stream( &file );
1129 stream.setCodec( "UTF8" );
1131 QDomDocument d;
1132 QDomElement e;
1134 Q3ListViewItem *after = 0;
1136 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
1137 { /*Couldn't open the file or it had invalid content, so let's create an empty element*/
1138 PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Podcasts") );
1139 p->setId( 0 );
1140 loadPodcastsFromDatabase( p );
1141 return p;
1143 else {
1144 e = d.namedItem( "category" ).toElement();
1146 if ( e.attribute("formatversion") == "1.1" ) {
1147 debug() << "Podcasts are being moved to the database...";
1148 m_podcastItemsToScan.clear();
1150 PlaylistCategory *p = new PlaylistCategory( m_listview, after, e );
1151 p->setId( 0 );
1152 //delete the file, it is deprecated
1153 KIO::del( KUrl( podcastBrowserCache() ) );
1155 if( !m_podcastItemsToScan.isEmpty() )
1156 m_podcastTimer->start( m_podcastTimerInterval );
1158 return p;
1161 PlaylistCategory *p = new PlaylistCategory( m_listview, after, i18n("Podcasts") );
1162 p->setId( 0 );
1163 return p;
1166 void PlaylistBrowser::loadPodcastsFromDatabase( PlaylistCategory *p )
1168 DEBUG_BLOCK
1169 if( !p ) p = m_podcastCategory;
1170 m_podcastItemsToScan.clear();
1172 while( p->firstChild() )
1173 delete p->firstChild();
1175 QMap<int,PlaylistCategory*> folderMap = loadPodcastFolders( p );
1177 Q3ValueList<PodcastChannelBundle> channels;
1179 channels = CollectionDB::instance()->getPodcastChannels();
1181 PodcastChannel *channel = 0;
1183 oldForeachType( Q3ValueList<PodcastChannelBundle>, channels )
1185 PlaylistCategory *parent = p;
1186 const int parentId = (*it).parentId();
1187 if( parentId > 0 && folderMap.find( parentId ) != folderMap.end() )
1188 parent = folderMap[parentId];
1190 channel = new PodcastChannel( parent, channel, *it );
1192 bool hasNew = CollectionDB::instance()->query( QString("SELECT COUNT(parent) FROM podcastepisodes WHERE ( parent='%1' AND isNew=%2 ) LIMIT 1" )
1193 .arg( (*it).url().url(), CollectionDB::instance()->boolT() ) )
1194 .first().toInt() > 0;
1196 channel->setNew( hasNew );
1198 if( channel->autoscan() )
1199 m_podcastItemsToScan.append( channel );
1202 if( !m_podcastItemsToScan.isEmpty() )
1203 m_podcastTimer->start( m_podcastTimerInterval );
1206 QMap<int,PlaylistCategory*>
1207 PlaylistBrowser::loadPodcastFolders( PlaylistCategory *p )
1209 DEBUG_BLOCK
1210 QString sql = "SELECT * FROM podcastfolders ORDER BY parent ASC;";
1211 QStringList values = CollectionDB::instance()->query( sql );
1213 // store the folder and IDs so finding a parent is fast
1214 QMap<int,PlaylistCategory*> folderMap;
1215 PlaylistCategory *folder = 0;
1216 oldForeach( values )
1218 const int id = (*it).toInt();
1219 const QString t = *++it;
1220 const int parentId = (*++it).toInt();
1221 const bool isOpen = ( (*++it) == CollectionDB::instance()->boolT() ? true : false );
1223 PlaylistCategory *parent = p;
1224 if( parentId > 0 && folderMap.find( parentId ) != folderMap.end() )
1225 parent = folderMap[parentId];
1227 folder = new PlaylistCategory( parent, folder, t, id );
1228 folder->setOpen( isOpen );
1230 folderMap[id] = folder;
1232 // check if the base folder exists
1233 p->setOpen( Amarok::config( "PlaylistBrowser" ).readEntry( "Podcast Folder Open", true ) );
1235 return folderMap;
1238 void PlaylistBrowser::savePodcastFolderStates( PlaylistCategory *folder )
1240 if( !folder ) return;
1242 PlaylistCategory *child = static_cast<PlaylistCategory*>(folder->firstChild());
1243 while( child )
1245 if( isCategory( child ) )
1246 savePodcastFolderStates( child );
1247 else
1248 break;
1250 child = static_cast<PlaylistCategory*>(child->nextSibling());
1252 if( folder != m_podcastCategory )
1254 if( folder->id() < 0 ) // probably due to a 1.3->1.4 migration
1255 { // we add the folder to the db, set the id and then update all the children
1256 int parentId = static_cast<PlaylistCategory*>(folder->parent())->id();
1257 int newId = CollectionDB::instance()->addPodcastFolder( folder->text(0), parentId, folder->isOpen() );
1258 folder->setId( newId );
1259 PodcastChannel *chan = static_cast<PodcastChannel*>(folder->firstChild());
1260 while( chan )
1262 if( isPodcastChannel( chan ) )
1263 // will update the database so child has correct parentId.
1264 chan->setParent( folder );
1265 chan = static_cast<PodcastChannel*>(chan->nextSibling());
1268 else
1270 CollectionDB::instance()->updatePodcastFolder( folder->id(), folder->text(0),
1271 static_cast<PlaylistCategory*>(folder->parent())->id(), folder->isOpen() );
1276 void PlaylistBrowser::scanPodcasts()
1278 //don't want to restart timer unnecessarily. addPodcast will start it if it is necessary
1279 if( m_podcastItemsToScan.isEmpty() ) return;
1281 for( uint i=0; i < m_podcastItemsToScan.count(); i++ )
1283 Q3ListViewItem *item = m_podcastItemsToScan.at( i );
1284 PodcastChannel *pc = static_cast<PodcastChannel*>(item);
1285 pc->rescan();
1287 //restart timer
1288 m_podcastTimer->start( m_podcastTimerInterval );
1291 void PlaylistBrowser::refreshPodcasts( Q3ListViewItem *parent )
1293 for( Q3ListViewItem *child = parent->firstChild();
1294 child;
1295 child = child->nextSibling() )
1297 if( isPodcastChannel( child ) )
1298 static_cast<PodcastChannel*>( child )->rescan();
1299 else if( isCategory( child ) )
1300 refreshPodcasts( child );
1304 void PlaylistBrowser::addPodcast( Q3ListViewItem *parent )
1306 bool ok;
1307 const QString name = KInputDialog::getText(i18n("Add Podcast"), i18n("Enter Podcast URL:"), QString(), &ok, this);
1309 if( ok && !name.isEmpty() )
1311 PodcastChannelProvider * podcastChannelProvider =
1312 static_cast<PodcastChannelProvider *>(
1313 The::playlistManager()->playlistProvider( PlaylistManager::PodcastChannel, i18n("Local Podcasts") )
1315 if( podcastChannelProvider )
1316 podcastChannelProvider->addPodcast( name ); //KUrl( name ), parent );
1317 else
1318 debug() << "no PodcastCollection found";
1322 void PlaylistBrowser::configurePodcasts( Q3ListViewItem *parent )
1324 Q3PtrList<PodcastChannel> podcastChannelList;
1325 for( Q3ListViewItem *child = parent->firstChild();
1326 child;
1327 child = child->nextSibling() )
1329 if( isPodcastChannel( child ) )
1331 podcastChannelList.append( static_cast<PodcastChannel*>( child ) );
1334 if( !podcastChannelList.isEmpty() )
1335 configurePodcasts( podcastChannelList, i18nc( "Podcasts contained in %1",
1336 "All in %1", parent->text( 0 ) ) );
1339 void PlaylistBrowser::configureSelectedPodcasts()
1341 Q3PtrList<PodcastChannel> selected;
1342 Q3ListViewItemIterator it( m_listview, Q3ListViewItemIterator::Selected);
1343 for( ; it.current(); ++it )
1345 if( isPodcastChannel( (*it) ) )
1346 selected.append( static_cast<PodcastChannel*>(*it) );
1348 if (selected.isEmpty() )
1349 return; //shouldn't happen
1351 if( selected.count() == 1 )
1352 selected.getFirst()->configure();
1353 else
1354 configurePodcasts( selected, i18np("1 Podcast", "%1 Podcasts", selected.count() ) );
1356 if( m_podcastItemsToScan.isEmpty() )
1357 m_podcastTimer->stop();
1359 else if( m_podcastItemsToScan.count() == 1 )
1360 m_podcastTimer->start( m_podcastTimerInterval );
1361 // else timer is already running
1364 void PlaylistBrowser::configurePodcasts( Q3PtrList<PodcastChannel> &podcastChannelList,
1365 const QString &caption )
1368 if( podcastChannelList.isEmpty() )
1370 debug() << "BUG: podcastChannelList is empty";
1371 return;
1373 Q3PtrList<PodcastSettings> podcastSettingsList;
1374 oldForeachType( Q3PtrList<PodcastChannel>, podcastChannelList)
1376 podcastSettingsList.append( (*it)->getSettings() );
1378 PodcastSettingsDialog *dialog = new PodcastSettingsDialog( podcastSettingsList, caption );
1379 if( dialog->configure() )
1381 PodcastChannel *channel = podcastChannelList.first();
1382 oldForeachType( Q3PtrList<PodcastSettings>, podcastSettingsList )
1384 if ( (*it)->title() == channel->title() )
1386 channel->setSettings( *it );
1388 else
1389 debug() << " BUG in playlistbrowser.cpp:configurePodcasts( )";
1391 channel = podcastChannelList.next();
1396 PodcastChannel *
1397 PlaylistBrowser::findPodcastChannel( const KUrl &feed, Q3ListViewItem *parent ) const
1399 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_podcastCategory);
1401 for( Q3ListViewItem *it = parent->firstChild();
1403 it = it->nextSibling() )
1405 if( isPodcastChannel( it ) )
1407 PodcastChannel *channel = static_cast<PodcastChannel *>( it );
1408 if( channel->url().prettyUrl() == feed.prettyUrl() )
1410 return channel;
1413 else if( isCategory( it ) )
1415 PodcastChannel *channel = findPodcastChannel( feed, it );
1416 if( channel )
1417 return channel;
1421 return 0;
1424 PodcastEpisode *
1425 PlaylistBrowser::findPodcastEpisode( const KUrl &episode, const KUrl &feed ) const
1427 PodcastChannel *channel = findPodcastChannel( feed );
1428 if( !channel )
1429 return 0;
1431 if( !channel->isPolished() )
1432 channel->load();
1434 Q3ListViewItem *child = channel->firstChild();
1435 while( child )
1437 #define child static_cast<PodcastEpisode*>(child)
1438 if( child->url() == episode )
1439 return child;
1440 #undef child
1441 child = child->nextSibling();
1444 return 0;
1447 void PlaylistBrowser::addPodcast( const KUrl& origUrl, Q3ListViewItem *parent )
1449 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_podcastCategory);
1451 KUrl url( origUrl );
1452 if( url.protocol() == "itpc" || url.protocol() == "pcast" )
1453 url.setProtocol( "http" );
1455 PodcastChannel *channel = findPodcastChannel( url );
1456 if( channel )
1458 Amarok::StatusBar::instance()->longMessage(
1459 i18n( "Already subscribed to feed %1 as %2", url.prettyUrl(),
1460 channel->title() ),
1461 KDE::StatusBar::Sorry );
1462 return;
1465 PodcastChannel *pc = new PodcastChannel( parent, 0, url );
1467 if( m_podcastItemsToScan.isEmpty() )
1469 m_podcastItemsToScan.append( pc );
1470 m_podcastTimer->start( m_podcastTimerInterval );
1472 else
1474 m_podcastItemsToScan.append( pc );
1477 parent->sortChildItems( 0, true );
1478 parent->setOpen( true );
1481 void PlaylistBrowser::changePodcastInterval()
1483 double time = static_cast<double>(m_podcastTimerInterval / ( 60 * 60 * 1000 ));
1484 bool ok;
1485 double interval = KInputDialog::getDouble( i18n("Download Interval"),
1486 i18n("Scan interval (hours):"), time,
1487 0.5, 100.0, .5, 1, // min, max, step, base
1488 &ok, this);
1489 int milliseconds = static_cast<int>(interval*60.0*60.0*1000.0);
1490 if( ok )
1492 if( milliseconds != m_podcastTimerInterval )
1494 m_podcastTimerInterval = milliseconds;
1495 m_podcastTimer->start( m_podcastTimerInterval );
1500 bool PlaylistBrowser::deleteSelectedPodcastItems( const bool removeItem, const bool silent )
1502 KUrl::List urls;
1503 Q3ListViewItemIterator it( m_podcastCategory, Q3ListViewItemIterator::Selected );
1504 Q3PtrList<PodcastEpisode> erasedItems;
1506 for( ; it.current(); ++it )
1508 if( isPodcastEpisode( *it ) )
1510 #define item static_cast<PodcastEpisode*>(*it)
1511 if( item->isOnDisk() ) {
1512 urls.append( item->localUrl() );
1513 erasedItems.append( item );
1515 #undef item
1519 if( urls.isEmpty() ) return false;
1520 int button;
1521 if( !silent )
1522 button = KMessageBox::warningContinueCancel( this,
1523 i18np( "<p>You have selected 1 podcast episode to be <b>irreversibly</b> deleted. ",
1524 "<p>You have selected %1 podcast episodes to be <b>irreversibly</b> deleted. ",
1525 urls.count() ), QString(), KStandardGuiItem::del() );
1526 if( silent || button != KMessageBox::Continue )
1527 return false;
1529 KIO::Job *job = KIO::del( urls );
1531 PodcastEpisode *item;
1532 for ( item = erasedItems.first(); item; item = erasedItems.next() )
1534 if( removeItem )
1536 CollectionDB::instance()->removePodcastEpisode( item->dBId() );
1537 delete item;
1539 else
1540 connect( job, SIGNAL( result( KIO::Job* ) ), item, SLOT( isOnDisk() ) );;
1542 return true;
1545 bool PlaylistBrowser::deletePodcasts( Q3PtrList<PodcastChannel> items )
1547 if( items.isEmpty() ) return false;
1549 KUrl::List urls;
1550 oldForeachType( Q3PtrList<PodcastChannel>, items )
1552 for( Q3ListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() )
1554 #define ch static_cast<PodcastEpisode*>(ch)
1555 if( ch->isOnDisk() )
1557 //delete downloaded media
1558 urls.append( ch->localUrl() );
1560 #undef ch
1561 /// we don't need to delete from the database, because removing the channel from the database
1562 /// automatically removes the children as well.
1563 m_podcastItemsToScan.remove( static_cast<PodcastChannel*>(*it) );
1565 CollectionDB::instance()->removePodcastChannel( static_cast<PodcastChannel*>(*it)->url() );
1568 // TODO We need to check which files have been deleted successfully
1569 if ( urls.count() )
1570 KIO::del( urls );
1571 return true;
1574 void PlaylistBrowser::downloadSelectedPodcasts()
1576 Q3ListViewItemIterator it( m_listview, Q3ListViewItemIterator::Selected );
1578 for( ; it.current(); ++it )
1580 if( isPodcastEpisode( *it ) )
1582 #define item static_cast<PodcastEpisode*>(*it)
1583 if( !item->isOnDisk() )
1584 m_podcastDownloadQueue.append( item );
1585 #undef item
1588 downloadPodcastQueue();
1591 void PlaylistBrowser::downloadPodcastQueue() //SLOT
1593 if( m_podcastDownloadQueue.isEmpty() ) return;
1595 PodcastEpisode *first = m_podcastDownloadQueue.first();
1596 first->downloadMedia();
1597 m_podcastDownloadQueue.removeFirst();
1599 connect( first, SIGNAL( downloadFinished() ), this, SLOT( downloadPodcastQueue() ) );
1600 connect( first, SIGNAL( downloadAborted() ), this, SLOT( abortPodcastQueue() ) );
1603 void PlaylistBrowser::abortPodcastQueue() //SLOT
1605 m_podcastDownloadQueue.clear();
1608 void PlaylistBrowser::registerPodcastSettings( const QString &title, const PodcastSettings *settings )
1610 m_podcastSettings.insert( title, settings );
1614 *************************************************************************
1615 * PLAYLISTS
1616 *************************************************************************
1619 QString PlaylistBrowser::playlistBrowserCache() const
1621 //returns the playlists stats cache file
1622 return Amarok::saveLocation() + "playlistbrowser_save.xml";
1625 PlaylistCategory* PlaylistBrowser::loadPlaylists()
1627 QFile file( playlistBrowserCache() );
1629 QTextStream stream( &file );
1630 stream.setCodec( "UTF8" );
1632 QDomDocument d;
1633 QDomElement e;
1635 if( !file.open( QIODevice::ReadOnly ) || !d.setContent( stream.read() ) )
1636 { /*Couldn't open the file or it had invalid content, so let's create an empty element*/
1637 return new PlaylistCategory(m_listview, 0 , i18n("Playlists") );
1639 else {
1640 e = d.namedItem( "category" ).toElement();
1641 if ( e.attribute("formatversion") =="1.1" )
1643 PlaylistCategory* p = new PlaylistCategory( m_listview, 0 , e );
1644 p->setText( 0, i18n("Playlists") );
1645 return p;
1647 else { // Old unversioned format
1648 PlaylistCategory* p = new PlaylistCategory( m_listview, 0 , i18n("Playlists") );
1649 Q3ListViewItem *last = 0;
1650 QDomNode n = d.namedItem( "playlistbrowser" ).namedItem("playlist");
1652 for ( ; !n.isNull(); n = n.nextSibling() )
1653 last = new PlaylistEntry( p, last, n.toElement() );
1655 return p;
1660 Q3ListViewItem *
1661 PlaylistBrowser::findItemInTree( const QString &searchstring, int c ) const
1663 QStringList list = QStringList::split( "/", searchstring, true );
1665 // select the 1st level
1666 QStringList::Iterator it = list.begin();
1667 Q3ListViewItem *pli = findItem (*it, c);
1668 if ( !pli ) return pli;
1670 for ( ++it ; it != list.end(); ++it )
1673 Q3ListViewItemIterator it2( pli );
1674 for( ++it2 ; it2.current(); ++it2 )
1676 if ( *it == (*it2)->text(0) )
1678 pli = *it2;
1679 break;
1681 // test, to not go over into the next category
1682 if ( isCategory( *it2 ) && (pli->nextSibling() == *it2) )
1683 return 0;
1685 if ( ! it2.current() )
1686 return 0;
1689 return pli;
1692 DynamicMode *PlaylistBrowser::findDynamicModeByTitle( const QString &title )
1694 if( !m_polished )
1695 polish();
1697 for ( Q3ListViewItem *item = m_dynamicCategory->firstChild(); item; item = item->nextSibling() )
1699 DynamicEntry *entry = dynamic_cast<DynamicEntry *>( item );
1700 if ( entry && entry->title() == title )
1701 return entry;
1704 return 0;
1707 PlaylistEntry *
1708 PlaylistBrowser::findPlaylistEntry( const QString &url, Q3ListViewItem *parent ) const
1710 if( !parent ) parent = static_cast<Q3ListViewItem*>(m_playlistCategory);
1712 for( Q3ListViewItem *it = parent->firstChild();
1714 it = it->nextSibling() )
1716 if( isPlaylist( it ) )
1718 PlaylistEntry *pl = static_cast<PlaylistEntry*>( it );
1719 debug() << pl->url().path() << " == " << url;
1720 if( pl->url().path() == url )
1722 debug() << "ok!";
1723 return pl;
1726 else if( isCategory( it ) )
1728 PlaylistEntry *pl = findPlaylistEntry( url, it );
1729 if( pl )
1730 return pl;
1734 return 0;
1737 int PlaylistBrowser::loadPlaylist( const QString &playlist, bool /*force*/ )
1739 // roland
1740 DEBUG_BLOCK
1742 Q3ListViewItem *pli = findItemInTree( playlist, 0 );
1743 if ( ! pli ) return -1;
1745 slotDoubleClicked( pli );
1746 return 0;
1747 // roland
1750 void PlaylistBrowser::addPlaylist( const QString &path, Q3ListViewItem *parent, bool force, bool imported )
1752 // this function adds a playlist to the playlist browser
1754 if( !m_polished )
1755 polish();
1757 QFile file( path );
1758 if( !file.exists() ) return;
1760 PlaylistEntry *playlist = findPlaylistEntry( path );
1762 if( playlist && force )
1763 playlist->load(); //reload the playlist
1765 if( imported ) {
1766 Q3ListViewItem *playlistImports = 0;
1767 //First try and find the imported folder
1768 for ( Q3ListViewItem *it = m_playlistCategory->firstChild(); it; it = it->nextSibling() )
1770 if ( dynamic_cast<PlaylistCategory*>( it ) && static_cast<PlaylistCategory*>( it )->isFolder() &&
1771 it->text( 0 ) == i18n( "Imported" ) )
1773 playlistImports = it;
1774 break;
1777 if ( !playlistImports ) //We didn't find the Imported folder, so create it.
1778 playlistImports = new PlaylistCategory( m_playlistCategory, 0, i18n("Imported") );
1779 parent = playlistImports;
1781 else if( !parent ) parent = static_cast<Q3ListViewItem*>(m_playlistCategory);
1783 if( !playlist ) {
1784 if( !m_playlistCategory || !m_playlistCategory->childCount() ) { //first child
1785 removeButton->setEnabled( true );
1786 renameButton->setEnabled( true );
1789 KUrl auxKURL;
1790 auxKURL.setPath(path);
1791 m_lastPlaylist = playlist = new PlaylistEntry( parent, 0, auxKURL );
1794 parent->setOpen( true );
1795 parent->sortChildItems( 0, true );
1796 m_listview->clearSelection();
1797 playlist->setSelected( true );
1800 bool PlaylistBrowser::savePlaylist( const QString &path, const Q3ValueList<KUrl> &in_urls,
1801 const Q3ValueList<QString> &titles, const Q3ValueList<int> &lengths,
1802 bool relative )
1804 if( path.isEmpty() )
1805 return false;
1807 QFile file( path );
1809 if( !file.open( QIODevice::WriteOnly ) )
1811 KMessageBox::sorry( MainWindow::self(), i18n( "Cannot write playlist (%1).").arg(path) );
1812 return false;
1815 QTextStream stream( &file );
1816 stream << "#EXTM3U\n";
1818 KUrl::List urls;
1819 for( int i = 0, n = in_urls.count(); i < n; ++i )
1821 const KUrl &url = in_urls[i];
1822 if( url.isLocalFile() && QFileInfo( url.path() ).isDir() )
1823 urls += recurse( url );
1824 else
1825 urls += url;
1828 for( int i = 0, n = urls.count(); i < n; ++i )
1830 const KUrl &url = urls[i];
1832 if( !titles.isEmpty() && !lengths.isEmpty() )
1834 stream << "#EXTINF:";
1835 stream << QString::number( lengths[i] );
1836 stream << ',';
1837 stream << titles[i];
1838 stream << '\n';
1840 if (url.protocol() == "file" ) {
1841 if ( relative ) {
1842 const QFileInfo fi(file);
1843 stream << KUrl::relativePath(fi.path(), url.path());
1844 } else
1845 stream << url.path();
1846 } else {
1847 stream << url.url();
1849 stream << "\n";
1852 file.close(); // Flushes the file, before we read it
1853 PlaylistBrowser::instance()->addPlaylist( path, 0, true );
1855 return true;
1858 void PlaylistBrowser::openPlaylist( Q3ListViewItem *parent ) //SLOT
1860 // open a file selector to add playlists to the playlist browser
1861 QStringList files;
1862 files = KFileDialog::getOpenFileNames( KUrl(), "*.m3u *.pls *.xspf|" + i18n("Playlist Files"), this, i18n("Import Playlists") );
1864 const QStringList::ConstIterator end = files.constEnd();
1865 for( QStringList::ConstIterator it = files.constBegin(); it != end; ++it )
1866 addPlaylist( *it, parent );
1868 savePlaylists();
1871 void PlaylistBrowser::savePlaylists()
1873 QFile file( playlistBrowserCache() );
1875 QDomDocument doc;
1876 QDomElement playlistsB = m_playlistCategory->xml();
1877 playlistsB.setAttribute( "product", "Amarok" );
1878 playlistsB.setAttribute( "version", APP_VERSION );
1879 playlistsB.setAttribute( "formatversion", "1.1" );
1880 QDomNode playlistsNode = doc.importNode( playlistsB, true );
1881 doc.appendChild( playlistsNode );
1883 QString temp( doc.toString() );
1885 // Only open the file after all data is ready. If it crashes, data is not lost!
1886 if ( !file.open( QIODevice::WriteOnly ) ) return;
1888 QTextStream stream( &file );
1889 stream.setCodec( "UTF8" );
1890 stream << "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n";
1891 stream << temp;
1894 bool PlaylistBrowser::deletePlaylists( Q3PtrList<PlaylistEntry> items )
1896 KUrl::List urls;
1897 oldForeachType( Q3PtrList<PlaylistEntry>, items )
1899 urls.append( (*it)->url() );
1901 if( !urls.isEmpty() )
1902 return deletePlaylists( urls );
1904 return false;
1907 bool PlaylistBrowser::deletePlaylists( KUrl::List items )
1909 if ( items.isEmpty() ) return false;
1911 // TODO We need to check which files have been deleted successfully
1912 // Avoid deleting dirs. See bug #122480
1913 for ( KUrl::List::iterator it = items.begin(), end = items.end(); it != end; ++it ) {
1914 if ( QFileInfo( (*it).path() ).isDir() ) {
1915 it = items.erase( it );
1916 continue;
1919 KIO::del( items );
1920 return true;
1923 void PlaylistBrowser::savePlaylist( PlaylistEntry *item )
1925 bool append = false;
1927 if( item->trackList().count() == 0 ) //the playlist hasn't been loaded so we append the dropped tracks
1928 append = true;
1930 //save the modified playlist in m3u or pls format
1931 const QString ext = fileExtension( item->url().path() );
1932 if( ext.toLower() == "m3u" )
1933 saveM3U( item, append );
1934 else if ( ext.toLower() == "xspf" )
1935 saveXSPF( item, append );
1936 else
1937 savePLS( item, append );
1941 *************************************************************************
1942 * General Methods
1943 *************************************************************************
1946 PlaylistBrowserEntry *
1947 PlaylistBrowser::findItem( QString &t, int c ) const
1949 return static_cast<PlaylistBrowserEntry *>( m_listview->findItem( t, c, Q3ListView::ExactMatch ) );
1952 bool PlaylistBrowser::createPlaylist( Q3ListViewItem *parent, bool current, QString title )
1954 if( title.isEmpty() ) title = i18n("Untitled");
1956 const QString path = PlaylistDialog::getSaveFileName( title );
1957 if( path.isEmpty() )
1958 return false;
1960 if( !parent )
1961 parent = static_cast<Q3ListViewItem *>( m_playlistCategory );
1963 if( current )
1965 if ( !Playlist::instance()->saveM3U( path ) ) {
1966 return false;
1969 else
1971 //Remove any items in Listview that have the same path as this one
1972 // Should only happen when overwriting a playlist
1973 Q3ListViewItem *item = parent->firstChild();
1974 while( item )
1976 if( static_cast<PlaylistEntry*>( item )->url() == path )
1978 Q3ListViewItem *todelete = item;
1979 item = item->nextSibling();
1980 delete todelete;
1982 else
1983 item = item->nextSibling();
1986 //Remove existing playlist if it exists
1987 if ( QFileInfo( path ).exists() )
1988 QFileInfo( path ).dir().remove( path );
1990 m_lastPlaylist = new PlaylistEntry( parent, 0, path );
1991 parent->sortChildItems( 0, true );
1994 savePlaylists();
1996 return true;
1999 void PlaylistBrowser::addSelectedToPlaylist( int options )
2001 if ( options == -1 )
2002 options = Playlist::Unique | Playlist::Append;
2004 KUrl::List list;
2006 Q3ListViewItemIterator it( m_listview, Q3ListViewItemIterator::Selected );
2007 for( ; it.current(); ++it )
2009 #define item (*it)
2010 if ( isPlaylist( item ) )
2011 list << static_cast<PlaylistEntry*>(item)->url();
2013 else if( isLastFm( item ) )
2014 list << static_cast<LastFmEntry*>(item)->url();
2016 else if ( isStream( item ) )
2017 list << static_cast<StreamEntry*>(item)->url();
2019 else if ( isPodcastChannel( item ) )
2021 #define channel static_cast<PodcastChannel*>(item)
2022 if( !channel->isPolished() )
2023 channel->load();
2024 #undef channel
2025 KUrl::List _list;
2026 Q3ListViewItem *child = item->firstChild();
2027 while( child )
2029 #define child static_cast<PodcastEpisode *>(child)
2030 child->isOnDisk() ?
2031 _list.prepend( child->localUrl() ):
2032 _list.prepend( child->url() );
2033 #undef child
2034 child = child->nextSibling();
2036 list += _list ;
2039 else if ( isPodcastEpisode( item ) )
2041 #define pod static_cast<PodcastEpisode*>(item)
2042 if( pod->isOnDisk() )
2043 list << pod->localUrl();
2044 else
2045 list << pod->url();
2046 #undef pod
2049 else if ( isPlaylistTrackItem( item ) )
2050 list << static_cast<PlaylistTrackItem*>(item)->url();
2051 #undef item
2054 if( !list.isEmpty() )
2055 The::playlistModel()->insertMedia( list, options );
2058 void
2059 PlaylistBrowser::invokeItem( Q3ListViewItem* i, const QPoint& point, int column ) //SLOT
2061 if( column == -1 )
2062 return;
2064 PlaylistBrowserView *view = getListView();
2066 QPoint p = mapFromGlobal( point );
2067 if ( p.x() > view->header()->sectionPos( view->header()->mapToIndex( 0 ) ) + view->treeStepSize() * ( i->depth() + ( view->rootIsDecorated() ? 1 : 0) ) + view->itemMargin()
2068 || p.x() < view->header()->sectionPos( view->header()->mapToIndex( 0 ) ) )
2069 slotDoubleClicked( i );
2072 void PlaylistBrowser::slotDoubleClicked( Q3ListViewItem *item ) //SLOT
2074 if( !item ) return;
2075 PlaylistBrowserEntry *entry = dynamic_cast<PlaylistBrowserEntry*>(item);
2076 if ( entry )
2077 entry->slotDoubleClicked();
2080 void PlaylistBrowser::collectionScanDone()
2082 if( !m_polished || CollectionDB::instance()->isEmpty() )
2084 return;
2086 else if( !m_smartCategory )
2088 m_smartCategory = loadSmartPlaylists();
2089 loadDefaultSmartPlaylists();
2090 m_smartCategory->setOpen( true );
2094 void PlaylistBrowser::removeSelectedItems() //SLOT
2096 // this function remove selected playlists and tracks
2098 int playlistCount = 0;
2099 int trackCount = 0;
2100 int streamCount = 0;
2101 int smartyCount = 0;
2102 int dynamicCount = 0;
2103 int podcastCount = 0;
2104 int folderCount = 0;
2105 int lastfmCount = 0;
2107 Q3PtrList<PlaylistEntry> playlistsToDelete;
2108 Q3PtrList<PodcastChannel> podcastsToDelete;
2110 Q3PtrList<PlaylistCategory> playlistFoldersToDelete;
2111 Q3PtrList<PlaylistCategory> podcastFoldersToDelete;
2113 //remove currentItem, no matter if selected or not
2114 m_listview->setSelected( m_listview->currentItem(), true );
2116 Q3PtrList<Q3ListViewItem> selected;
2117 Q3ListViewItemIterator it( m_listview, Q3ListViewItemIterator::Selected );
2118 for( ; it.current(); ++it )
2120 if( !static_cast<PlaylistBrowserEntry*>(*it)->isKept() )
2121 continue;
2123 if( isCategory( *it ) && !static_cast<PlaylistCategory*>(*it)->isFolder() ) //its a base category
2124 continue;
2126 // if the playlist containing this item is already selected the current item will be skipped
2127 // it will be deleted from the parent
2128 Q3ListViewItem *parent = it.current()->parent();
2130 if( parent && parent->isSelected() ) //parent will remove children
2131 continue;
2133 if (parent) {
2134 while( parent->parent() && static_cast<PlaylistBrowserEntry*>(parent)->isKept() )
2135 parent = parent->parent();
2138 if( parent && !static_cast<PlaylistBrowserEntry*>(parent)->isKept() )
2139 continue;
2141 switch( (*it)->rtti() )
2143 case PlaylistEntry::RTTI:
2144 playlistsToDelete.append( static_cast<PlaylistEntry*>(*it) );
2145 playlistCount++;
2146 continue; // don't add the folder to selected, else it will be deleted twice
2148 case PlaylistTrackItem::RTTI:
2149 trackCount++;
2150 break;
2152 case LastFmEntry::RTTI:
2153 lastfmCount++;
2154 break;
2156 case StreamEntry::RTTI:
2157 streamCount++;
2158 break;
2160 case DynamicEntry::RTTI:
2161 dynamicCount++;
2162 break;
2164 case SmartPlaylist::RTTI:
2165 smartyCount++;
2166 break;
2168 case PodcastChannel::RTTI:
2169 podcastCount++;
2170 podcastsToDelete.append( static_cast<PodcastChannel*>(*it) );
2171 case PodcastEpisode::RTTI: //episodes can't be removed
2172 continue; // don't add the folder to selected, else it will be deleted twice
2174 case PlaylistCategory::RTTI:
2175 folderCount++;
2176 if( parent == m_playlistCategory )
2178 for( Q3ListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() )
2180 if( isCategory( ch ) )
2182 folderCount++;
2183 playlistFoldersToDelete.append( static_cast<PlaylistCategory*>(ch) );
2185 else
2187 playlistCount++;
2188 playlistsToDelete.append( static_cast<PlaylistEntry*>(ch) );
2191 playlistFoldersToDelete.append( static_cast<PlaylistCategory*>(*it) );
2192 continue; // don't add the folder to selected, else it will be deleted twice
2194 else if( parent == m_podcastCategory )
2196 for( Q3ListViewItem *ch = (*it)->firstChild(); ch; ch = ch->nextSibling() )
2198 if( isCategory( ch ) )
2200 folderCount++;
2201 podcastFoldersToDelete.append( static_cast<PlaylistCategory*>(ch) );
2203 else
2205 podcastCount++;
2206 podcastsToDelete.append( static_cast<PodcastChannel*>(ch) );
2209 podcastFoldersToDelete.append( static_cast<PlaylistCategory*>(*it) );
2210 continue; // don't add the folder to selected, else it will be deleted twice
2213 default:
2214 break;
2217 selected.append( it.current() );
2220 int totalCount = playlistCount + smartyCount + dynamicCount +
2221 streamCount + podcastCount + folderCount + lastfmCount;
2223 if( selected.isEmpty() && !totalCount ) return;
2225 QString message = i18n( "<p>You have selected:<ul>" );
2227 if( playlistCount ) message += "<li>" + i18np( "1 playlist", "%1 playlists", playlistCount ) + "</li>";
2229 if( smartyCount ) message += "<li>" + i18np( "1 smart playlist", "%1 smart playlists", smartyCount ) + "</li>";
2231 if( dynamicCount ) message += "<li>" + i18np( "1 dynamic playlist", "%1 dynamic playlists", dynamicCount ) + "</li>";
2233 if( streamCount ) message += "<li>" + i18np( "1 stream", "%1 streams", streamCount ) + "</li>";
2235 if( podcastCount ) message += "<li>" + i18np( "1 podcast", "%1 podcasts", podcastCount ) + "</li>";
2237 if( folderCount ) message += "<li>" + i18np( "1 folder", "%1 folders", folderCount ) + "</li>";
2239 if( lastfmCount ) message += "<li>" + i18np( "1 last.fm stream", "%1 last.fm streams", lastfmCount ) + "</li>";
2241 message += i18n( "</ul><br>to be <b>irreversibly</b> deleted.</p>" );
2243 if( podcastCount )
2244 message += i18n( "<br><p>All downloaded podcast episodes will also be deleted.</p>" );
2246 if( totalCount > 0 )
2248 int button = KMessageBox::warningContinueCancel( this, message, QString(), KStandardGuiItem::del() );
2249 if( button != KMessageBox::Continue )
2250 return;
2253 oldForeachType( Q3PtrList<Q3ListViewItem>, selected )
2255 if ( isPlaylistTrackItem( *it ) )
2257 static_cast<PlaylistEntry*>( (*it)->parent() )->removeTrack( (*it) );
2258 continue;
2260 if ( isDynamic( *it ) )
2261 static_cast<DynamicEntry*>( *it )->deleting();
2262 delete (*it);
2265 // used for deleting playlists first, then folders.
2266 if( playlistCount )
2268 if( deletePlaylists( playlistsToDelete ) )
2270 oldForeachType( Q3PtrList<PlaylistEntry>, playlistsToDelete )
2272 m_dynamicEntries.remove(*it);
2273 delete (*it);
2278 if( podcastCount )
2280 if( deletePodcasts( podcastsToDelete ) )
2281 oldForeachType( Q3PtrList<PodcastChannel>, podcastsToDelete )
2282 delete (*it);
2285 oldForeachType( Q3PtrList<PlaylistCategory>, playlistFoldersToDelete )
2286 delete (*it);
2288 oldForeachType( Q3PtrList<PlaylistCategory>, podcastFoldersToDelete )
2289 removePodcastFolder( *it );
2291 if( playlistCount || trackCount )
2292 savePlaylists();
2294 if( streamCount ) saveStreams();
2295 if( smartyCount ) saveSmartPlaylists();
2296 if( dynamicCount ) saveDynamics();
2297 if( lastfmCount ) saveLastFm();
2300 // remove podcast folders. we need to do this recursively to ensure all children are removed from the db
2301 void PlaylistBrowser::removePodcastFolder( PlaylistCategory *item )
2303 if( !item ) return;
2304 if( !item->childCount() )
2306 CollectionDB::instance()->removePodcastFolder( item->id() );
2307 delete item;
2308 return;
2311 Q3ListViewItem *child = item->firstChild();
2312 while( child )
2314 Q3ListViewItem *nextChild = 0;
2315 if( isPodcastChannel( child ) )
2317 #define child static_cast<PodcastChannel*>(child)
2318 nextChild = child->nextSibling();
2319 CollectionDB::instance()->removePodcastChannel( child->url() );
2320 m_podcastItemsToScan.remove( child );
2321 #undef child
2323 else if( isCategory( child ) )
2325 nextChild = child->nextSibling();
2326 removePodcastFolder( static_cast<PlaylistCategory*>(child) );
2329 child = nextChild;
2331 CollectionDB::instance()->removePodcastFolder( item->id() );
2332 delete item;
2335 void PlaylistBrowser::renameSelectedItem() //SLOT
2337 Q3ListViewItem *item = m_listview->currentItem();
2338 if( !item ) return;
2340 if( item == m_randomDynamic || item == m_suggestedDynamic )
2341 return;
2343 PlaylistBrowserEntry *entry = dynamic_cast<PlaylistBrowserEntry*>( item );
2344 if ( entry )
2345 entry->slotRenameItem();
2349 void PlaylistBrowser::renamePlaylist( Q3ListViewItem* item, const QString& newName, int ) //SLOT
2351 PlaylistBrowserEntry *entry = dynamic_cast<PlaylistBrowserEntry*>( item );
2352 if ( entry )
2353 entry->slotPostRenameItem( newName );
2357 void PlaylistBrowser::saveM3U( PlaylistEntry *item, bool append )
2359 QFile file( item->url().path() );
2361 if( append ? file.open( QIODevice::WriteOnly | QIODevice::Append ) : file.open( QIODevice::WriteOnly ) )
2363 QTextStream stream( &file );
2364 if( !append )
2365 stream << "#EXTM3U\n";
2366 Q3PtrList<TrackItemInfo> trackList = append ? item->droppedTracks() : item->trackList();
2367 for( TrackItemInfo *info = trackList.first(); info; info = trackList.next() )
2369 stream << "#EXTINF:";
2370 stream << info->length();
2371 stream << ',';
2372 stream << info->title();
2373 stream << '\n';
2374 stream << (info->url().protocol() == "file" ? info->url().path() : info->url().url());
2375 stream << "\n";
2378 file.close();
2382 void PlaylistBrowser::saveXSPF( PlaylistEntry *item, bool append )
2384 XSPFPlaylist playlist;
2386 playlist.setCreator( "Amarok" );
2387 playlist.setTitle( item->text(0) );
2389 XSPFtrackList list;
2390 //PORT 2.0
2391 //XSPFPlaylist has been ported to Meta::, PlaylistBrowser needs to be
2392 #if 0
2393 Q3PtrList<TrackItemInfo> trackList = append ? item->droppedTracks() : item->trackList();
2394 for( TrackItemInfo *info = trackList.first(); info; info = trackList.next() )
2396 XSPFtrack track;
2397 MetaBundle b( info->url() );
2398 track.creator = b.artist();
2399 track.title = b.title();
2400 track.location = b.url().url();
2401 list.append( track );
2404 playlist.setTrackList( list, append );
2405 #endif
2406 QFile file( item->url().path() );
2407 file.open( QIODevice::WriteOnly );
2409 QTextStream stream ( &file );
2411 playlist.save( stream, 2 );
2413 file.close();
2417 void PlaylistBrowser::savePLS( PlaylistEntry *item, bool append )
2419 QFile file( item->url().path() );
2421 if( append ? file.open( QIODevice::WriteOnly | QIODevice::Append ) : file.open( QIODevice::WriteOnly ) )
2423 QTextStream stream( &file );
2424 Q3PtrList<TrackItemInfo> trackList = append ? item->droppedTracks() : item->trackList();
2425 stream << "NumberOfEntries=" << trackList.count() << endl;
2426 int c=1;
2427 for( TrackItemInfo *info = trackList.first(); info; info = trackList.next(), ++c )
2429 stream << "File" << c << "=";
2430 stream << (info->url().protocol() == "file" ? info->url().path() : info->url().url());
2431 stream << "\nTitle" << c << "=";
2432 stream << info->title();
2433 stream << "\nLength" << c << "=";
2434 stream << info->length();
2435 stream << "\n";
2438 stream << "Version=2\n";
2439 file.close();
2443 #include <kdirlister.h>
2444 #include <QEventLoop>
2445 #include "playlistloader.h"
2446 //this function (C) Copyright 2003-4 Max Howell, (C) Copyright 2004 Mark Kretschmann
2447 KUrl::List PlaylistBrowser::recurse( const KUrl &url )
2449 typedef QMap<QString, KUrl> FileMap;
2451 KDirLister lister( false );
2452 lister.setAutoUpdate( false );
2453 lister.setAutoErrorHandlingEnabled( false, 0 );
2454 lister.openUrl( url );
2456 while( !lister.isFinished() )
2457 kapp->processEvents( QEventLoop::ExcludeUserInput );
2459 KFileItemList items = lister.items(); //returns QPtrList, so we MUST only do it once!
2460 KUrl::List urls;
2461 FileMap files;
2462 oldForeachType( KFileItemList, items ) {
2463 if( (*it)->isFile() ) { files[(*it)->name()] = (*it)->url(); continue; }
2464 if( (*it)->isDir() ) urls += recurse( (*it)->url() );
2467 oldForeachType( FileMap, files )
2468 // users often have playlist files that reflect directories
2469 // higher up, or stuff in this directory. Don't add them as
2470 // it produces double entries
2471 if( !PlaylistFile::isPlaylistFile( (*it).fileName() ) )
2472 urls += *it;
2474 return urls;
2478 void PlaylistBrowser::currentItemChanged( Q3ListViewItem *item ) //SLOT
2480 // rename remove and delete buttons are disabled if there are no playlists
2481 // rename and delete buttons are disabled for track items
2483 bool enable_remove = false;
2484 bool enable_rename = false;
2486 if ( !item )
2487 goto enable_buttons;
2489 if ( isCategory( item ) )
2491 if( static_cast<PlaylistCategory*>(item)->isFolder() &&
2492 static_cast<PlaylistCategory*>(item)->isKept() )
2493 enable_remove = enable_rename = true;
2495 else if ( isPodcastChannel( item ) )
2497 enable_remove = true;
2498 enable_rename = false;
2500 else if ( !isPodcastEpisode( item ) )
2501 enable_remove = enable_rename = static_cast<PlaylistCategory*>(item)->isKept();
2503 static_cast<PlaylistBrowserEntry*>(item)->updateInfo();
2505 enable_buttons:
2507 removeButton->setEnabled( enable_remove );
2508 renameButton->setEnabled( enable_rename );
2512 void PlaylistBrowser::customEvent( QEvent *e )
2514 // If a playlist is found in collection folders it will be automatically added to the playlist browser
2515 // The ScanController sends a PlaylistFoundEvent when a playlist is found.
2517 ScanController::PlaylistFoundEvent* p = static_cast<ScanController::PlaylistFoundEvent*>( e );
2518 addPlaylist( p->path(), 0, false, true );
2522 void PlaylistBrowser::slotAddMenu( int id ) //SLOT
2524 switch( id )
2526 case STREAM:
2527 addStream();
2528 break;
2530 case SMARTPLAYLIST:
2531 addSmartPlaylist();
2532 break;
2534 case PODCAST:
2535 addPodcast();
2536 break;
2538 case ADDDYNAMIC:
2539 ConfigDynamic::dynamicDialog(this);
2540 break;
2545 void PlaylistBrowser::slotAddPlaylistMenu( int id ) //SLOT
2547 switch( id )
2549 case PLAYLIST:
2550 createPlaylist( 0/*base cat*/, false/*make empty*/ );
2551 break;
2553 case PLAYLIST_IMPORT:
2554 openPlaylist();
2555 break;
2561 ************************
2562 * Context Menu Entries
2563 ************************
2566 void PlaylistBrowser::showContextMenu( Q3ListViewItem *item, const QPoint &p, int ) //SLOT
2568 if( !item ) return;
2570 PlaylistBrowserEntry *entry = dynamic_cast<PlaylistBrowserEntry*>( item );
2571 if ( entry )
2572 entry->showContextMenu( p );
2575 /////////////////////////////////////////////////////////////////////////////
2576 // CLASS PlaylistBrowserView
2577 ////////////////////////////////////////////////////////////////////////////
2579 PlaylistBrowserView::PlaylistBrowserView( QWidget *parent, const char *name )
2580 : K3ListView( parent )
2581 , m_marker( 0 )
2583 setObjectName( name );
2584 addColumn( i18n("Playlists") );
2586 setSelectionMode( Q3ListView::Extended );
2587 setResizeMode( Q3ListView::AllColumns );
2588 setShowSortIndicator( true );
2589 setRootIsDecorated( true );
2591 setDropVisualizer( true ); //the visualizer (a line marker) is drawn when dragging over tracks
2592 setDropHighlighter( true ); //and the highligther (a focus rect) is drawn when dragging over playlists
2593 setDropVisualizerWidth( 3 );
2594 setAcceptDrops( true );
2596 setTreeStepSize( 20 );
2598 connect( this, SIGNAL( mouseButtonPressed ( int, Q3ListViewItem *, const QPoint &, int ) ),
2599 this, SLOT( mousePressed( int, Q3ListViewItem *, const QPoint &, int ) ) );
2601 //TODO moving tracks
2602 //connect( this, SIGNAL( moved(QListViewItem *, QListViewItem *, QListViewItem * )),
2603 // this, SLOT( itemMoved(QListViewItem *, QListViewItem *, QListViewItem * )));
2606 PlaylistBrowserView::~PlaylistBrowserView() { }
2608 void PlaylistBrowserView::contentsDragEnterEvent( QDragEnterEvent *e )
2610 e->accept( e->source() == viewport() || K3URLDrag::canDecode( e ) );
2613 void PlaylistBrowserView::contentsDragMoveEvent( QDragMoveEvent* e )
2615 //Get the closest item _before_ the cursor
2616 const QPoint p = contentsToViewport( e->pos() );
2617 Q3ListViewItem *item = itemAt( p );
2618 if( !item ) {
2619 eraseMarker();
2620 return;
2623 //only for track items (for playlist items we draw the highlighter)
2624 if( isPlaylistTrackItem( item ) )
2625 item = item->itemAbove();
2627 if( item != m_marker )
2629 eraseMarker();
2630 m_marker = item;
2631 viewportPaintEvent( 0 );
2635 void PlaylistBrowserView::contentsDragLeaveEvent( QDragLeaveEvent* )
2637 eraseMarker();
2641 void PlaylistBrowserView::contentsDropEvent( QDropEvent *e )
2643 Q3ListViewItem *parent = 0;
2644 Q3ListViewItem *after;
2646 const QPoint p = contentsToViewport( e->pos() );
2647 Q3ListViewItem *item = itemAt( p );
2648 if( !item ) {
2649 eraseMarker();
2650 return;
2653 if( !isPlaylist( item ) )
2654 findDrop( e->pos(), parent, after );
2656 eraseMarker();
2658 if( e->source() == this )
2660 moveSelectedItems( item ); // D&D sucks, do it ourselves
2662 else {
2663 KUrl::List decodedList;
2664 Q3ValueList<MetaBundle> bundles;
2665 if( K3URLDrag::decode( e, decodedList ) )
2667 KUrl::List::ConstIterator it = decodedList.begin();
2668 MetaBundle first( *it );
2669 const QString album = first.album();
2670 const QString artist = first.artist();
2672 int suggestion = !album.trimmed().isEmpty() ? 1 : !artist.trimmed().isEmpty() ? 2 : 3;
2674 for ( ; it != decodedList.end(); ++it )
2676 if( isCategory(item) )
2677 { // check if it is podcast category
2678 Q3ListViewItem *cat = item;
2679 while( isCategory(cat) && cat!=PlaylistBrowser::instance()->podcastCategory() )
2680 cat = cat->parent();
2682 if( cat == PlaylistBrowser::instance()->podcastCategory() )
2683 PlaylistBrowser::instance()->addPodcast(*it, item);
2684 continue;
2687 QString filename = (*it).fileName();
2689 if( filename.endsWith("m3u") || filename.endsWith("pls") )
2690 PlaylistBrowser::instance()->addPlaylist( (*it).path() );
2691 else //TODO: check canDecode ?
2693 MetaBundle mb(*it);
2694 bundles.append( mb );
2695 if( suggestion == 1 && mb.album()->toLower().trimmed() != album.toLower().trimmed() )
2696 suggestion = 2;
2697 if( suggestion == 2 && mb.artist()->toLower().trimmed() != artist.toLower().trimmed() )
2698 suggestion = 3;
2702 if( bundles.isEmpty() ) return;
2704 if( parent && isPlaylist( parent ) ) {
2705 //insert the dropped tracks
2706 PlaylistEntry *playlist = static_cast<PlaylistEntry *>( parent );
2707 playlist->insertTracks( after, bundles );
2709 else //dropped on a playlist item
2711 Q3ListViewItem *parent = item;
2712 bool isPlaylistFolder = false;
2714 while( parent )
2716 if( parent == PlaylistBrowser::instance()->m_playlistCategory )
2718 isPlaylistFolder = true;
2719 break;
2721 parent = parent->parent();
2724 if( isPlaylist( item ) ) {
2725 PlaylistEntry *playlist = static_cast<PlaylistEntry *>( item );
2726 //append the dropped tracks
2727 playlist->insertTracks( 0, bundles );
2729 else if( isCategory( item ) && isPlaylistFolder )
2731 PlaylistBrowser *pb = PlaylistBrowser::instance();
2732 QString title = suggestion == 1 ? album
2733 : suggestion == 2 ? artist
2734 : QString();
2735 if ( pb->createPlaylist( item, false, title ) )
2736 pb->m_lastPlaylist->insertTracks( 0, bundles );
2740 else
2741 e->ignore();
2746 void PlaylistBrowserView::eraseMarker() //SLOT
2748 if( m_marker )
2750 QRect spot;
2751 if( isPlaylist( m_marker ) )
2752 spot = drawItemHighlighter( 0, m_marker );
2753 else
2754 spot = drawDropVisualizer( 0, 0, m_marker );
2756 m_marker = 0;
2757 viewport()->repaint( spot, false );
2761 void PlaylistBrowserView::viewportPaintEvent( QPaintEvent *e )
2763 if( e ) K3ListView::viewportPaintEvent( e ); //we call with 0 in contentsDropEvent()
2765 if( m_marker )
2767 QPainter painter( viewport() );
2768 if( isPlaylist( m_marker ) ) //when dragging on a playlist we draw a focus rect
2769 drawItemHighlighter( &painter, m_marker );
2770 else //when dragging on a track we draw a line marker
2771 painter.fillRect( drawDropVisualizer( 0, 0, m_marker ),
2772 QBrush( colorGroup().highlight(), Qt::Dense4Pattern ) );
2776 void PlaylistBrowserView::mousePressed( int button, Q3ListViewItem *item, const QPoint &pnt, int ) //SLOT
2778 // this function expande/collapse the playlist if the +/- symbol has been pressed
2779 // and show the save menu if the save icon has been pressed
2781 if( !item || button != Qt::LeftButton ) return;
2783 if( isPlaylist( item ) )
2785 QPoint p = mapFromGlobal( pnt );
2786 p.setY( p.y() - header()->height() );
2788 QRect itemrect = itemRect( item );
2790 QRect expandRect = QRect( 4, itemrect.y() + (item->height()/2) - 5, 15, 15 );
2791 if( expandRect.contains( p ) ) { //expand symbol clicked
2792 setOpen( item, !item->isOpen() );
2793 return;
2798 void PlaylistBrowserView::moveSelectedItems( Q3ListViewItem *newParent )
2800 if( !newParent || isDynamic( newParent ) || isPodcastChannel( newParent ) ||
2801 isSmartPlaylist( newParent ) || isPodcastEpisode( newParent ) )
2802 return;
2804 #define newParent static_cast<PlaylistBrowserEntry*>(newParent)
2805 if( !newParent->isKept() )
2806 return;
2807 #undef newParent
2809 Q3PtrList<Q3ListViewItem> selected;
2810 Q3ListViewItemIterator it( this, Q3ListViewItemIterator::Selected );
2811 for( ; it.current(); ++it )
2813 if( !(*it)->parent() ) //must be a base category we are draggin'
2814 continue;
2816 selected.append( *it );
2819 Q3ListViewItem *after=0;
2820 for( Q3ListViewItem *item = selected.first(); item; item = selected.next() )
2822 Q3ListViewItem *itemParent = item->parent();
2823 if( isPlaylistTrackItem( item ) )
2825 if( isPlaylistTrackItem( newParent ) )
2827 if( !after && newParent != newParent->parent()->firstChild() )
2828 after = newParent->itemAbove();
2830 newParent = static_cast<PlaylistEntry*>(newParent->parent());
2832 else if( !isPlaylist( newParent ) )
2833 continue;
2836 #define newParent static_cast<PlaylistEntry*>(newParent)
2837 newParent->insertTracks( after, KUrl::List( static_cast<PlaylistTrackItem*>(item)->url() ));
2838 #undef newParent
2839 #define itemParent static_cast<PlaylistEntry*>(itemParent)
2840 itemParent->removeTrack( static_cast<PlaylistTrackItem*>(item) );
2841 #undef itemParent
2842 continue;
2844 else if( !isCategory( newParent ) )
2845 continue;
2847 Q3ListViewItem *base = newParent;
2848 while( base->parent() )
2849 base = base->parent();
2851 if( base == PlaylistBrowser::instance()->m_playlistCategory && isPlaylist( item ) ||
2852 base == PlaylistBrowser::instance()->m_streamsCategory && isStream( item ) ||
2853 base == PlaylistBrowser::instance()->m_smartCategory && isSmartPlaylist( item ) ||
2854 base == PlaylistBrowser::instance()->m_dynamicCategory && isDynamic( item ) )
2856 itemParent->takeItem( item );
2857 newParent->insertItem( item );
2858 newParent->sortChildItems( 0, true );
2860 else if( base == PlaylistBrowser::instance()->m_podcastCategory && isPodcastChannel( item ) )
2862 #define item static_cast<PodcastChannel*>(item)
2863 item->setParent( static_cast<PlaylistCategory*>(newParent) );
2864 #undef item
2869 void PlaylistBrowserView::rename( Q3ListViewItem *item, int c )
2871 K3ListView::rename( item, c );
2873 QRect rect( itemRect( item ) );
2874 int fieldX = rect.x() + treeStepSize() + 2;
2875 int fieldW = rect.width() - treeStepSize() - 2;
2877 KLineEdit *renameEdit = renameLineEdit();
2878 renameEdit->setGeometry( fieldX, rect.y(), fieldW, rect.height() );
2879 renameEdit->show();
2882 void PlaylistBrowserView::keyPressEvent( QKeyEvent *e )
2884 switch( e->key() ) {
2885 case Qt::Key_Space: //load
2886 PlaylistBrowser::instance()->slotDoubleClicked( currentItem() );
2887 break;
2889 case Qt::ShiftModifier+Qt::Key_Delete: //delete
2890 case Qt::Key_Delete: //remove
2891 PlaylistBrowser::instance()->removeSelectedItems();
2892 break;
2894 case Qt::Key_F2: //rename
2895 PlaylistBrowser::instance()->renameSelectedItem();
2896 break;
2898 default:
2899 K3ListView::keyPressEvent( e );
2900 break;
2905 void PlaylistBrowserView::startDrag()
2907 DEBUG_BLOCK
2909 KUrl::List urls;
2910 KUrl::List itemList; // this is for CollectionDB::createDragPixmap()
2911 KUrl::List podList; // used to add podcast episodes of the same channel in reverse order (usability)
2912 PodcastEpisode *lastPodcastEpisode = 0; // keep track of the last podcastepisode we visited.
2913 K3MultipleDrag *drag = new K3MultipleDrag( this );
2915 Q3ListViewItemIterator it( this, Q3ListViewItemIterator::Selected );
2916 QString pixText;
2917 uint count = 0;
2919 for( ; it.current(); ++it )
2921 if( !isPodcastEpisode( *it ) && !podList.isEmpty() )
2922 { // we left the podcast channel, so append those items we iterated over
2923 urls += podList;
2924 podList.clear();
2927 if( isPlaylist( *it ) )
2929 urls += static_cast<PlaylistEntry*>(*it)->url();
2930 itemList += static_cast<PlaylistEntry*>(*it)->url();
2931 pixText = (*it)->text(0);
2934 else if( isStream( *it ) )
2936 urls += static_cast<StreamEntry*>(*it)->url();
2937 itemList += KUrl( "stream://" );
2938 pixText = (*it)->text(0);
2941 else if( isLastFm( *it ) )
2943 urls += static_cast<LastFmEntry*>(*it)->url();
2944 itemList += static_cast<LastFmEntry*>(*it)->url();
2945 pixText = (*it)->text(0);
2948 else if( isPodcastEpisode( *it ) )
2950 if( (*it)->parent()->isSelected() ) continue;
2951 if( !podList.isEmpty() && lastPodcastEpisode && lastPodcastEpisode->Q3ListViewItem::parent() != (*it)->parent() )
2952 { // we moved onto a new podcast channel
2953 urls += podList;
2954 podList.clear();
2956 #define item static_cast<PodcastEpisode *>(*it)
2957 if( item->isOnDisk() )
2959 podList.prepend( item->localUrl() );
2960 itemList += item->url();
2962 else
2964 podList.prepend( item->url() );
2965 itemList += item->url();
2967 lastPodcastEpisode = item;
2968 pixText = (*it)->text(0);
2969 #undef item
2971 else if( isPodcastChannel( *it ) )
2973 #define item static_cast<PodcastChannel *>(*it)
2974 if( !item->isPolished() )
2975 item->load();
2977 Q3ListViewItem *child = item->firstChild();
2978 KUrl::List tmp;
2979 // we add the podcasts in reverse, its much nicer to add them chronologically :)
2980 while( child )
2982 PodcastEpisode *pe = static_cast<PodcastEpisode*>( child );
2983 if( pe->isOnDisk() )
2984 tmp.prepend( pe->localUrl() );
2985 else
2986 tmp.prepend( pe->url() );
2987 child = child->nextSibling();
2989 urls += tmp;
2990 itemList += KUrl( item->url().url() );
2991 pixText = (*it)->text(0);
2992 #undef item
2995 else if( isSmartPlaylist( *it ) )
2997 SmartPlaylist *item = static_cast<SmartPlaylist*>( *it );
2999 if( !item->query().isEmpty() )
3001 Q3TextDrag *textdrag = new Q3TextDrag( item->text(0) + '\n' + item->query(), 0 );
3002 textdrag->setSubtype( "amarok-sql" );
3003 drag->addDragObject( textdrag );
3005 itemList += KUrl( QString("smartplaylist://%1").arg( item->text(0) ) );
3006 pixText = (*it)->text(0);
3009 else if( isDynamic( *it ) )
3011 DynamicEntry *item = static_cast<DynamicEntry*>( *it );
3013 // Serialize pointer to string
3014 const QString str = QString::number( reinterpret_cast<qulonglong>( item ) );
3016 Q3TextDrag *textdrag = new Q3TextDrag( str, 0 );
3017 textdrag->setSubtype( "dynamic" );
3018 drag->addDragObject( textdrag );
3019 itemList += KUrl( QString("dynamic://%1").arg( item->text(0) ) );
3020 pixText = (*it)->text(0);
3023 else if( isPlaylistTrackItem( *it ) )
3025 if( (*it)->parent()->isSelected() ) continue;
3026 urls += static_cast<PlaylistTrackItem*>(*it)->url();
3027 itemList += static_cast<PlaylistTrackItem*>(*it)->url();
3029 count++;
3032 if( !podList.isEmpty() )
3033 urls += podList;
3035 if( count > 1 ) pixText.clear();
3037 drag->addDragObject( new K3URLDrag( urls, viewport() ) );
3038 drag->setPixmap( CollectionDB::createDragPixmap( itemList, pixText ),
3039 QPoint( CollectionDB::DRAGPIXMAP_OFFSET_X, CollectionDB::DRAGPIXMAP_OFFSET_Y ) );
3040 drag->dragCopy();
3043 /////////////////////////////////////////////////////////////////////////////
3044 // CLASS PlaylistDialog
3045 ////////////////////////////////////////////////////////////////////////////
3047 QString PlaylistDialog::getSaveFileName( const QString &suggestion, bool proposeOverwriting ) //static
3049 PlaylistDialog dialog;
3050 if( !suggestion.isEmpty() )
3052 QString path = Amarok::saveLocation("playlists/") + "%1" + ".m3u";
3053 if( QFileInfo( path.arg( suggestion ) ).exists() && !proposeOverwriting )
3055 int n = 2;
3056 while( QFileInfo( path.arg( i18n( "%1 (%2)", suggestion, QString::number( n ) ) ) ).exists() )
3057 n++;
3058 dialog.edit->setText( i18n( "%1 (%2)", suggestion, QString::number( n ) ) );
3060 else
3061 dialog.edit->setText( suggestion );
3063 if( dialog.exec() == Accepted )
3064 return dialog.result;
3065 return QString();
3068 PlaylistDialog::PlaylistDialog()
3069 : KDialog( MainWindow::self() )
3070 , customChosen( false )
3072 setCaption( i18n( "Save Playlist" ) );
3073 setModal( true );
3074 setButtons( Ok | Cancel | User1 );
3075 setDefaultButton( Ok );
3076 showButtonSeparator( false );
3077 setButtonGuiItem( User1, KGuiItem( i18n( "Save to location..." ) ) ); //KIcon( Amarok::icon( "files" ) )
3080 KVBox *vbox = new KVBox( this );
3081 setMainWidget( vbox );
3083 QLabel *label = new QLabel( i18n( "&Enter a name for the playlist:" ), vbox );
3084 edit = new KLineEdit( vbox );
3085 edit->setFocus();
3086 label->setBuddy( edit );
3087 enableButtonOk( false );
3088 connect( edit, SIGNAL( textChanged( const QString & ) ),
3089 this, SLOT( slotTextChanged( const QString& ) ) );
3090 connect( this, SIGNAL( user1Clicked() ), SLOT( slotCustomPath() ) );
3091 connect(this,SIGNAL(okClicked()),this,SLOT(slotOk()));
3094 void PlaylistDialog::slotOk()
3096 // TODO Remove this hack for 1.2. It's needed because playlists was a file once.
3097 QString folder = Amarok::saveLocation( "playlists" );
3098 QFileInfo info( folder );
3099 if ( !info.isDir() ) QFile::remove( folder );
3101 if( !customChosen && !edit->text().isEmpty() )
3102 result = Amarok::saveLocation( "playlists/" ) + edit->text() + ".m3u";
3104 if( !QFileInfo( result ).exists() ||
3105 KMessageBox::warningContinueCancel(
3106 MainWindow::self(),
3107 i18n( "A playlist named \"%1\" already exists. Do you want to overwrite it?", edit->text() ),
3108 i18n( "Overwrite Playlist?" ), KGuiItem( i18n( "Overwrite" ) ) ) == KMessageBox::Continue )
3110 //KDialog::slotOk();
3111 slotButtonClicked( Ok );
3115 void PlaylistDialog::slotTextChanged( const QString &s )
3117 enableButtonOk( !s.isEmpty() );
3120 void PlaylistDialog::slotCustomPath()
3122 result = KFileDialog::getSaveFileName( KUrl( "kfiledialog:///saveplaylists" ), "*.m3u" );
3123 if( !result.isNull() )
3125 edit->setText( result );
3126 edit->setReadOnly( true );
3127 enableButtonOk( true );
3128 customChosen = true;
3133 InfoPane::InfoPane( QWidget *parent )
3134 : KVBox( parent ),
3135 m_enable( false ),
3136 m_storedHeight( 100 )
3138 KVBox *container = new KVBox( this );
3139 container->setObjectName( "container" );
3140 container->hide();
3143 KHBox *box = new KHBox( container );
3144 //box->setMargin( 3 );
3145 box->setBackgroundMode( Qt::PaletteBase );
3147 m_infoBrowser = new KHTMLPart( box );
3149 container->setFrameStyle( QFrame::StyledPanel );
3150 container->setMargin( 3 );
3151 container->setBackgroundMode( Qt::PaletteBase );
3154 m_pushButton = new KPushButton( KGuiItem( i18n("&Show Extended Info"), "info" ), this );
3155 m_pushButton->setToggleButton( true );
3156 m_pushButton->setEnabled( m_enable );
3157 connect( m_pushButton, SIGNAL(toggled( bool )), SLOT(toggle( bool )) );
3159 //Set the height to fixed. The button shouldn't be resized.
3160 setFixedHeight( m_pushButton->sizeHint().height() );
3163 InfoPane::~InfoPane()
3165 // Ensure the KHTMLPart dies before its KHTMLView dies,
3166 // because KHTMLPart's dtoring relies on its KHTMLView still being alive
3167 // (see bug 130494).
3168 delete m_infoBrowser;
3172 InfoPane::getHeight()
3174 if( findChild<QWidget*>( "container" )->isShown() )
3176 //If the InfoPane is shown, return true height.
3177 return static_cast<QSplitter*>( parentWidget() )->sizes().last();
3180 return m_storedHeight;
3183 void
3184 InfoPane::setStoredHeight( const int newHeight ) {
3185 m_storedHeight = newHeight;
3188 void
3189 InfoPane::toggle( bool toggled )
3191 QSplitter *splitter = static_cast<QSplitter*>( parentWidget() );
3193 if ( !toggled )
3195 //Save the height for later
3196 setStoredHeight( splitter->sizes().last() );
3198 //Set the height to fixed. The button shouldn't be resized.
3199 setFixedHeight( m_pushButton->sizeHint().height() );
3201 //Now the info pane is not shown, we can disable the button if necessary
3202 m_pushButton->setEnabled( m_enable );
3204 else {
3205 setMaximumHeight( ( int )( parentWidget()->height() / 1.5 ) );
3207 //Restore the height of the InfoPane (change the splitter properties)
3208 //Done every time since the pane forgets its height if you try to resize it while the info is hidden.
3209 Q3ValueList<int> sizes = splitter->sizes();
3210 const int sizeOffset = getHeight() - sizes.last();
3211 sizes.first() -= sizeOffset;
3212 sizes.last() += sizeOffset;
3213 splitter->setSizes( sizes );
3215 setMinimumHeight( 150 );
3218 findChild<QWidget*>( "container" )->setShown( toggled );
3221 void
3222 InfoPane::setInfo( const QString &title, const QString &info )
3224 //If the info pane is not shown, we can enable or disable the button depending on
3225 //whether there is content to show. Otherwise, just remember what we wanted to do
3226 //so we can do it later, when the user does hide the pane.
3227 m_enable = !( info.isEmpty() && title.isEmpty() );
3228 if ( !findChild<QWidget*>("container")->isShown() )
3229 m_pushButton->setEnabled( m_enable );
3231 if( m_pushButton->isOn() )
3232 toggle( !(info.isEmpty() && title.isEmpty()) );
3234 m_infoBrowser->write(
3235 m_enable ?
3236 QString( "<div id='extended_box' class='box'>"
3237 "<div id='extended_box-header-title' class='box-header'>"
3238 "<span id='extended_box-header-title' class='box-header-title'>"
3239 " %1 "
3240 "</span>"
3241 "</div>"
3242 "<table id='extended_box-table' class='box-body' width='100%' cellpadding='0' cellspacing='0'>"
3243 "<tr>"
3244 "<td id='extended_box-information-td'>"
3245 " %2 "
3246 "</td>"
3247 "</tr>"
3248 "</table>"
3249 "</div>" ).arg( title, info ) :
3250 QString() );
3253 #include "playlistbrowser.moc"